Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
test: make cypress run in secure context and add WebAuthn tests
Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux authored and AndyScherzinger committed Apr 1, 2025
commit 2d48c02913b4a4a3a7d29f806299dff7570fa26f
5 changes: 4 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ module.exports = {
jsdoc: {
mode: 'typescript',
},
'import/resolver': {
typescript: {}, // this loads <rootdir>/tsconfig.json to eslint
},
},
overrides: [
// Allow any in tests
Expand All @@ -43,6 +46,6 @@ module.exports = {
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
},
}
},
],
}
14 changes: 12 additions & 2 deletions cypress/dockerNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ export const startNextcloud = async function(branch: string = getCurrentGitBranc
Type: 'tmpfs',
ReadOnly: false,
}],
PortBindings: {
'80/tcp': [{
HostIP: '0.0.0.0',
HostPort: '8083',
}],
},
},
Env: [
`BRANCH=${branch}`,
Expand Down Expand Up @@ -242,11 +248,15 @@ export const getContainerIP = async function(
while (ip === '' && tries < 10) {
tries++

await container.inspect(function(err, data) {
container.inspect(function(err, data) {
if (err) {
throw err
}
ip = data?.NetworkSettings?.IPAddress || ''
if (data?.HostConfig.PortBindings?.['80/tcp']?.[0]?.HostPort) {
ip = `localhost:${data.HostConfig.PortBindings['80/tcp'][0].HostPort}`
} else {
ip = data?.NetworkSettings?.IPAddress || ''
}
})

if (ip !== '') {
Expand Down
41 changes: 16 additions & 25 deletions cypress/e2e/files/LivePhotosUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,25 @@ type SetupInfo = {
}

/**
*
* @param user
* @param fileName
* @param domain
* @param requesttoken
* @param metadata
*/
function setMetadata(user: User, fileName: string, requesttoken: string, metadata: object) {
cy.url().then(url => {
const hostname = new URL(url).hostname
cy.request({
method: 'PROPPATCH',
url: `http://${hostname}/remote.php/dav/files/${user.userId}/${fileName}`,
auth: { user: user.userId, pass: user.password },
headers: {
requesttoken,
},
body: `<?xml version="1.0"?>
<d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.org/ns">
<d:set>
<d:prop>
${Object.entries(metadata).map(([key, value]) => `<${key}>${value}</${key}>`).join('\n')}
</d:prop>
</d:set>
</d:propertyupdate>`,
})
const base = Cypress.config('baseUrl')!.replace(/\/index\.php\/?/, '')
cy.request({
method: 'PROPPATCH',
url: `${base}/remote.php/dav/files/${user.userId}/${fileName}`,
auth: { user: user.userId, pass: user.password },
headers: {
requesttoken,
},
body: `<?xml version="1.0"?>
<d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.org/ns">
<d:set>
<d:prop>
${Object.entries(metadata).map(([key, value]) => `<${key}>${value}</${key}>`).join('\n')}
</d:prop>
</d:set>
</d:propertyupdate>`,
})

}

/**
Expand Down
61 changes: 39 additions & 22 deletions cypress/e2e/files_external/files-user-credentials.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@ describe('Files user credentials', { testIsolation: true }, () => {
let user2: User
let storageUser: User

beforeEach(() => {
})

before(() => {
cy.runOccCommand('app:enable files_external')

// Create some users
cy.createRandomUser().then((user) => user1 = user)
cy.createRandomUser().then((user) => user2 = user)
cy.createRandomUser().then((user) => {
user1 = user
})
cy.createRandomUser().then((user) => {
user2 = user
})

// This user will hold the webdav storage
cy.createRandomUser().then((user) => {
Expand All @@ -34,7 +35,7 @@ describe('Files user credentials', { testIsolation: true }, () => {

after(() => {
// Cleanup global storages
cy.runOccCommand(`files_external:list --output=json`).then(({stdout}) => {
cy.runOccCommand('files_external:list --output=json').then(({ stdout }) => {
const list = JSON.parse(stdout)
list.forEach((storage) => cy.runOccCommand(`files_external:delete --yes ${storage.mount_id}`), { failOnNonZeroExit: false })
})
Expand All @@ -43,8 +44,10 @@ describe('Files user credentials', { testIsolation: true }, () => {
})

it('Create a user storage with user credentials', () => {
const url = Cypress.config('baseUrl') + '/remote.php/dav/files/' + storageUser.userId
createStorageWithConfig(storageUser.userId, StorageBackend.DAV, AuthBackend.UserProvided, { host: url.replace('index.php/', ''), 'secure': 'false' })
// Its not the public server address but the address so the server itself can connect to it
const base = 'http://localhost'
const host = `${base}/remote.php/dav/files/${storageUser.userId}`
createStorageWithConfig(storageUser.userId, StorageBackend.DAV, AuthBackend.UserProvided, { host, secure: 'false' })

cy.login(user1)
cy.visit('/apps/files/extstoragemounts')
Expand All @@ -71,7 +74,8 @@ describe('Files user credentials', { testIsolation: true }, () => {
cy.wait('@setCredentials')

// Auth dialog should be closed and the set credentials button should be gone
authDialog.should('not.exist', { timeout: 2000 })
cy.get('@authDialog').should('not.exist', { timeout: 2000 })

getActionEntryForFile(storageUser.userId, ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist')

// Finally, the storage should be accessible
Expand All @@ -81,8 +85,10 @@ describe('Files user credentials', { testIsolation: true }, () => {
})

it('Create a user storage with GLOBAL user credentials', () => {
const url = Cypress.config('baseUrl') + '/remote.php/dav/files/' + storageUser.userId
createStorageWithConfig('storage1', StorageBackend.DAV, AuthBackend.UserGlobalAuth, { host: url.replace('index.php/', ''), 'secure': 'false' })
// Its not the public server address but the address so the server itself can connect to it
const base = 'http://localhost'
const host = `${base}/remote.php/dav/files/${storageUser.userId}`
createStorageWithConfig('storage1', StorageBackend.DAV, AuthBackend.UserGlobalAuth, { host, secure: 'false' })

cy.login(user2)
cy.visit('/apps/files/extstoragemounts')
Expand All @@ -93,23 +99,32 @@ describe('Files user credentials', { testIsolation: true }, () => {
triggerInlineActionForFile('storage1', ACTION_CREDENTIALS_EXTERNAL_STORAGE)

// See credentials dialog
const storageDialog = cy.findByRole('dialog', { name: 'Storage credentials' })
storageDialog.should('be.visible')
storageDialog.findByRole('textbox', { name: 'Login' }).type(storageUser.userId)
storageDialog.get('input[type="password"]').type(storageUser.password)
storageDialog.get('button').contains('Confirm').click()
storageDialog.should('not.exist')
cy.findByRole('dialog', { name: 'Storage credentials' })
.as('storageDialog')
cy.get('@storageDialog')
.should('be.visible')
.findByRole('textbox', { name: 'Login' })
.type(storageUser.userId)
cy.get('@storageDialog')
.find('input[type="password"]')
.type(storageUser.password)
cy.get('@storageDialog')
.contains('button', 'Confirm')
.click()
cy.get('@storageDialog')
.should('not.exist')

// Storage dialog now closed, the user auth dialog should be visible
const authDialog = cy.findByRole('dialog', { name: 'Confirm your password' })
authDialog.should('be.visible')
cy.findByRole('dialog', { name: 'Confirm your password' })
.as('authDialog')
.should('be.visible')
handlePasswordConfirmation(user2.password)

// Wait for the credentials to be set
cy.wait('@setCredentials')

// Auth dialog should be closed and the set credentials button should be gone
authDialog.should('not.exist', { timeout: 2000 })
cy.get('@authDialog').should('not.exist', { timeout: 2000 })
getActionEntryForFile('storage1', ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist')

// Finally, the storage should be accessible
Expand All @@ -119,8 +134,10 @@ describe('Files user credentials', { testIsolation: true }, () => {
})

it('Create another user storage while reusing GLOBAL user credentials', () => {
const url = Cypress.config('baseUrl') + '/remote.php/dav/files/' + storageUser.userId
createStorageWithConfig('storage2', StorageBackend.DAV, AuthBackend.UserGlobalAuth, { host: url.replace('index.php/', ''), 'secure': 'false' })
// Its not the public server address but the address so the server itself can connect to it
const base = 'http://localhost'
const host = `${base}/remote.php/dav/files/${storageUser.userId}`
createStorageWithConfig('storage2', StorageBackend.DAV, AuthBackend.UserGlobalAuth, { host, secure: 'false' })

cy.login(user2)
cy.visit('/apps/files/extstoragemounts')
Expand Down
25 changes: 15 additions & 10 deletions cypress/e2e/files_versions/version_deletion.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ describe('Versions restoration', () => {
})

it('Does not work without delete permission through direct API access', () => {
let hostname: string
let fileId: string|undefined
let versionId: string|undefined

Expand All @@ -68,24 +67,30 @@ describe('Versions restoration', () => {
navigateToFolder(folderName)
openVersionsPanel(randomFilePath)

cy.url().then(url => { hostname = new URL(url).hostname })
getRowForFile(randomFileName).invoke('attr', 'data-cy-files-list-row-fileid').then(_fileId => { fileId = _fileId })
cy.get('[data-files-versions-version]').eq(1).invoke('attr', 'data-files-versions-version').then(_versionId => { versionId = _versionId })
getRowForFile(randomFileName)
.should('be.visible')
.invoke('attr', 'data-cy-files-list-row-fileid')
.then(($fileId) => { fileId = $fileId })

cy.get('[data-files-versions-version]')
.eq(1)
.invoke('attr', 'data-files-versions-version')
.then(($versionId) => { versionId = $versionId })

cy.logout()
cy.then(() => {
cy.logout()
cy.request({
const base = Cypress.config('baseUrl')!.replace(/\/index\.php\/?$/, '')
return cy.request({
method: 'DELETE',
url: `${base}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
auth: { user: recipient.userId, pass: recipient.password },
headers: {
cookie: '',
},
url: `http://${hostname}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
failOnStatusCode: false,
})
.then(({ status }) => {
expect(status).to.equal(403)
})
}).then(({ status }) => {
expect(status).to.equal(403)
})
})
})
Expand Down
27 changes: 16 additions & 11 deletions cypress/e2e/files_versions/version_download.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,31 +52,36 @@ describe('Versions download', () => {
})

it('Does not work without download permission through direct API access', () => {
let hostname: string
let fileId: string|undefined
let versionId: string|undefined

setupTestSharedFileFromUser(user, randomFileName, { download: false })
.then(recipient => {
.then((recipient) => {
openVersionsPanel(randomFileName)

cy.url().then(url => { hostname = new URL(url).hostname })
getRowForFile(randomFileName).invoke('attr', 'data-cy-files-list-row-fileid').then(_fileId => { fileId = _fileId })
cy.get('[data-files-versions-version]').eq(1).invoke('attr', 'data-files-versions-version').then(_versionId => { versionId = _versionId })
getRowForFile(randomFileName)
.should('be.visible')
.invoke('attr', 'data-cy-files-list-row-fileid')
.then(($fileId) => { fileId = $fileId })

cy.get('[data-files-versions-version]')
.eq(1)
.invoke('attr', 'data-files-versions-version')
.then(($versionId) => { versionId = $versionId })

cy.logout()
cy.then(() => {
cy.logout()
cy.request({
const base = Cypress.config('baseUrl')!.replace(/\/index\.php\/?$/, '')
return cy.request({
url: `${base}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
auth: { user: recipient.userId, pass: recipient.password },
headers: {
cookie: '',
},
url: `http://${hostname}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
failOnStatusCode: false,
})
.then(({ status }) => {
expect(status).to.equal(403)
})
}).then(({ status }) => {
expect(status).to.equal(403)
})
})
})
Expand Down
Loading