diff --git a/src/app/imex/sync/encryption-password-change.service.spec.ts b/src/app/imex/sync/encryption-password-change.service.spec.ts index 0e76459d9b..6dcc663876 100644 --- a/src/app/imex/sync/encryption-password-change.service.spec.ts +++ b/src/app/imex/sync/encryption-password-change.service.spec.ts @@ -6,6 +6,7 @@ import { OperationLogUploadService } from '../../op-log/sync/operation-log-uploa import { DerivedKeyCacheService } from '../../op-log/encryption/derived-key-cache.service'; import { SyncProviderId } from '../../op-log/sync-providers/provider.const'; import { SyncWrapperService } from './sync-wrapper.service'; +import { OperationLogStoreService } from '../../op-log/persistence/operation-log-store.service'; describe('EncryptionPasswordChangeService', () => { let service: EncryptionPasswordChangeService; @@ -14,6 +15,7 @@ describe('EncryptionPasswordChangeService', () => { let mockUploadService: jasmine.SpyObj; let mockDerivedKeyCache: jasmine.SpyObj; let mockSyncWrapper: jasmine.SpyObj; + let mockOpLogStore: jasmine.SpyObj; let mockSyncProvider: jasmine.SpyObj; const TEST_PASSWORD = 'new-secure-password-123'; @@ -67,6 +69,10 @@ describe('EncryptionPasswordChangeService', () => { }, ); + // Mock OperationLogStoreService - default to no unsynced operations + mockOpLogStore = jasmine.createSpyObj('OperationLogStoreService', ['getUnsynced']); + mockOpLogStore.getUnsynced.and.returnValue(Promise.resolve([])); + TestBed.configureTestingModule({ providers: [ EncryptionPasswordChangeService, @@ -75,6 +81,7 @@ describe('EncryptionPasswordChangeService', () => { { provide: OperationLogUploadService, useValue: mockUploadService }, { provide: DerivedKeyCacheService, useValue: mockDerivedKeyCache }, { provide: SyncWrapperService, useValue: mockSyncWrapper }, + { provide: OperationLogStoreService, useValue: mockOpLogStore }, ], }); service = TestBed.inject(EncryptionPasswordChangeService); @@ -156,6 +163,28 @@ describe('EncryptionPasswordChangeService', () => { expect(mockCleanSlateService.createCleanSlate).not.toHaveBeenCalled(); }); + it('should throw error if there are unsynced operations', async () => { + mockOpLogStore.getUnsynced.and.returnValue( + Promise.resolve([{ id: 'op1' } as any, { id: 'op2' } as any]), + ); + + await expectAsync(service.changePassword(TEST_PASSWORD)).toBeRejectedWithError( + /Cannot change password: 2 operation\(s\) have not been synced yet/, + ); + + // Should not proceed with password change + expect(mockCleanSlateService.createCleanSlate).not.toHaveBeenCalled(); + }); + + it('should proceed when there are no unsynced operations', async () => { + mockOpLogStore.getUnsynced.and.returnValue(Promise.resolve([])); + + await service.changePassword(TEST_PASSWORD); + + // Should proceed with password change + expect(mockCleanSlateService.createCleanSlate).toHaveBeenCalled(); + }); + it('should revert password config if upload fails', async () => { const originalConfig = { encryptKey: 'old-password', diff --git a/src/app/imex/sync/encryption-password-change.service.ts b/src/app/imex/sync/encryption-password-change.service.ts index 1cf0696a29..351e901323 100644 --- a/src/app/imex/sync/encryption-password-change.service.ts +++ b/src/app/imex/sync/encryption-password-change.service.ts @@ -8,6 +8,7 @@ import { DerivedKeyCacheService } from '../../op-log/encryption/derived-key-cach import { CleanSlateService } from '../../op-log/clean-slate/clean-slate.service'; import { OperationLogUploadService } from '../../op-log/sync/operation-log-upload.service'; import { SyncWrapperService } from './sync-wrapper.service'; +import { OperationLogStoreService } from '../../op-log/persistence/operation-log-store.service'; /** * Service for changing the encryption password for SuperSync. @@ -26,6 +27,7 @@ export class EncryptionPasswordChangeService { private _uploadService = inject(OperationLogUploadService); private _derivedKeyCache = inject(DerivedKeyCacheService); private _syncWrapper = inject(SyncWrapperService); + private _opLogStore = inject(OperationLogStoreService); /** * Changes the encryption password using the clean slate approach. @@ -58,6 +60,15 @@ export class EncryptionPasswordChangeService { throw new Error('Sync provider does not support operation sync'); } + // Check for unsynced operations before proceeding + const unsyncedOps = await this._opLogStore.getUnsynced(); + if (unsyncedOps.length > 0) { + throw new Error( + `Cannot change password: ${unsyncedOps.length} operation(s) have not been synced yet. ` + + 'Please wait for sync to complete or manually trigger a sync before changing the password.', + ); + } + // Run the entire password change with sync blocked to prevent race conditions. // This waits for any ongoing sync to complete, then blocks new syncs. await this._syncWrapper.runWithSyncBlocked(async () => {