Skip to content

Commit 6de7644

Browse files
authored
Merge pull request #3189 from opencloud-eu/test/vault-space-e2e-tests
test(e2e): add e2ee space tests
2 parents 66d83b1 + 2cbc998 commit 6de7644

8 files changed

Lines changed: 303 additions & 8 deletions

File tree

.woodpecker.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# The version of OpenCloud to use in pipelines
2-
OPENCLOUD_COMMITID=a3c7b2dbb396d7198b4d5682b917f1bbd36ca6da
2+
OPENCLOUD_COMMITID=5b2529602fb22e1f5e37bf7879ffd132697bdc77
33
OPENCLOUD_BRANCH=main
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
Feature: Work with an rclone-crypt encrypted vault space
2+
As a user with an end-to-end encrypted project space
3+
I want to unlock it and work with its files under cleartext names
4+
So that I can collaborate on encrypted content without decrypting it manually
5+
We check that when uploading files or editing them, the payload sent to the server is encrypted
6+
7+
Background:
8+
Given "Admin" creates following users using API
9+
| id |
10+
| Alice |
11+
| Brian |
12+
And "Admin" assigns following role to the users using API
13+
| id | role |
14+
| Alice | Space Admin |
15+
16+
@rclone-crypt
17+
Scenario: Create a vault space, work with its files and share it with a space member
18+
When "Alice" logs in
19+
And "Alice" navigates to the projects space page
20+
And "Alice" creates the following project spaces
21+
| name | password |
22+
| vaultspace | foobar |
23+
And "Alice" enters the vault space "vaultspace" with passphrase "foobar"
24+
And "Alice" creates the following resources
25+
| resource | type | content | password |
26+
| hello.txt | txtFile | hello world | foobar |
27+
And "Alice" uploads the following resource
28+
| resource | password |
29+
| testavatar.png | foobar |
30+
Then following resources should be displayed in the files list for user "Alice"
31+
| resource |
32+
| hello.txt |
33+
| testavatar.png |
34+
And "Alice" should not be able to share following resources from the space "vaultspace"
35+
| resource |
36+
| hello.txt |
37+
| testavatar.png |
38+
When "Alice" opens the following file in texteditor
39+
| resource |
40+
| hello.txt |
41+
Then "Alice" should see the content "hello world" in editor "TextEditor"
42+
And "Alice" closes the file viewer
43+
When "Alice" opens the following file in mediaviewer
44+
| resource |
45+
| testavatar.png |
46+
Then "Alice" is in a media-viewer
47+
And "Alice" closes the file viewer
48+
When "Alice" navigates to the project space "vaultspace"
49+
And "Alice" adds following user to the project space
50+
| user | role | kind |
51+
| Brian | Can edit | user |
52+
And "Alice" logs out
53+
54+
When "Brian" logs in
55+
And "Brian" enters the vault space "vaultspace" with passphrase "foobar"
56+
Then following resources should be displayed in the files list for user "Brian"
57+
| resource |
58+
| hello.txt |
59+
| testavatar.png |
60+
And "Brian" should not be able to share following resources from the space "vaultspace"
61+
| resource |
62+
| hello.txt |
63+
| testavatar.png |
64+
When "Brian" opens the following file in texteditor
65+
| resource |
66+
| hello.txt |
67+
Then "Brian" should see the content "hello world" in editor "TextEditor"
68+
And "Brian" closes the file viewer
69+
When "Brian" opens the following file in mediaviewer
70+
| resource |
71+
| testavatar.png |
72+
Then "Brian" is in a media-viewer
73+
And "Brian" closes the file viewer
74+
And "Brian" logs out
75+
76+
@rclone-crypt
77+
Scenario: Reloading an unlocked vault space locks it again
78+
When "Alice" logs in
79+
And "Alice" navigates to the projects space page
80+
And "Alice" creates the following project spaces
81+
| name | password |
82+
| vaultspace | foobar |
83+
And "Alice" enters the vault space "vaultspace" with passphrase "foobar"
84+
And "Alice" creates the following resources
85+
| resource | type | content | password |
86+
| hello.txt | txtFile | hello world | foobar |
87+
And "Alice" reloads the page
88+
Then "Alice" should see the unlock page of the vault space "vaultspace"
89+
When "Alice" unlocks the vault space with passphrase "foobar"
90+
Then following resource should be displayed in the files list for user "Alice"
91+
| resource |
92+
| hello.txt |
93+
And "Alice" logs out

