Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(docker): implement DB snapshot and create default set of users #599

Merged
merged 2 commits into from
Aug 26, 2024
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
6 changes: 4 additions & 2 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ process.env.NODE_ENV = 'development'
process.env.npm_package_name = 'nextcloud-cypress'

/* eslint-disable import/first */
import { configureNextcloud, startNextcloud, stopNextcloud, waitOnNextcloud } from './lib/docker'
import { configureNextcloud, createSnapshot, setupUsers, startNextcloud, stopNextcloud, waitOnNextcloud } from './lib/docker'
import { defineConfig } from 'cypress'
import CodeCoverage from '@cypress/code-coverage/task'
import webpackConfig from '@nextcloud/webpack-vue-config'
Expand Down Expand Up @@ -61,7 +61,9 @@ export default defineConfig({
return ip
})
.then(waitOnNextcloud as (ip: string) => Promise<undefined>) // void !== undefined for Typescript
.then(configureNextcloud)
.then(configureNextcloud as () => Promise<undefined>)
.then(setupUsers as () => Promise<undefined>)
.then(() => createSnapshot('init'))
.then(() => {
return config
})
Expand Down
16 changes: 16 additions & 0 deletions cypress/e2e/sessions.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ describe('Login and logout', function() {
})
})

it('Login with a pre-existing user and logout', function() {
cy.login(new User('test1', 'test1'))

cy.visit('/apps/files')
cy.url().should('include', '/apps/files')

cy.window().then(window => {
expect(window?.OC?.currentUser).to.eq('test1')
})

cy.logout()

cy.visit('/apps/files')
cy.url().should('include', '/login')
})

