Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 .woodpecker.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# The version of OpenCloud to use in pipelines
OPENCLOUD_COMMITID=a3c7b2dbb396d7198b4d5682b917f1bbd36ca6da
OPENCLOUD_COMMITID=5b2529602fb22e1f5e37bf7879ffd132697bdc77
OPENCLOUD_BRANCH=main
85 changes: 85 additions & 0 deletions tests/e2e/features/rclone-crypt/spaceVault.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
Feature: Work with an rclone-crypt encrypted vault space
As a user with an end-to-end encrypted project space
I want to unlock it and work with its files under cleartext names
So that I can collaborate on encrypted content without decrypting it manually
We check that when uploading files or editing them, the payload sent to the server is encrypted

Background:
Given "Admin" creates following users using API
| id |
| Alice |
| Brian |
And "Admin" assigns following role to the users using API
| id | role |
| Alice | Space Admin |

@rclone-crypt
Scenario: Create a vault space, work with its files and share it with a space member
When "Alice" logs in
And "Alice" navigates to the projects space page
And "Alice" creates the following project spaces
| name | password |
| vaultspace | foobar |
And "Alice" enters the vault space "vaultspace" with passphrase "foobar"
And "Alice" creates the following resources
| resource | type | content | password |
| hello.txt | txtFile | hello world | foobar |
And "Alice" uploads the following resource
| resource | password |
| testavatar.png | foobar |
Then following resources should be displayed in the files list for user "Alice"
| resource |
| hello.txt |
| testavatar.png |
When "Alice" opens the following file in texteditor
| resource |
| hello.txt |
Then "Alice" should see the content "hello world" in editor "TextEditor"
And "Alice" closes the file viewer
When "Alice" opens the following file in mediaviewer
| resource |
| testavatar.png |
Then "Alice" is in a media-viewer
And "Alice" closes the file viewer
When "Alice" navigates to the project space "vaultspace"
And "Alice" adds following user to the project space
| user | role | kind |
| Brian | Can edit | user |
And "Alice" logs out

When "Brian" logs in
And "Brian" enters the vault space "vaultspace" with passphrase "foobar"
Then following resources should be displayed in the files list for user "Brian"
| resource |
| hello.txt |
| testavatar.png |
When "Brian" opens the following file in texteditor
| resource |
| hello.txt |
Then "Brian" should see the content "hello world" in editor "TextEditor"
And "Brian" closes the file viewer
When "Brian" opens the following file in mediaviewer
| resource |
| testavatar.png |
Then "Brian" is in a media-viewer
And "Brian" closes the file viewer
And "Brian" logs out

@rclone-crypt
Scenario: Reloading an unlocked vault space locks it again
When "Alice" logs in
And "Alice" navigates to the projects space page
And "Alice" creates the following project spaces
| name | password |
| vaultspace | foobar |
And "Alice" enters the vault space "vaultspace" with passphrase "foobar"
And "Alice" creates the following resources
| resource | type | content | password |
| hello.txt | txtFile | hello world | foobar |
And "Alice" reloads the page
Then "Alice" should see the unlock page of the vault space "vaultspace"
When "Alice" unlocks the vault space with passphrase "foobar"
Then following resource should be displayed in the files list for user "Alice"
| resource |
| hello.txt |
And "Alice" logs out
44 changes: 42 additions & 2 deletions tests/e2e/steps/ui/spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { DataTable } from 'playwright-bdd'
import { expect } from '@playwright/test'
import { World } from '../../environment/world'
import { objects } from '../../support'
import { Space } from '../../support/types'

When(
'{string} navigates to the personal space page',
Expand Down Expand Up @@ -34,7 +33,10 @@ When(
const spacesObject = new objects.applicationFiles.Spaces({ page })

for (const space of stepTable.hashes()) {
await spacesObject.create({ key: space.id || space.name, space: space as unknown as Space })
await spacesObject.create({
key: space.id || space.name,
space: { name: space.name, password: space.password }
})
}
}
)
Expand All @@ -50,6 +52,44 @@ When(
}
)