tests/e2e/steps/ui/resources.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,25 @@ Then(
418418
}
419419
)
420420

421+
Then(
422+
'{string} should not be able to share following resource(s) from the space {string}',
423+
async (
424+
{ world }: { world: World },
425+
stepUser: string,
426+
space: string,
427+
stepTable: DataTable
428+
): Promise<void> => {
429+
const { page } = world.actorsEnvironment.getActor({ key: stepUser })
430+
const spacesObject = new objects.applicationFiles.Spaces({ page })
431+
const resourceObject = new objects.applicationFiles.Resource({ page })
432+
433+
await spacesObject.expectOpen({ key: space })
434+
for (const info of stepTable.hashes()) {
435+
await resourceObject.expectNotShareable({ resource: info.resource })
436+
}
437+
}
438+
)
439+
421440
Then(
422441
/^following resources? (should|should not) be displayed in the search list for user "([^"]*)"$/,
423442
async (

tests/e2e/steps/ui/spaces.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { DataTable } from 'playwright-bdd'
33
import { expect } from '@playwright/test'
44
import { World } from '../../environment/world'
55
import { objects } from '../../support'
6-
import { Space } from '../../support/types'
76

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

3635
for (const space of stepTable.hashes()) {
37-
await spacesObject.create({ key: space.id || space.name, space: space as unknown as Space })
36+
await spacesObject.create({
37+
key: space.id || space.name,
38+
space: { name: space.name, password: space.password }
39+
})
3840
}
3941
}
4042
)
@@ -50,6 +52,44 @@ When(
5052
}
5153
)
5254