it('Login with a different user without logging out', function() {
cy.createRandomUser().then((user) => {
cy.login(user)
Expand Down
44 changes: 44 additions & 0 deletions cypress/e2e/snapshots.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '../../dist'
import { randHash } from '../utils'

describe('Create a snapshot and a user', function() {
let snapshot: string

it('Create a snapshot', function() {
cy.createDBSnapshot().then(_snapshot => {
snapshot = _snapshot
})
})

const hash = 'user' + randHash()
const user = new User(hash, 'password')
it('Create a user and login', function() {
cy.createUser(user).then(() => {
cy.login(user)
})

cy.visit('/apps/files')
cy.url().should('include', '/apps/files')

cy.listUsers().then(users => {
expect(users).to.contain(user.userId)
})
})

it('Restore the snapshot', function() {
cy.restoreDBSnapshot(snapshot)
})

it('Fail login with the user', function() {
cy.visit('/apps/files')
cy.url().should('include', '/login')

cy.listUsers().then(users => {
expect(users).to.not.contain(user.userId)
})
})
})
1 change: 1 addition & 0 deletions lib/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
export * from './getNc'
export * from './sessions'
export * from './users'
export * from './snapshots'
28 changes: 28 additions & 0 deletions lib/commands/snapshots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { basename } from 'path'

const getContainerName = function(): Cypress.Chainable<string> {
return cy.exec('pwd').then(({ stdout }) => {
const app = basename(stdout).replace(' ', '')
return cy.wrap(`nextcloud-cypress-tests_${app}`)
})
}

export const createDBSnapshot = function(snapshot?: string): Cypress.Chainable<string> {
const hash = new Date().toISOString().replace(/[^0-9]/g, '')
getContainerName().then(name => {
cy.exec(`docker exec --user www-data ${name} cp /var/www/html/data/owncloud.db /var/www/html/data/owncloud.db-${snapshot ?? hash}`)
})
cy.log(`Created snapshot ${snapshot ?? hash}`)
return cy.wrap(snapshot ?? hash)
}

export const restoreDBSnapshot = function(snapshot: string = 'init') {
getContainerName().then(name => {
cy.exec(`docker exec --user www-data ${name} cp /var/www/html/data/owncloud.db-${snapshot} /var/www/html/data/owncloud.db`)
})
cy.log(`Restored snapshot ${snapshot}`)
}
49 changes: 44 additions & 5 deletions lib/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export async function startNextcloud(branch = 'master', mountApp: boolean|string
let appId: string|undefined
let appVersion: string|undefined
if (appPath) {
console.log('Mounting app directory')
console.log('Mounting app directories…')
while (appPath) {
const appInfoPath = resolve(join(appPath, 'appinfo', 'info.xml'))
if (existsSync(appInfoPath)) {
Expand All @@ -110,7 +110,7 @@ export async function startNextcloud(branch = 'master', mountApp: boolean|string

try {
// Pulling images
console.log('Pulling images… ⏳')
console.log('\nPulling images… ⏳')
await new Promise((resolve, reject) => docker.pull(SERVER_IMAGE, (_err, stream: Stream) => {
const onFinished = function(err: Error | null) {
if (!err) {
Expand Down Expand Up @@ -262,11 +262,48 @@ export const configureNextcloud = async function(apps = ['viewer'], vendoredBran
await runExec(container, ['php', 'occ', 'app:install', '--force', app], true)
}
}
// await runExec(container, ['php', 'occ', 'app:list'], true)

console.log('└─ Nextcloud is now ready to use 🎉')
}

/**
* Setup test users
*
* @param {Container|undefined} container Optional server container to use (defaults to current container)
*/
export const setupUsers = async function(container?: Container) {
console.log('\nCreating test users… 👤')
const users = ['test1', 'test2', 'test3', 'test4', 'test5']
for (const user of users) {
await runExec(container ?? getContainer(), ['php', 'occ', 'user:add', user, '--password-from-env'], true, 'www-data', ['OC_PASS=' + user])
}
console.log('└─ Done')
}

/**
* Create a snapshot of the current database
* @param {string|undefined} snapshot Name of the snapshot (default is a timestamp)
* @param {Container|undefined} container Optional server container to use (defaults to current container)
* @return Promise resolving to the snapshot name
*/
export const createSnapshot = async function(snapshot?: string, container?: Container): Promise<string> {
const hash = new Date().toISOString().replace(/[^0-9]/g, '')
console.log('\nCreating init DB snapshot…')
await runExec(container ?? getContainer(), ['cp', '/var/www/html/data/owncloud.db', `/var/www/html/data/owncloud.db-${snapshot ?? hash}`], true)
console.log('└─ Done')
return snapshot ?? hash
}

/**
* Restore a snapshot of the database
* @param {string|undefined} snapshot Name of the snapshot (default is 'init')
* @param {Container|undefined} container Optional server container to use (defaults to current container)
*/
export const restoreSnapshot = async function(snapshot = 'init', container?: Container) {
console.log('\nRestoring DB snapshot…')
await runExec(container ?? getContainer(), ['cp', `/var/www/html/data/owncloud.db-${snapshot}`, '/var/www/html/data/owncloud.db'], true)
console.log('└─ Done')
}

/**
* Force stop the testing container
*/
Expand Down Expand Up @@ -323,13 +360,15 @@ const runExec = async function(
container: Docker.Container,
command: string[],
verbose = false,
user = 'www-data'
user = 'www-data',
env: string[] = [],
) {
const exec = await container.exec({
Cmd: command,
AttachStdout: true,
AttachStderr: true,
User: user,
Env: env,
})

return new Promise<string>((resolve, reject) => {
Expand Down
17 changes: 16 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
import { getNc } from "./commands"
import { login, logout } from "./commands/sessions"
import { createDBSnapshot, restoreDBSnapshot } from "./commands/snapshots"
import { User, createRandomUser, createUser, deleteUser, modifyUser, listUsers, getUserData, enableUser } from "./commands/users"
import type { Selector } from "./selectors"

Expand Down Expand Up @@ -75,13 +76,25 @@ declare global {
* @param enable True to enable, false to disable (default is enable)
*/
enableUser(user: User, enable?: boolean): Cypress.Chainable<Cypress.Response<any>>

/**
*
* Query metadata for, and in behalf, of a given user
*
* @param user User whom metadata to query
*/
getUserData(user: User): Cypress.Chainable<Cypress.Response<any>>
getUserData(user: User): Cypress.Chainable<Cypress.Response<any>>

/**
* Create a snapshot of the current database
*/
createDBSnapshot(snapshot?: string): Cypress.Chainable<string>,

/**
* Restore a snapshot of the database
* Default is the post-setup state
*/
restoreDBSnapshot(snapshot?: string): Cypress.Chainable,
}
}
}
Expand All @@ -104,6 +117,8 @@ export const addCommands = function() {
Cypress.Commands.add('modifyUser', modifyUser)
Cypress.Commands.add('enableUser', enableUser)
Cypress.Commands.add('getUserData', getUserData)
Cypress.Commands.add('createDBSnapshot', createDBSnapshot)
Cypress.Commands.add('restoreDBSnapshot', restoreDBSnapshot)
}

export { User }
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"build:doc": "typedoc --out dist/doc lib/commands lib/selectors lib && touch dist/doc/.nojekyll",
"build:instrumented": "rollup --config rollup.instrumented.mjs",
"dev": "echo 'No dev build available, production only' && npm run build",
"watch": "rollup --config rollup.config.js --watch",
"watch": "rollup --config rollup.config.mjs --watch",
"test": "jest --passWithNoTests",
"test:watch": "jest --watchAll --verbose true --passWithNoTests",
"test:coverage": "jest --coverage --passWithNoTests",
Expand Down
Loading