Skip to content

Commit

Permalink
Merge pull request #599 from nextcloud-libraries/perf/db-snapshot
Browse files Browse the repository at this point in the history
feat(docker): implement DB snapshot and create default set of users
  • Loading branch information
susnux committed Aug 26, 2024
2 parents d9ec2e5 + fa19a80 commit bdbebc7
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 9 deletions.
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

0 comments on commit bdbebc7

Please sign in to comment.