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
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
93 changes: 93 additions & 0 deletions tests/e2e/features/rclone-crypt/spaceVault.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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 |
And "Alice" should not be able to share following resources from the space "vaultspace"
| 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 |
And "Brian" should not be able to share following resources from the space "vaultspace"
| 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
19 changes: 19 additions & 0 deletions tests/e2e/steps/ui/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,25 @@ Then(
}
)

Then(
'{string} should not be able to share following resource(s) from the space {string}',
async (
{ world }: { world: World },
stepUser: string,
space: string,
stepTable: DataTable
): Promise<void> => {
const { page } = world.actorsEnvironment.getActor({ key: stepUser })
const spacesObject = new objects.applicationFiles.Spaces({ page })
const resourceObject = new objects.applicationFiles.Resource({ page })

await spacesObject.expectOpen({ key: space })
for (const info of stepTable.hashes()) {
await resourceObject.expectNotShareable({ resource: info.resource })
}
}
)

Then(
/^following resources? (should|should not) be displayed in the search list for user "([^"]*)"$/,
async (
Expand Down
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
64 changes: 63 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,13 @@ 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'
const filesContextMenu = 'div[id^="context-menu-drop"]'
const showSharesActionSelector = 'button.oc-files-actions-show-shares-trigger'
const quickActionShareButton =
'//*[@data-test-resource-name="%s"]/ancestor::tr//button[contains(@class, "files-quick-action-show-shares")]'
const inviteCollaboratorForm = '#new-collaborators-form'
const addPublicLinkButton = '#files-file-link-add'

export const getResourceLocator = ({
page,
Expand Down Expand Up @@ -2770,10 +2777,65 @@ 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 expectResourceNotShareable = async ({
page,
resource
}: {
page: Page
resource: string
}): Promise<void> => {
await expect(page.locator(util.format(quickActionShareButton, resource))).toBeHidden()

await sidebar.open({ page, resource })
await sidebar.openPanel({ page, name: 'sharing' })
await expect(page.locator(inviteCollaboratorForm)).toBeHidden()
await expect(page.locator(addPublicLinkButton)).toBeHidden()
await sidebar.close({ page })

await page.locator(util.format(resourceNameSelector, resource)).click({ button: 'right' })
const contextMenu = page.locator(filesContextMenu)
await expect(contextMenu).toBeVisible()
await expect(contextMenu.locator(showSharesActionSelector)).toBeHidden()
await page.keyboard.press('Escape')
await expect(contextMenu).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
8 changes: 6 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 Expand Up @@ -139,6 +139,10 @@ export class Resource {
await this.#page.goto(startUrl)
}

async expectNotShareable({ resource }: { resource: string }): Promise<void> {
return await po.expectResourceNotShareable({ page: this.#page, resource })
}

async expectThatDeleteTrashBinButtonIsNotVisible(
args: Omit<po.deleteResourceTrashbinArgs, 'page'>
): Promise<void> {
Expand Down
62 changes: 60 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,12 @@ 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 spaceHeaderNameSelector = '.space-header h2'
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 +56,84 @@ 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 expectSpaceOpen = async (args: { page: Page; name: string }): Promise<void> => {
const { page, name } = args
await expect(page.locator(spaceHeaderNameSelector)).toHaveText(name)
}

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
Loading