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
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
- Max <max@nextcloud.com>
- max-nextcloud <max@nextcloud.com>
- Pytal <24800714+Pytal@users.noreply.github.com>
- Tobias Knöppler <tobias@knoeppler.org>
47 changes: 30 additions & 17 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,20 @@
process.env.NODE_ENV = 'development'
process.env.npm_package_name = 'nextcloud-e2e-test-server'

import type { RunExecOptions, RunExecResult } from './lib/docker.ts'

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

export default defineConfig({
projectId: 'h2z7r3',
Expand All @@ -24,29 +35,31 @@ export default defineConfig({
// Disable session isolation
testIsolation: false,

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

// Remove container after run
on('after:run', () => {
stopNextcloud()
on('after:run', async () => {
await stopNextcloud()
await docker.getVolume('apps_writable').remove()
})

on('task', {
async runExec({ command, options }: { command: string | string[], options: Partial<RunExecOptions> }): Promise<RunExecResult> {
return await dockerRunExec(command, options)
},
})

// Before the browser launches
// starting Nextcloud testing container
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((ip) => waitOnNextcloud(ip))
.then(() => configureNextcloud())
.then(() => setupUsers())
.then(() => createSnapshot('init'))
.then(() => {
return config
})
const ip = await startNextcloud(process.env.BRANCH, false, { forceRecreate: true, exposePort: 8086 })
// Setting container's IP as base Url
config.baseUrl = `http://${ip}/index.php`
await waitOnNextcloud(ip)
await configureNextcloud()
await setupUsers()
await createSnapshot('init')
return config
},
},
})
2 changes: 1 addition & 1 deletion cypress/e2e/docker.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('Run command', function() {
})

it('hands on the env', function() {
cy.runCommand('env', { env: { DATA: 'Hello' } })
cy.runCommand('env', { env: ['DATA=Hello'] })
.its('stdout')
.should('contain', 'DATA')
.should('contain', 'Hello')
Expand Down
32 changes: 15 additions & 17 deletions lib/commands/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,29 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { basename } from '@nextcloud/paths'
import type { RunExecOptions, RunExecResult } from '../docker.ts'

/**
*
*/
function getContainerName(): Cypress.Chainable<string> {
return cy.exec('pwd').then(({ stdout }) => {
const name = basename(stdout).replace(' ', '')
return cy.wrap(`nextcloud-e2e-test-server_${name}`)
})
const defaultOptions = {
failOnError: true,
user: 'www-data',
verbose: false,
}

/**
*
* @param command

Check warning on line 16 in lib/commands/docker.ts

View workflow job for this annotation

GitHub Actions / eslint

Missing JSDoc @param "command" description
* @param options

Check warning on line 17 in lib/commands/docker.ts

View workflow job for this annotation

GitHub Actions / eslint

Missing JSDoc @param "options" description
*/
export function runCommand(command: string, options?: Partial<Cypress.ExecOptions>): Cypress.Chainable<Cypress.Exec> {
export function runCommand(command: string[] | string, options?: Partial<RunExecOptions>): Cypress.Chainable<RunExecResult> {
const env = Object.entries(options?.env ?? {})
.map(([name, value]) => `-e '${name}=${value}'`)
.join(' ')
.map(([name, value]) => `${name}=${value}`)

return getContainerName()
.then((containerName) => {
// Wrapping command inside bash -c "..." to allow using '*'.
return cy.exec(`docker exec --user www-data --workdir /var/www/html ${env} ${containerName} bash -c "${command}"`, options)
})
return cy.task('runExec', {
command,
options: {
...defaultOptions,
...options,
env,
},
})
}
6 changes: 4 additions & 2 deletions lib/commands/occ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { RunExecOptions, RunExecResult } from '../docker.ts'

import { runCommand } from './docker.ts'

/**
*
* @param command

Check warning on line 12 in lib/commands/occ.ts

View workflow job for this annotation

GitHub Actions / eslint

Missing JSDoc @param "command" description
* @param options

Check warning on line 13 in lib/commands/occ.ts

View workflow job for this annotation

GitHub Actions / eslint

Missing JSDoc @param "options" description
*/
export function runOccCommand(command: string, options?: Partial<Cypress.ExecOptions>): Cypress.Chainable<Cypress.Exec> {
return runCommand(`php ./occ ${command}`, options)
export function runOccCommand(command: string, options?: Partial<RunExecOptions>): Cypress.Chainable<RunExecResult> {
return runCommand(['php', 'occ', command], { verbose: true, ...options })
}
21 changes: 10 additions & 11 deletions lib/commands/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import { runCommand } from './docker.ts'
export function saveState(): Cypress.Chainable<string> {
const snapshot = Math.random().toString(36).substring(7)

runCommand(`rm /var/www/html/data-${snapshot}.tar`, { failOnNonZeroExit: false })
runCommand(['rm', `/var/www/html/data-${snapshot}.tar`], { failOnError: false, verbose: true })
// The instance keeps writing into ./data while we archive it (e.g. the
// nextcloud.log, caches, session files), so a file can change mid-read and
// GNU tar exits 1 with "file changed as we read it". That is harmless for a
// test-state snapshot: silence the warning and treat exit 1 (benign,
// "some files differ") as success while still failing on a real error
// (exit 2, fatal).
runCommand(`tar --warning=no-file-changed -cf /var/www/html/data-${snapshot}.tar ./data || [ $? -eq 1 ]`)
runCommand(['bash', '-c', `tar --warning=no-file-changed -cf /var/www/html/data-${snapshot}.tar ./data || [ $? -eq 1 ]`], { verbose: true })

cy.log(`Created snapshot ${snapshot}`)

Expand All @@ -31,13 +31,12 @@ export function saveState(): Cypress.Chainable<string> {
* @param snapshot - The name of the snapshot to restore. If not provided, the default snapshot 'init' will be used.
*/
export function restoreState(snapshot: string = 'init') {
runCommand('rm -vfr ./data/*')
runCommand(`tar -xf '/var/www/html/data-${snapshot}.tar'`)

// Any user sessions created between saveState() and restoreState()
// are not present in the database, but exist in the web server.
// Using them leads to unknown behavior, so we clear them all to prevent session errors.
Cypress.session.clearAllSavedSessions()

cy.log(`Restored snapshot ${snapshot}`)
runCommand(['bash', '-c', 'rm -vfr ./data/*'], { verbose: true }).then(() => {
runCommand(['bash', '-c', `tar -xf '/var/www/html/data-${snapshot}.tar'`], { verbose: true }).then(() => {
// Any user sessions created between saveState() and restoreState()
// are not present in the database, but exist in the web server.
// Using them leads to unknown behavior, so we clear them all to prevent session errors.
Cypress.session.clearAllSavedSessions().then(() => cy.log(`Restored snapshot ${snapshot}`))
})
})
}
5 changes: 3 additions & 2 deletions lib/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { RunExecOptions, RunExecResult } from './docker.ts'
import type { Selector } from './selectors/index.ts'

import { getNc, restoreState, runCommand, runOccCommand, saveState } from './commands/index.ts'
Expand Down Expand Up @@ -109,12 +110,12 @@ declare global {
* Run a command in the docker container
*
*/
runCommand(command: string, options?: Partial<Cypress.ExecOptions>): Cypress.Chainable<Cypress.Exec>
runCommand(command: string, options?: Partial<RunExecOptions>): Cypress.Chainable<RunExecResult>

/**
* Run an occ command
*/
runOccCommand(command: string, options?: Partial<Cypress.ExecOptions>): Cypress.Chainable<Cypress.Exec>
runOccCommand(command: string, options?: Partial<RunExecOptions>): Cypress.Chainable<RunExecResult>
}
}
}
Expand Down
21 changes: 10 additions & 11 deletions lib/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { User } from './User.ts'

const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server'

export const docker = new Docker()
export const docker = new Docker({ socketPath: process.env.DOCKER_SOCKET ?? '/var/run/docker.sock' })

// Store the container name, different names are used to prevent conflicts when testing multiple apps locally
let _containerName: string | null = null
Expand Down Expand Up @@ -164,12 +164,6 @@ export async function startNextcloud(branch = 'master', mountApp: boolean | stri
Image: SERVER_IMAGE,
name: getContainerName(),
Env: [`BRANCH=${branch}`, 'APCU=1'],
Volumes: {
apps_writable: {
Mountpoint: '/var/www/html/apps-writable',
Readonly: false,
},
},
HostConfig: {
Binds: mounts.length > 0 ? mounts : undefined,
PortBindings,
Expand All @@ -180,6 +174,11 @@ export async function startNextcloud(branch = 'master', mountApp: boolean | stri
Source: '',
Type: 'tmpfs',
ReadOnly: false,
}, {
Target: '/var/www/html/apps-writable',
Source: 'apps_writable',
Type: 'volume',
ReadOnly: false,
}],
},
})
Expand Down Expand Up @@ -268,7 +267,7 @@ export async function configureNextcloud(apps = ['viewer'], vendoredBranch?: str

console.log('├─ Using "apps-writable" folder for mounted apps')
await runExec(['mkdir', '-p', '/var/www/html/apps-writable'], { container })
await runExec(['chown', 'www-data:www-data', '/var/www/html/apps-writable'], { container })
await runExec(['chown', 'www-data:www-data', '/var/www/html/apps-writable'], { container, user: 'root' })
const appsConfig = `<?php
$CONFIG = [
'apps_paths' => [
Expand Down Expand Up @@ -364,7 +363,7 @@ export async function stopNextcloud() {
try {
const container = getContainer()
console.log('Stopping Nextcloud container…')
container.remove({ force: true })
await container.remove({ force: true })
console.log('└─ Nextcloud container removed 🥀')
} catch (err) {
console.log(err)
Expand Down Expand Up @@ -419,7 +418,7 @@ export async function waitOnNextcloud(ip: string) {
console.log('└─ Done')
}

interface RunExecOptions {
export interface RunExecOptions {
/**
* The container to run the command in. If not provided, the current container will be used.
*/
Expand All @@ -442,7 +441,7 @@ interface RunExecOptions {
verbose: boolean
}

type RunExecResult = {
export type RunExecResult = {
stdout: string
stderr: string
exitCode: number
Expand Down
3 changes: 2 additions & 1 deletion package-lock.json

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

7 changes: 4 additions & 3 deletions playwright/start-nextcloud-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { startNextcloud, stopNextcloud } from '@nextcloud/e2e-test-server/docker'
import { docker, startNextcloud, stopNextcloud } from '@nextcloud/e2e-test-server/docker'
import { readFileSync } from 'fs'

/**
Expand Down Expand Up @@ -33,8 +33,9 @@ function getBranch() {
// Start the Nextcloud docker container
await start()
// Listen for process to exit (tests done) and shut down the docker container
process.on('beforeExit', () => {
stopNextcloud()
process.on('beforeExit', async () => {
await stopNextcloud()
await docker.getVolume('apps_writable').remove()
})

// Idle to wait for shutdown
Expand Down
2 changes: 1 addition & 1 deletion playwright/support/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ import { test as setup } from '@playwright/test'
* as that only checks for the URL to be accessible which happens already before everything is configured.
*/
setup('Configure Nextcloud', async () => {
const appsToInstall = []
const appsToInstall: string[] = []
await configureNextcloud(appsToInstall)
})
9 changes: 6 additions & 3 deletions tests/docker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@

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

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

after(async () => await stopNextcloud())
after(async () => {
await stopNextcloud()
await docker.getVolume('apps_writable').remove()
})

await test('Additional apps: Default mapping works', async () => {
const container = getContainer()
Expand Down
9 changes: 6 additions & 3 deletions tests/runExec.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@ import type { Container } from 'dockerode'

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

describe('Docker: runExec', async () => {
let container: Container

before(async () => {
const ip = await startNextcloud('master', false)
const ip = await startNextcloud('master', false, { exposePort: 8087 })
await waitOnNextcloud(ip)
container = getContainer()
})

after(async () => await stopNextcloud())
after(async () => {
await stopNextcloud()
await docker.getVolume('apps_writable').remove()
})

await test('captures stdout of a command', async () => {
const { stdout, stderr, exitCode } = await runExec(['echo', 'hello world'], { container })
Expand Down
Loading