55+
When(
56+
'{string} enters the vault space {string} with passphrase {string}',
57+
async function (
58+
{ world }: { world: World },
59+
stepUser: string,
60+
key: string,
61+
passphrase: string
62+
): Promise<void> {
63+
const { page } = world.actorsEnvironment.getActor({ key: stepUser })
64+
const spacesObject = new objects.applicationFiles.Spaces({ page })
65+
const pageObject = new objects.applicationFiles.page.spaces.Projects({ page })
66+
await pageObject.navigate()
67+
await spacesObject.openVault({ key, passphrase })
68+
}
69+
)
70+
71+
When(
72+
'{string} unlocks the vault space with passphrase {string}',
73+
async function (
74+
{ world }: { world: World },
75+
stepUser: string,
76+
passphrase: string
77+
): Promise<void> {
78+
const { page } = world.actorsEnvironment.getActor({ key: stepUser })
79+
const spacesObject = new objects.applicationFiles.Spaces({ page })
80+
await spacesObject.unlockVault({ passphrase })
81+
}
82+
)
83+
84+
Then(
85+
'{string} should see the unlock page of the vault space {string}',
86+
async function ({ world }: { world: World }, stepUser: string, key: string): Promise<void> {
87+
const { page } = world.actorsEnvironment.getActor({ key: stepUser })
88+
const spacesObject = new objects.applicationFiles.Spaces({ page })
89+
await spacesObject.expectVaultLocked({ key })
90+
}
91+
)
92+
5393
When(
5494
/^"([^"]*)" (?:changes|updates) the space "([^"]*)" (name|subtitle|description|quota|image|icon) to "([^"]*)"$/,
5595
async function (

tests/e2e/support/objects/app-files/resource/actions.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ const uploadList = '#upload-list'
171171
const encryptFolderSwitch = '[data-testid="create-folder-encrypt"] [data-testid="oc-switch-btn"]'
172172
const vaultSetupPassphraseInput = '#vault-setup-passphrase'
173173
const unlockVaultBtn = '#vault-unlock-submit'
174+
const vaultPassphraseInput = '#vault-passphrase'
175+
const filesContextMenu = 'div[id^="context-menu-drop"]'
176+
const showSharesActionSelector = 'button.oc-files-actions-show-shares-trigger'
177+
const quickActionShareButton =
178+
'//*[@data-test-resource-name="%s"]/ancestor::tr//button[contains(@class, "files-quick-action-show-shares")]'
179+
const inviteCollaboratorForm = '#new-collaborators-form'
180+
const addPublicLinkButton = '#files-file-link-add'
174181

175182
export const getResourceLocator = ({
176183
page,
@@ -2770,10 +2777,65 @@ const unlockVault = async ({
27702777
}): Promise<void> => {
27712778
const unlockButton = page.locator(unlockVaultBtn)
27722779
await expect(unlockButton).toBeDisabled()
2773-
await page.locator('#vault-passphrase').fill(passphrase)
2780+
await page.locator(vaultPassphraseInput).fill(passphrase)
27742781
await unlockButton.click()
27752782
}
27762783

2784+
/**
2785+
* Navigate back to where a step started. If the start is a vault, it needs
2786+
* to be unlocked because the vault gets locked initially after a reload.
2787+
*/
2788+
export const returnToStartUrl = async ({
2789+
page,
2790+
startUrl,
2791+
password
2792+
}: {
2793+
page: Page
2794+
startUrl: string
2795+
password?: string
2796+
}): Promise<void> => {
2797+
await page.goto(startUrl)
2798+
if (!password) {
2799+
return
2800+
}
2801+
const passphraseInput = page.locator(vaultPassphraseInput)
2802+
try {
2803+
// Whichever of the two renders first says where the load landed: the
2804+
// unlock page for a locked vault, the file list for anything else.
2805+
await expect(passphraseInput.or(page.locator(filesView))).toBeVisible()
2806+
} catch {
2807+
return
2808+
}
2809+
if (!(await passphraseInput.isVisible())) {
2810+
return
2811+
}
2812+
await unlockVault({ page, passphrase: password })
2813+
await expect(page.locator(appLoadingSpinner)).toBeHidden()
2814+
}
2815+
2816+
export const expectResourceNotShareable = async ({
2817+
page,
2818+
resource
2819+
}: {
2820+
page: Page
2821+
resource: string
2822+
}): Promise<void> => {
2823+
await expect(page.locator(util.format(quickActionShareButton, resource))).toBeHidden()
2824+
2825+
await sidebar.open({ page, resource })
2826+
await sidebar.openPanel({ page, name: 'sharing' })
2827+
await expect(page.locator(inviteCollaboratorForm)).toBeHidden()
2828+
await expect(page.locator(addPublicLinkButton)).toBeHidden()
2829+
await sidebar.close({ page })
2830+
2831+
await page.locator(util.format(resourceNameSelector, resource)).click({ button: 'right' })
2832+
const contextMenu = page.locator(filesContextMenu)
2833+
await expect(contextMenu).toBeVisible()
2834+
await expect(contextMenu.locator(showSharesActionSelector)).toBeHidden()
2835+
await page.keyboard.press('Escape')
2836+
await expect(contextMenu).toBeHidden()
2837+
}
2838+
27772839
export const lockVault = async ({ page, vault }: { page: Page; vault: string }): Promise<void> => {
27782840
await page.locator(util.format(resourceNameSelector, vault)).click({ button: 'right' })
27792841
await page.locator(filesContextLockVaultAction).click()

tests/e2e/support/objects/app-files/resource/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ export class Resource {
1414
async create(args: Omit<po.createResourceArgs, 'page'>): Promise<void> {
1515
const startUrl = this.#page.url()
1616
await po.createResources({ ...args, page: this.#page })
17-
await this.#page.goto(startUrl)
17+
await po.returnToStartUrl({ page: this.#page, startUrl, password: args.password })
1818
}
1919

2020
async upload(args: Omit<po.uploadResourceArgs, 'page'>): Promise<void> {
2121
const startUrl = this.#page.url()
2222
await po.uploadResource({ ...args, page: this.#page })
23-
await this.#page.goto(startUrl)
23+
await po.returnToStartUrl({ page: this.#page, startUrl, password: args.password })
2424
}
2525

2626
async tryToUpload(args: Omit<po.uploadResourceArgs, 'page'>): Promise<void> {
@@ -139,6 +139,10 @@ export class Resource {
139139
await this.#page.goto(startUrl)
140140
}
141141

142+
async expectNotShareable({ resource }: { resource: string }): Promise<void> {
143+
return await po.expectResourceNotShareable({ page: this.#page, resource })
144+
}
145+
142146
async expectThatDeleteTrashBinButtonIsNotVisible(
143147
args: Omit<po.deleteResourceTrashbinArgs, 'page'>
144148
): Promise<void> {

tests/e2e/support/objects/app-files/spaces/actions.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ const editSpacesDescription = '.oc-files-actions-edit-readme-content-trigger:vis
2727
const spacesDescriptionInputArea = '.text-editor-provider .ProseMirror'
2828
const spacesDescriptionSaveTextFileInEditorButton = '#app-save-action:visible'
2929
const spaceHeaderSelector = '.space-header'
30+
const spaceHeaderNameSelector = '.space-header h2'
31+
const encryptSpaceSwitch = '[data-testid="create-space-encrypt"] [data-testid="oc-switch-btn"]'
32+
const vaultSetupPassphraseInput = '#vault-setup-passphrase'
33+
const vaultPassphraseInput = '#vault-passphrase'
34+
const vaultUnlockButton = '#vault-unlock-submit'
35+
const vaultNameSelector = '[data-testid="vault-name"]'
3036
const activitySidebarPanel = 'sidebar-panel-activities'
3137
const activitySidebarPanelBodyContent = '#sidebar-panel-activities .sidebar-panel__body-content'
3238

@@ -50,32 +56,84 @@ export const openActivitiesPanel = async (page: Page): Promise<void> => {
5056
export interface createSpaceArgs {
5157
name: string
5258
page: Page
59+
password?: string
5360
}
5461

5562
export const createSpace = async (args: createSpaceArgs): Promise<string> => {
56-
const { page, name } = args
63+
const { page, name, password } = args
5764

5865
await page.locator(newSpaceMenuButton).click()
5966
await page.locator(spaceNameInputField).fill(name)
6067

68+
// An encrypted space skips the default template, so the server never creates
69+
// a `.space` folder for it.
70+
const template = password ? 'none' : 'default'
6171
const postResponsePromise = page.waitForResponse(
6272
(postResp) =>
6373
postResp.status() === 201 &&
6474
postResp.request().method() === 'POST' &&
65-
postResp.url().endsWith('drives?template=default')
75+
postResp.url().endsWith(`drives?template=${template}`)
6676
)
6777

78+
if (!password) {
79+
const [responses] = await Promise.all([
80+
postResponsePromise,
81+
page.locator(actionConfirmButton).click()
82+
])
83+
const { id } = await responses.json()
84+
return id
85+
}
86+
87+
await page.locator(encryptSpaceSwitch).click()
88+
await page.locator(actionConfirmButton).click()
89+
await page.locator(vaultSetupPassphraseInput).fill(password)
90+
91+
// Committing the password writes the integrity token onto the new space root.
92+
const proppatchPromise = page.waitForResponse((resp) => resp.request().method() === 'PROPPATCH')
6893
const [responses] = await Promise.all([
6994
postResponsePromise,
7095
page.locator(actionConfirmButton).click()
7196
])
97+
await proppatchPromise
7298

7399
const { id } = await responses.json()
74100
return id
75101
}
76102

77103
/**/
78104

105+
export const unlockVaultSpace = async (args: { page: Page; passphrase: string }): Promise<void> => {
106+
const { page, passphrase } = args
107+
const unlockButton = page.locator(vaultUnlockButton)
108+
await expect(unlockButton).toBeDisabled()
109+
await page.locator(vaultPassphraseInput).fill(passphrase)
110+
await unlockButton.click()
111+
}
112+
113+
export const openVaultSpace = async (args: {
114+
page: Page
115+
id: string
116+
passphrase: string
117+
}): Promise<void> => {
118+
const { page, id, passphrase } = args
119+
await page.locator(util.format(spaceIdSelector, id)).click()
120+
await unlockVaultSpace({ page, passphrase })
121+
await page.locator(spaceHeaderSelector).waitFor()
122+
}
123+
124+
export const expectSpaceOpen = async (args: { page: Page; name: string }): Promise<void> => {
125+
const { page, name } = args
126+
await expect(page.locator(spaceHeaderNameSelector)).toHaveText(name)
127+
}
128+
129+
export const expectVaultSpaceLocked = async (args: { page: Page; name: string }): Promise<void> => {
130+
const { page, name } = args
131+
await expect(page.locator(vaultPassphraseInput)).toBeVisible()
132+
await expect(page.locator(vaultNameSelector)).toHaveText(name)
133+
}
134+
135+
/**/
136+
79137
export interface openSpaceArgs {
80138
id: string
81139
page: Page

0 commit comments

Comments
 (0)