11import { mock } from 'vitest-mock-extended'
22import { Resource , SpaceResource } from '@opencloud-eu/web-client'
33import { WebDAV } from '@opencloud-eu/web-client/webdav'
4- import { streamToArrayBuffer } from '@opencloud-eu/web-pkg'
4+ import { FolderVaultEngine , streamToArrayBuffer } from '@opencloud-eu/web-pkg'
55import { probeVaultNeedsSetup , unlockVault , VaultTarget } from '../../src/unlock'
66import { createEngine } from '../../src/crypto/engine'
77import { INTEGRITY_ID_PROP } from '../../src/integrity'
88
9+ // Every unlock derives a real scrypt key (N=2^14), which takes long enough on a
10+ // loaded CI runner to blow the 5s default.
11+ vi . setConfig ( { testTimeout : 15_000 } )
12+
913const vaultRoot = '/my.vault'
1014const passphrase = 'foobar'
1115// "report.txt" encrypted with the passphrase above - the sample `verifySegment`
1216// decrypts when a vault carries no integrity token.
1317const encryptedChildName = 'unq54c7b9fj4lam8t82q1hofdo'
1418
19+ // One engine - and therefore one scrypt derivation - per passphrase for the
20+ // whole file. Engines hold no per-test state, and the derivation is the single
21+ // most expensive thing these tests do.
22+ const engines = new Map < string , FolderVaultEngine > ( )
23+ function engineFor ( password : string ) : FolderVaultEngine {
24+ if ( ! engines . has ( password ) ) {
25+ engines . set ( password , createEngine ( vaultRoot , password ) )
26+ }
27+ return engines . get ( password )
28+ }
29+
1530function tokenFor ( password : string ) : Promise < string > {
16- return createEngine ( vaultRoot , password ) . createIntegrityToken ( )
31+ return engineFor ( password ) . createIntegrityToken ( )
1732}
1833
1934/** Real rclone-crypt ciphertext for `content`, as the server would hold it. */
2035function encryptedFile ( password : string , content : string ) : Promise < ArrayBuffer > {
21- const engine = createEngine ( vaultRoot , password )
22- return streamToArrayBuffer ( engine . encryptContent ( new Blob ( [ content ] ) . stream ( ) ) )
36+ return streamToArrayBuffer ( engineFor ( password ) . encryptContent ( new Blob ( [ content ] ) . stream ( ) ) )
37+ }
38+
39+ /** The ciphertext a sampled file returns by default, computed once. */
40+ let sampleFileContent : Promise < ArrayBuffer > | undefined
41+ function encryptedSampleFile ( ) : Promise < ArrayBuffer > {
42+ sampleFileContent ??= encryptedFile ( passphrase , 'hello vault' )
43+ return sampleFileContent
2344}
2445
2546type Child = { name : string ; isFolder ?: boolean ; size ?: number }
@@ -61,7 +82,7 @@ async function createTarget({
6182 )
6283 } )
6384 webdav . getFileContents . mockResolvedValue ( {
64- body : fileContent ?? ( await encryptedFile ( passphrase , 'hello vault' ) )
85+ body : fileContent ?? ( await encryptedSampleFile ( ) )
6586 } as never )
6687 webdav . setProperties . mockResolvedValue ( undefined as never )
6788
@@ -76,6 +97,12 @@ function writtenToken(webdav: WebDAV): string {
7697 return ( properties as Record < string , string > ) [ INTEGRITY_ID_PROP ]
7798}
7899
100+ // `clearMocks` only clears calls, so a console spy would stay in place for every
101+ // test that follows it.
102+ afterEach ( ( ) => {
103+ vi . restoreAllMocks ( )
104+ } )
105+
79106describe ( 'unlockVault' , ( ) => {
80107 describe ( 'a vault carrying an integrity token' , ( ) => {
81108 it ( 'unlocks with the right passphrase and writes nothing' , async ( ) => {
@@ -113,7 +140,7 @@ describe('unlockVault', () => {
113140 const result = await unlockVault ( target , passphrase )
114141
115142 expect ( result . status ) . toBe ( 'unlocked' )
116- const engine = createEngine ( vaultRoot , passphrase )
143+ const engine = engineFor ( passphrase )
117144 expect ( await engine . verifyIntegrityToken ( writtenToken ( webdav ) ) ) . toBe ( true )
118145 } )
119146
@@ -175,7 +202,7 @@ describe('unlockVault', () => {
175202 { extraProps : [ INTEGRITY_ID_PROP ] }
176203 )
177204 // The backfilled token has to verify under the passphrase we just used.
178- const engine = createEngine ( vaultRoot , passphrase )
205+ const engine = engineFor ( passphrase )
179206 expect ( await engine . verifyIntegrityToken ( writtenToken ( webdav ) ) ) . toBe ( true )
180207 } )
181208
@@ -194,7 +221,7 @@ describe('unlockVault', () => {
194221 // catches it - without that, we would write a token for the wrong passphrase
195222 // and lock the real one out permanently.
196223 const { target, webdav } = await createTarget ( { children : [ { name : encryptedChildName } ] } )
197- const engine = createEngine ( vaultRoot , 'definitely-wrong' )
224+ const engine = engineFor ( 'definitely-wrong' )
198225 expect ( await engine . verifySegment ( encryptedChildName ) ) . toBe ( true )
199226
200227 const result = await unlockVault ( target , 'definitely-wrong' )
@@ -285,6 +312,7 @@ describe('unlockVault', () => {
285312 it ( 'still unlocks when the backfill is rejected' , async ( ) => {
286313 // A viewer on a shared vault or a public-link visitor cannot write
287314 // properties. The passphrase is verified by then, so keep them in.
315+ vi . spyOn ( console , 'warn' ) . mockImplementation ( ( ) => undefined )
288316 const { target, webdav } = await createTarget ( { children : [ { name : encryptedChildName } ] } )
289317 webdav . setProperties . mockRejectedValue ( new Error ( '403' ) )
290318
@@ -301,7 +329,7 @@ describe('unlockVault', () => {
301329 const result = await unlockVault ( target , passphrase )
302330
303331 expect ( result . status ) . toBe ( 'unlocked' )
304- const engine = createEngine ( vaultRoot , passphrase )
332+ const engine = engineFor ( passphrase )
305333 expect ( await engine . verifyIntegrityToken ( writtenToken ( webdav ) ) ) . toBe ( true )
306334 } )
307335
0 commit comments