Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,4 @@ jobs:
name: cypress-summary
steps:
- name: Summary status
run: if ${{ (needs.cypress-e2e.result != 'success' && needs.cypress-e2e.result != 'skipped') || (needs.cypress-component.result != 'success' && needs.cypress-component.result != 'skipped') }}; then exit 1; fi
run: if ${{ needs.cypress-e2e.result != 'success' && needs.cypress-e2e.result != 'skipped' }}; then exit 1; fi
11 changes: 7 additions & 4 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ process.env.npm_package_name = 'nextcloud-e2e-test-server'

import { configureNextcloud, createSnapshot, setupUsers, startNextcloud, stopNextcloud, waitOnNextcloud } from './lib/docker'
import { defineConfig } from 'cypress'
import vitePreprocessor from 'cypress-vite'

export default defineConfig({
projectId: 'h2z7r3',
Expand All @@ -23,22 +24,24 @@ export default defineConfig({
testIsolation: false,

setupNodeEvents(on, config) {
on('file:preprocessor', vitePreprocessor({ configFile: false }))

// Remove container after run
on('after:run', () => {
stopNextcloud()
})

// Before the browser launches
// starting Nextcloud testing container
return startNextcloud(process.env.BRANCH)
return startNextcloud(process.env.BRANCH, false, { forceRecreate: true })
.then((ip) => {
// Setting container's IP as base Url
config.baseUrl = `http://${ip}/index.php`
return ip
})
.then(waitOnNextcloud as (ip: string) => Promise<undefined>) // void !== undefined for Typescript
.then(configureNextcloud as () => Promise<undefined>)
.then(setupUsers as () => Promise<undefined>)
.then((ip) => waitOnNextcloud(ip))
.then(() => configureNextcloud())
.then(() => setupUsers())
.then(() => createSnapshot('init'))
.then(() => {
return config
Expand Down
1 change: 0 additions & 1 deletion cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,4 @@
// https://on.cypress.io/configuration
// ***********************************************************

import '@cypress/code-coverage/support'
import './commands'
2 changes: 2 additions & 0 deletions cypress/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"extends": "../tsconfig.json",
"exclude": [],
"include": ["./**/*.ts"],
"compilerOptions": {
"types": ["cypress", "node"],
Expand All @@ -14,6 +15,7 @@
"strict": true,
"noImplicitAny": false,
"outDir": "./dist",
"rootDir": "..",
"ignoreDeprecations": "6.0"
},
}
4 changes: 3 additions & 1 deletion lib/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Selector } from "./selectors"

import { User } from "./User"
import { getNc, restoreState, runCommand, runOccCommand, saveState } from "./commands"
import { login, logout } from "./commands/sessions"
import { createRandomUser, createUser, deleteUser, modifyUser, listUsers, getUserData, enableUser } from "./commands/users"
import type { Selector } from "./selectors"

declare global {
namespace Cypress {
Expand Down
84 changes: 55 additions & 29 deletions lib/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,9 @@ import { basename, join, resolve, sep } from 'path'
import { existsSync, readFileSync } from 'fs'
import { XMLParser } from 'fast-xml-parser'

import { User } from './User'
import { User } from './User.ts'

const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server'
const VENDOR_APPS: Record<string, string> = {
text: 'https://github.com/nextcloud/text.git',
viewer: 'https://github.com/nextcloud/viewer.git',
notifications: 'https://github.com/nextcloud/notifications.git',
activity: 'https://github.com/nextcloud/activity.git',
}

export const docker = new Docker()

Expand Down Expand Up @@ -268,13 +262,20 @@ export const configureNextcloud = async function(apps = ['viewer'], vendoredBran
} else if (app in applist.disabled) {
// built in or mounted already as the app under development
await runOcc(['app:enable', '--force', app], { container, verbose: true })
} else if (app in VENDOR_APPS) {
// apps that are vendored but still missing (i.e. not build in or mounted already)
await runExec(['git', 'clone', '--depth=1', `--branch=${vendoredBranch}`, VENDOR_APPS[app], `apps/${app}`], { container, verbose: true })
await runOcc(['app:enable', '--force', app], { container, verbose: true })
} else {
// try appstore
await runOcc(['app:install', '--force', app], { container, verbose: true })
const { shippedApps } = JSON.parse(await runExec(['cat', 'core/shipped.json'], { container }))
if (shippedApps.includes(app)) {
const branchOption = ['main', 'master'].includes(vendoredBranch) ? [] : [`--branch=${vendoredBranch}`]
// apps that are vendored but still missing (i.e. not build in or mounted already)
await runExec(
['git', 'clone', '--depth=1', ...branchOption, `https://github.com/nextcloud/${encodeURIComponent(app)}.git`, `apps/${app}`],
{ container, verbose: true },
)
await runOcc(['app:enable', '--force', app], { container, verbose: true })
} else {
// try appstore
await runOcc(['app:install', '--force', app], { container, verbose: true })
}
}
}
console.log('└─ Nextcloud is now ready to use 🎉')
Expand Down Expand Up @@ -339,7 +340,7 @@ export const stopNextcloud = async function() {
*
* @param container name of the container
*/
export const getContainerIP = async function(
export async function getContainerIP(
container = getContainer()
): Promise<string> {
const containerInspect = await container.inspect()
Expand All @@ -354,15 +355,17 @@ export const getContainerIP = async function(
while (ip === '' && tries < 10) {
tries++

await container.inspect((_err, data) => {
ip = data?.NetworkSettings?.Networks?.default?.IPAddress
// @ts-expect-error -- fallback to legacy network settings
|| data?.NetworkSettings?.IPAddress
|| ''
})

if (ip !== '') {
break
try {
const containerInfo = await container.inspect()
const network = containerInfo.NetworkSettings.Networks.default
|| containerInfo.NetworkSettings.Networks.bridge
|| Object.values(containerInfo.NetworkSettings.Networks)[0]
if (network.IPAddress) {
ip = network.IPAddress
break
}
} catch {
// ignore and retry
}

await sleep(1000 * tries)
Expand All @@ -383,18 +386,34 @@ export const waitOnNextcloud = async function(ip: string) {
}

interface RunExecOptions {
container: Docker.Container;
user: string;
env: string[];
verbose: boolean;
/**
* The container to run the command in. If not provided, the current container will be used.
*/
container: Docker.Container
/**
* The user to run the command as. Defaults to 'www-data'.
*/
user: string
/**
* The command will throw an error if it exits with a non-zero exit code. Defaults to true.
*/
failOnError: boolean
/**
* Environment variables to set for the command. Defaults to an empty array.
*/
env: string[]
/**
* If true, the command's output will be printed to the console. Defaults to false.
*/
verbose: boolean
}

/**
* Execute a command in the container
*/
export const runExec = async function(
command: string | string[],
{ container, user='www-data', verbose=false, env=[] }: Partial<RunExecOptions> = {},
{ container, user='www-data', verbose=false, env=[], failOnError=true }: Partial<RunExecOptions> = {},
) {
container = container || getContainer()
const exec = await container.exec({
Expand Down Expand Up @@ -427,7 +446,14 @@ export const runExec = async function(
}
})
dataStream.on('error', (err) => reject(err))
dataStream.on('end', () => resolve(data.join('')))
dataStream.on('end', async () => {
const result = await exec.inspect()
if (result.ExitCode && failOnError) {
reject(new Error(data.join(''), { cause: result.ExitCode }))
return
}
resolve(data.join(''))
})
})
}

Expand Down
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,15 @@
"build": "vite --mode production build",
"build:doc": "typedoc --out dist/doc lib/commands lib/selectors/index.ts lib && touch dist/doc/.nojekyll",
"build:instrumented": "vite --mode instrumented build",
"cypress": "npm run cypress:e2e && npm run cypress:component",
"cypress": "npm run cypress:e2e",
"cypress:e2e": "cypress run --e2e",
"cypress:gui": "cypress open",
"dev": "echo 'No dev build available, production only' && npm run build",
"start:nextcloud": "node playwright/start-nextcloud-server.mjs",
"test": "npm run build && playwright install chromium --only-shell && playwright test",
"test": "npm run test:node && npm run test:playwright",
"test:node": "ts-node --esm tests/*.spec.ts",
"test:playwright": "playwright test",
"pretest": "playwright install chromium --only-shell",
"watch": "vite --mode production build --watch"
},
"dependencies": {
Expand All @@ -86,6 +89,7 @@
"@types/dockerode": "^4.0.1",
"@types/wait-on": "^5.3.4",
"cypress": "^15.18.0",
"cypress-vite": "^1.10.2",
"ts-node": "^10.9.2",
"tslib": "^2.6.2",
"typedoc": "^0.28.0",
Expand Down
35 changes: 35 additions & 0 deletions tests/docker.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { after, before, describe, test } from 'node:test'
import { configureNextcloud, getContainer, runExec, startNextcloud, stopNextcloud, waitOnNextcloud } from '../lib/docker.ts'

describe('Docker: Pre-installation of apps', async () => {
before(async () => {
const ip = await startNextcloud('master', false)
await waitOnNextcloud(ip)
await configureNextcloud(['viewer', 'text', 'forms'])
})

after(async () => await stopNextcloud())

await test('Additional apps: Default mapping works', async () => {
const container = getContainer()
// this must not throw
await runExec(['file', '-f', 'apps/viewer/appinfo/info.xml'], { container, failOnError: true })
})

await test('Additional apps: Mapping "main" branches', async () => {
const container = getContainer()
// this must not throw
await runExec(['file', '-f', 'apps/text/appinfo/info.xml'], { container, failOnError: true})
})

await test('Additional apps: fetching from appstore works', async () => {
const container = getContainer()
// this must not throw
await runExec(['file', '-f', 'apps/forms/appinfo/info.xml'], { container, failOnError: true})
})
})
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"include": ["./lib"],
"exclude": ["./cypress"],
"compilerOptions": {
"lib": [
"ES2024",
Expand Down
Loading