When(
'{string} enters the vault space {string} with passphrase {string}',
async function (
{ world }: { world: World },
stepUser: string,
key: string,
passphrase: string
): Promise<void> {
const { page } = world.actorsEnvironment.getActor({ key: stepUser })
const spacesObject = new objects.applicationFiles.Spaces({ page })
const pageObject = new objects.applicationFiles.page.spaces.Projects({ page })
await pageObject.navigate()
await spacesObject.openVault({ key, passphrase })
}
)

When(
'{string} unlocks the vault space with passphrase {string}',
async function (
{ world }: { world: World },
stepUser: string,
passphrase: string
): Promise<void> {
const { page } = world.actorsEnvironment.getActor({ key: stepUser })
const spacesObject = new objects.applicationFiles.Spaces({ page })
await spacesObject.unlockVault({ passphrase })
}
)

Then(
'{string} should see the unlock page of the vault space {string}',
async function ({ world }: { world: World }, stepUser: string, key: string): Promise<void> {
const { page } = world.actorsEnvironment.getActor({ key: stepUser })
const spacesObject = new objects.applicationFiles.Spaces({ page })
await spacesObject.expectVaultLocked({ key })
}
)

When(
/^"([^"]*)" (?:changes|updates) the space "([^"]*)" (name|subtitle|description|quota|image|icon) to "([^"]*)"$/,
async function (
Expand Down
35 changes: 34 additions & 1 deletion tests/e2e/support/objects/app-files/resource/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ const uploadList = '#upload-list'
const encryptFolderSwitch = '[data-testid="create-folder-encrypt"] [data-testid="oc-switch-btn"]'
const vaultSetupPassphraseInput = '#vault-setup-passphrase'
const unlockVaultBtn = '#vault-unlock-submit'
const vaultPassphraseInput = '#vault-passphrase'

export const getResourceLocator = ({
page,
Expand Down Expand Up @@ -2770,10 +2771,42 @@ const unlockVault = async ({
}): Promise<void> => {
const unlockButton = page.locator(unlockVaultBtn)
await expect(unlockButton).toBeDisabled()
await page.locator('#vault-passphrase').fill(passphrase)
await page.locator(vaultPassphraseInput).fill(passphrase)
await unlockButton.click()
}

/**
* Navigate back to where a step started. If the start is a vault, it needs
* to be unlocked because the vault gets locked initially after a reload.
*/
export const returnToStartUrl = async ({
page,
startUrl,
password
}: {
page: Page
startUrl: string
password?: string
}): Promise<void> => {
await page.goto(startUrl)
if (!password) {
return
}
const passphraseInput = page.locator(vaultPassphraseInput)
try {
// Whichever of the two renders first says where the load landed: the
// unlock page for a locked vault, the file list for anything else.
await expect(passphraseInput.or(page.locator(filesView))).toBeVisible()
} catch {
return
}
if (!(await passphraseInput.isVisible())) {
return
}
await unlockVault({ page, passphrase: password })
await expect(page.locator(appLoadingSpinner)).toBeHidden()
}

export const lockVault = async ({ page, vault }: { page: Page; vault: string }): Promise<void> => {
await page.locator(util.format(resourceNameSelector, vault)).click({ button: 'right' })
await page.locator(filesContextLockVaultAction).click()
Expand Down
4 changes: 2 additions & 2 deletions tests/e2e/support/objects/app-files/resource/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ export class Resource {
async create(args: Omit<po.createResourceArgs, 'page'>): Promise<void> {
const startUrl = this.#page.url()
await po.createResources({ ...args, page: this.#page })
await this.#page.goto(startUrl)
await po.returnToStartUrl({ page: this.#page, startUrl, password: args.password })
}

async upload(args: Omit<po.uploadResourceArgs, 'page'>): Promise<void> {
const startUrl = this.#page.url()
await po.uploadResource({ ...args, page: this.#page })
await this.#page.goto(startUrl)
await po.returnToStartUrl({ page: this.#page, startUrl, password: args.password })
}

async tryToUpload(args: Omit<po.uploadResourceArgs, 'page'>): Promise<void> {
Expand Down
56 changes: 54 additions & 2 deletions tests/e2e/support/objects/app-files/spaces/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ const editSpacesDescription = '.oc-files-actions-edit-readme-content-trigger:vis
const spacesDescriptionInputArea = '.text-editor-provider .ProseMirror'
const spacesDescriptionSaveTextFileInEditorButton = '#app-save-action:visible'
const spaceHeaderSelector = '.space-header'
const encryptSpaceSwitch = '[data-testid="create-space-encrypt"] [data-testid="oc-switch-btn"]'
const vaultSetupPassphraseInput = '#vault-setup-passphrase'
const vaultPassphraseInput = '#vault-passphrase'
const vaultUnlockButton = '#vault-unlock-submit'
const vaultNameSelector = '[data-testid="vault-name"]'
const activitySidebarPanel = 'sidebar-panel-activities'
const activitySidebarPanelBodyContent = '#sidebar-panel-activities .sidebar-panel__body-content'

Expand All @@ -50,32 +55,79 @@ export const openActivitiesPanel = async (page: Page): Promise<void> => {
export interface createSpaceArgs {
name: string
page: Page
password?: string
}

export const createSpace = async (args: createSpaceArgs): Promise<string> => {
const { page, name } = args
const { page, name, password } = args

await page.locator(newSpaceMenuButton).click()
await page.locator(spaceNameInputField).fill(name)

// An encrypted space skips the default template, so the server never creates
// a `.space` folder for it.
const template = password ? 'none' : 'default'
const postResponsePromise = page.waitForResponse(
(postResp) =>
postResp.status() === 201 &&
postResp.request().method() === 'POST' &&
postResp.url().endsWith('drives?template=default')
postResp.url().endsWith(`drives?template=${template}`)
)

if (!password) {
const [responses] = await Promise.all([
postResponsePromise,
page.locator(actionConfirmButton).click()
])
const { id } = await responses.json()
return id
}

await page.locator(encryptSpaceSwitch).click()
await page.locator(actionConfirmButton).click()
await page.locator(vaultSetupPassphraseInput).fill(password)

// Committing the password writes the integrity token onto the new space root.
const proppatchPromise = page.waitForResponse((resp) => resp.request().method() === 'PROPPATCH')
const [responses] = await Promise.all([
postResponsePromise,
page.locator(actionConfirmButton).click()
])
await proppatchPromise

const { id } = await responses.json()
return id
}

/**/

export const unlockVaultSpace = async (args: { page: Page; passphrase: string }): Promise<void> => {
const { page, passphrase } = args
const unlockButton = page.locator(vaultUnlockButton)
await expect(unlockButton).toBeDisabled()
await page.locator(vaultPassphraseInput).fill(passphrase)
await unlockButton.click()
}

export const openVaultSpace = async (args: {
page: Page
id: string
passphrase: string
}): Promise<void> => {
const { page, id, passphrase } = args
await page.locator(util.format(spaceIdSelector, id)).click()
await unlockVaultSpace({ page, passphrase })
await page.locator(spaceHeaderSelector).waitFor()
}

export const expectVaultSpaceLocked = async (args: { page: Page; name: string }): Promise<void> => {
const { page, name } = args
await expect(page.locator(vaultPassphraseInput)).toBeVisible()
await expect(page.locator(vaultNameSelector)).toHaveText(name)
}

/**/

export interface openSpaceArgs {
id: string
page: Page
Expand Down
14 changes: 14 additions & 0 deletions tests/e2e/support/objects/app-files/spaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ export class Spaces {
await po.openSpace({ page: this.#page, id })
}

async openVault({ key, passphrase }: { key: string; passphrase: string }): Promise<void> {
const { id } = this.#spacesEnvironment.getSpace({ key })
await po.openVaultSpace({ page: this.#page, id, passphrase })
}

async unlockVault({ passphrase }: { passphrase: string }): Promise<void> {
await po.unlockVaultSpace({ page: this.#page, passphrase })
}

async expectVaultLocked({ key }: { key: string }): Promise<void> {
const { name } = this.#spacesEnvironment.getSpace({ key })
await po.expectVaultSpaceLocked({ page: this.#page, name })
}

async changeName({
key,
value,
Expand Down