mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
fix(sync): warn about unsynced changes before password change
Add check for unsynced operations before proceeding with encryption password change. If there are pending operations that haven't been synced yet, throw an error asking the user to wait for sync to complete. This prevents potential data loss where local changes would be discarded during the clean slate mechanism.
This commit is contained in:
parent
4ea30b183b
commit
842baf81bf
2 changed files with 40 additions and 0 deletions
|
|
@ -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<OperationLogUploadService>;
|
||||
let mockDerivedKeyCache: jasmine.SpyObj<DerivedKeyCacheService>;
|
||||
let mockSyncWrapper: jasmine.SpyObj<SyncWrapperService>;
|
||||
let mockOpLogStore: jasmine.SpyObj<OperationLogStoreService>;
|
||||
let mockSyncProvider: jasmine.SpyObj<any>;
|
||||
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue