@@ -183,13 +183,38 @@ describe('InteropService.noteLock', () => {
183183 expect ( result . warnings ) . toEqual ( [ '1 locked note(s) could not be unlocked and were not exported' ] ) ;
184184 } ) ;
185185
186- it . each ( [
187- { label : 'no note is locked' , flagEnabled : true , isLocked : 0 } ,
188- { label : 'note lock is disabled' , flagEnabled : false , isLocked : 1 } ,
189- ] ) ( 'should not write the key file when $label' , async ( { flagEnabled, isLocked } ) => {
190- Setting . setValue ( 'featureFlag.noteLock' , flagEnabled ) ;
186+ it ( 'should not write the key file when no note is locked' , async ( ) => {
187+ await setUpUnlockedSession ( ) ;
188+ const folder = await Folder . save ( { title : 'folder' } ) ;
189+ await Note . save ( { title : 'note' , body : 'plain body' , parent_id : folder . id } ) ;
190+
191+ await InteropService . instance ( ) . export ( { path : exportDir ( ) , format : ExportModuleOutputFormat . Raw } ) ;
192+
193+ expect ( await fs . pathExists ( `${ exportDir ( ) } /${ noteLockKeyFileName } ` ) ) . toBe ( false ) ;
194+ } ) ;
195+
196+ it ( 'should write the key file for a note locked with the profile key even with note lock disabled' , async ( ) => {
197+ await setUpUnlockedSession ( ) ;
198+ const folder = await Folder . save ( { title : 'folder' } ) ;
199+ const note = await Note . save ( { title : 'note' , body : 'secret' , parent_id : folder . id } ) ;
200+ await lockNote ( note . id ) ;
201+
202+ Setting . setValue ( 'featureFlag.noteLock' , false ) ;
203+ await InteropService . instance ( ) . export ( { path : exportDir ( ) , format : ExportModuleOutputFormat . Raw } ) ;
204+
205+ expect ( JSON . parse ( await fs . readFile ( `${ exportDir ( ) } /${ noteLockKeyFileName } ` , 'utf-8' ) ) . id ) . toBe ( NoteLockKey . instance ( ) . load ( ) . id ) ;
206+ } ) ;
207+
208+ it ( 'should not write the key file when the locked notes use another key' , async ( ) => {
209+ await setUpUnlockedSession ( ) ;
210+ const otherKey = await encryptionService ( ) . generateMasterKey ( 'other' ) ;
211+ const body = await encryptionService ( ) . encryptString ( 'secret' , {
212+ masterKeyId : '0123456789abcdef0123456789abcdef' ,
213+ decryptedMasterKey : await encryptionService ( ) . decryptMasterKeyContent ( otherKey , 'other' ) ,
214+ isNoteLock : true ,
215+ } ) ;
191216 const folder = await Folder . save ( { title : 'folder' } ) ;
192- await Note . save ( { title : 'note' , body : 'plain body' , parent_id : folder . id , is_locked : isLocked } ) ;
217+ await Note . save ( { title : 'note' , body, parent_id : folder . id , is_locked : 1 } ) ;
193218
194219 await InteropService . instance ( ) . export ( { path : exportDir ( ) , format : ExportModuleOutputFormat . Raw } ) ;
195220
@@ -204,11 +229,12 @@ describe('InteropService.noteLock', () => {
204229 await lockNote ( note . id ) ;
205230 await InteropService . instance ( ) . export ( { path : exportDir ( ) , format : ExportModuleOutputFormat . Raw } ) ;
206231
207- await InteropService . instance ( ) . import ( { path : exportDir ( ) , format : 'raw' } ) ;
232+ const result = await InteropService . instance ( ) . import ( { path : exportDir ( ) , format : 'raw' } ) ;
208233
209234 const imported = ( await Note . all ( ) ) . find ( n => n . id !== note . id && ! ! n . is_locked ) ;
210235 expect ( imported . body ) . not . toContain ( 'secret' ) ;
211236 expect ( ( await Note . load ( imported . id , { useNoteLock : true } ) ) . body ) . toContain ( 'secret' ) ;
237+ expect ( result . warnings ) . toEqual ( [ ] ) ;
212238
213239 // The extracted list follows the remapped resource id, so the resource stays associated.
214240 const importedResourceIds = Note . unserializeExtractedResourceIds ( imported . extracted_resource_ids ) ;
@@ -300,11 +326,29 @@ describe('InteropService.noteLock', () => {
300326
301327 await rotateProfileKey ( 'new password' ) ;
302328
303- await InteropService . instance ( ) . import ( { path : exportDir ( ) , format : 'raw' } ) ;
329+ const result = await InteropService . instance ( ) . import ( { path : exportDir ( ) , format : 'raw' } ) ;
304330
305331 const imported = ( await Note . all ( ) ) . find ( n => n . id !== note . id && ! ! n . is_locked ) ;
306332 expect ( imported . body ) . not . toContain ( 'secret' ) ;
307333 await expect ( Note . load ( imported . id , { useNoteLock : true } ) ) . rejects . toThrow ( ) ;
334+ expect ( result . warnings ) . toEqual ( [ 'The locked notes in this backup have not been migrated to the current note lock key and will be unreadable' ] ) ;
335+ } ) ;
336+
337+ it ( 'should warn when a backup with locked notes is imported without a handler into a profile with no note lock key' , async ( ) => {
338+ await setUpUnlockedSession ( ) ;
339+ const folder = await Folder . save ( { title : 'folder' } ) ;
340+ const note = await Note . save ( { title : 'note' , body : 'secret' , parent_id : folder . id } ) ;
341+ await lockNote ( note . id ) ;
342+ await InteropService . instance ( ) . export ( { path : exportDir ( ) , format : ExportModuleOutputFormat . Raw } ) ;
343+
344+ await setupDatabaseAndSynchronizer ( 2 ) ;
345+ await switchClient ( 2 ) ;
346+ NoteLockSession . destroyInstance ( ) ;
347+ NoteLockKey . destroyInstance ( ) ;
348+ Setting . setValue ( 'featureFlag.noteLock' , true ) ;
349+ const result = await InteropService . instance ( ) . import ( { path : exportDir ( ) , format : 'raw' } ) ;
350+
351+ expect ( result . warnings ) . toEqual ( [ 'The locked notes in this backup cannot be read, because no note lock key has been set up on this profile' ] ) ;
308352 } ) ;
309353
310354 it ( 'should keep locked notes unchanged and warn when the provided key does not fit' , async ( ) => {
0 commit comments