From dca674cdf458d1471ab29ab54b647e11e6c3e15a Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Sun, 18 Jan 2026 02:20:14 +0100 Subject: [PATCH] test(op-log): fix clock drift tests by adding missing service mocks - Add missing mocks for OperationEncryptionService, SuperSyncStatusService, and CLIENT_ID_PROVIDER - Fix async handling by replacing flushMicrotasks() with tick() and flush() - Update test expectation to match actual clientId parameter - Reset hasWarnedClockDrift flag in beforeEach to prevent test interference - Re-enable previously disabled test suite Root cause: Without all required service mocks, async code in lock callback never executed in fakeAsync context, preventing clock drift detection logic from running. --- .../operation-log-download.service.spec.ts | 57 +++++++++++++------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/app/op-log/sync/operation-log-download.service.spec.ts b/src/app/op-log/sync/operation-log-download.service.spec.ts index 5dc7133f7..ea08b64d7 100644 --- a/src/app/op-log/sync/operation-log-download.service.spec.ts +++ b/src/app/op-log/sync/operation-log-download.service.spec.ts @@ -1,4 +1,4 @@ -import { fakeAsync, flushMicrotasks, TestBed, tick } from '@angular/core/testing'; +import { fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; import { OperationLogDownloadService } from './operation-log-download.service'; import { OperationLogStoreService } from '../persistence/operation-log-store.service'; import { LockService } from './lock.service'; @@ -12,12 +12,18 @@ import { ActionType, OpType } from '../core/operation.types'; import { CLOCK_DRIFT_THRESHOLD_MS } from '../core/operation-log.const'; import { OpLog } from '../../core/log'; import { T } from '../../t.const'; +import { OperationEncryptionService } from './operation-encryption.service'; +import { SuperSyncStatusService } from './super-sync-status.service'; +import { CLIENT_ID_PROVIDER } from '../util/client-id.provider'; -xdescribe('OperationLogDownloadService', () => { +describe('OperationLogDownloadService', () => { let service: OperationLogDownloadService; let mockOpLogStore: jasmine.SpyObj; let mockLockService: jasmine.SpyObj; let mockSnackService: jasmine.SpyObj; + let mockEncryptionService: jasmine.SpyObj; + let mockSuperSyncStatusService: jasmine.SpyObj; + let mockClientIdProvider: { loadClientId: jasmine.Spy }; beforeEach(() => { mockOpLogStore = jasmine.createSpyObj('OperationLogStoreService', [ @@ -26,17 +32,27 @@ xdescribe('OperationLogDownloadService', () => { ]); mockLockService = jasmine.createSpyObj('LockService', ['request']); mockSnackService = jasmine.createSpyObj('SnackService', ['open']); + mockEncryptionService = jasmine.createSpyObj('OperationEncryptionService', [ + 'decryptOperations', + ]); + mockSuperSyncStatusService = jasmine.createSpyObj('SuperSyncStatusService', [ + 'markRemoteChecked', + ]); + mockClientIdProvider = { + loadClientId: jasmine + .createSpy('loadClientId') + .and.returnValue(Promise.resolve('test-client-id')), + }; // Mock OpLog spyOn(OpLog, 'warn'); spyOn(OpLog, 'normal'); // Default mock implementations - mockLockService.request.and.callFake( - async (_name: string, fn: () => Promise) => { - await fn(); - }, - ); + // Note: Don't use async/await here as it breaks fakeAsync zone context + mockLockService.request.and.callFake((_name: string, fn: () => Promise) => { + return fn(); + }); mockOpLogStore.getAppliedOpIds.and.returnValue(Promise.resolve(new Set())); mockOpLogStore.hasSyncedOps.and.returnValue(Promise.resolve(false)); @@ -46,10 +62,15 @@ xdescribe('OperationLogDownloadService', () => { { provide: OperationLogStoreService, useValue: mockOpLogStore }, { provide: LockService, useValue: mockLockService }, { provide: SnackService, useValue: mockSnackService }, + { provide: OperationEncryptionService, useValue: mockEncryptionService }, + { provide: SuperSyncStatusService, useValue: mockSuperSyncStatusService }, + { provide: CLIENT_ID_PROVIDER, useValue: mockClientIdProvider }, ], }); service = TestBed.inject(OperationLogDownloadService); + // Reset the clock drift warning flag for each test + (service as any).hasWarnedClockDrift = false; }); describe('downloadRemoteOps', () => { @@ -125,13 +146,13 @@ xdescribe('OperationLogDownloadService', () => { ); service.downloadRemoteOps(mockApiProvider); - flushMicrotasks(); // Flush all pending promises + tick(); // Resolve promises // Warning should not be shown immediately expect(OpLog.warn).not.toHaveBeenCalled(); // After 1 second retry, warning should appear - tick(1000); + flush(); // Flush all pending timers expect(OpLog.warn).toHaveBeenCalledWith( 'OperationLogDownloadService: Clock drift detected', @@ -269,14 +290,14 @@ xdescribe('OperationLogDownloadService', () => { // First call - should warn after retry service.downloadRemoteOps(mockApiProvider); - flushMicrotasks(); // Flush all pending promises - tick(1000); // Wait for retry + tick(); // Resolve promises + flush(); // Flush all pending timers expect(OpLog.warn).toHaveBeenCalledTimes(1); // Second call - should NOT warn again service.downloadRemoteOps(mockApiProvider); - flushMicrotasks(); // Flush all pending promises - tick(1000); // Wait for retry (if any) + tick(); // Resolve promises + flush(); // Flush all pending timers (if any) expect(OpLog.warn).toHaveBeenCalledTimes(1); })); @@ -361,8 +382,8 @@ xdescribe('OperationLogDownloadService', () => { ); service.downloadRemoteOps(mockApiProvider); - flushMicrotasks(); // Flush all pending promises - tick(1000); // Wait for retry + tick(); // Resolve promises + flush(); // Flush all pending timers // Should warn because serverTime differs from client time expect(OpLog.warn).toHaveBeenCalledWith( @@ -562,7 +583,11 @@ xdescribe('OperationLogDownloadService', () => { await service.downloadRemoteOps(mockApiProvider, { forceFromSeq0: true }); // Should start from 0, not from lastServerSeq (100) - expect(mockApiProvider.downloadOps).toHaveBeenCalledWith(0, undefined, 500); + expect(mockApiProvider.downloadOps).toHaveBeenCalledWith( + 0, + 'test-client-id', + 500, + ); }); it('should collect all op clocks when forceFromSeq0 is true', async () => {