diff --git a/packages/cli/src/modules/insights/__tests__/insights-pruning.service.integration.test.ts b/packages/cli/src/modules/insights/__tests__/insights-pruning.service.integration.test.ts index 2bc4dc4eec..f062ffcf68 100644 --- a/packages/cli/src/modules/insights/__tests__/insights-pruning.service.integration.test.ts +++ b/packages/cli/src/modules/insights/__tests__/insights-pruning.service.integration.test.ts @@ -6,7 +6,6 @@ import { testDb, testModules, } from '@n8n/backend-test-utils'; -import { Time } from '@n8n/constants'; import { Container } from '@n8n/di'; import { mock } from 'jest-mock-extended'; import { DateTime } from 'luxon'; @@ -43,6 +42,7 @@ describe('InsightsPruningService', () => { let insightsByPeriodRepository: InsightsByPeriodRepository; let insightsPruningService: InsightsPruningService; let licenseState: LicenseState; + beforeAll(async () => { insightsConfig = Container.get(InsightsConfig); insightsConfig.maxAgeDays = 10; @@ -154,92 +154,4 @@ describe('InsightsPruningService', () => { expect(maxAge).toBe(result); }, ); - - describe('pruning scheduling', () => { - beforeEach(() => { - jest.useFakeTimers(); - insightsPruningService.startPruningTimer(); - }); - - afterEach(() => { - jest.useRealTimers(); - insightsPruningService.stopPruningTimer(); - jest.restoreAllMocks(); - }); - - test('pruning timeout is scheduled on start and rescheduled after each run', async () => { - const insightsByPeriodRepository = mock({ - pruneOldData: async () => { - return { affected: 0 }; - }, - }); - const insightsPruningService = new InsightsPruningService( - insightsByPeriodRepository, - insightsConfig, - licenseState, - mockLogger(), - ); - const pruneSpy = jest.spyOn(insightsPruningService, 'pruneInsights'); - const scheduleNextPruneSpy = jest.spyOn(insightsPruningService as any, 'scheduleNextPrune'); - - insightsPruningService.startPruningTimer(); - - // Wait for pruning timer promise to resolve - await jest.advanceTimersToNextTimerAsync(); - - expect(pruneSpy).toHaveBeenCalledTimes(1); - expect(scheduleNextPruneSpy).toHaveBeenCalledTimes(2); - }); - - test('if stopped during prune, it does not reschedule the timeout', async () => { - const insightsByPeriodRepository = mock({ - pruneOldData: async () => { - return { affected: 0 }; - }, - }); - const insightsPruningService = new InsightsPruningService( - insightsByPeriodRepository, - insightsConfig, - licenseState, - mockLogger(), - ); - - let resolvePrune!: () => void; - const pruneInsightsMock = jest - .spyOn(insightsPruningService, 'pruneInsights') - .mockImplementation( - async () => - await new Promise((resolve) => { - resolvePrune = () => resolve(); - }), - ); - - insightsConfig.pruneCheckIntervalHours = 1; - - insightsPruningService.startPruningTimer(); - jest.advanceTimersByTime(Time.hours.toMilliseconds + 1); // 1h + 1min - - // Immediately stop while pruning is "in progress" - insightsPruningService.stopPruningTimer(); - resolvePrune(); // Now allow the fake pruning to complete - - // Wait for pruning timer promise and reschedule to resolve - await jest.runOnlyPendingTimersAsync(); - - expect(pruneInsightsMock).toHaveBeenCalledTimes(1); // Only from start, not re-scheduled - }); - - test('pruneInsights is retried up when failing', async () => { - const pruneOldDataSpy = jest - .spyOn(insightsByPeriodRepository, 'pruneOldData') - .mockRejectedValueOnce(new Error('Fail 1')) - .mockRejectedValueOnce(new Error('Fail 2')) - .mockResolvedValueOnce({ affected: 0 }); - - await insightsPruningService.pruneInsights(); - await jest.advanceTimersByTimeAsync(Time.seconds.toMilliseconds * 2 + 1); - - expect(pruneOldDataSpy).toHaveBeenCalledTimes(3); - }); - }); }); diff --git a/packages/cli/src/modules/insights/__tests__/insights-pruning.service.test.ts b/packages/cli/src/modules/insights/__tests__/insights-pruning.service.test.ts new file mode 100644 index 0000000000..e69b3f707f --- /dev/null +++ b/packages/cli/src/modules/insights/__tests__/insights-pruning.service.test.ts @@ -0,0 +1,119 @@ +import type { LicenseState } from '@n8n/backend-common'; +import { mockLogger } from '@n8n/backend-test-utils'; +import { Time } from '@n8n/constants'; +import { mock } from 'jest-mock-extended'; + +import type { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository'; +import { InsightsPruningService } from '../insights-pruning.service'; +import { InsightsConfig } from '../insights.config'; + +describe('InsightsPruningService', () => { + let insightsConfig: InsightsConfig; + let insightsByPeriodRepository: InsightsByPeriodRepository; + let insightsPruningService: InsightsPruningService; + let licenseState: LicenseState; + + beforeAll(() => { + insightsConfig = new InsightsConfig(); + insightsConfig.maxAgeDays = 10; + insightsConfig.pruneCheckIntervalHours = 1; + insightsByPeriodRepository = mock(); + licenseState = mock({ + getInsightsRetentionMaxAge: () => insightsConfig.maxAgeDays, + }); + insightsPruningService = new InsightsPruningService( + insightsByPeriodRepository, + insightsConfig, + licenseState, + mockLogger(), + ); + }); + + describe('pruning scheduling', () => { + beforeEach(() => { + jest.useFakeTimers(); + insightsPruningService.startPruningTimer(); + }); + + afterEach(() => { + jest.useRealTimers(); + insightsPruningService.stopPruningTimer(); + jest.restoreAllMocks(); + }); + + test('pruning timeout is scheduled on start and rescheduled after each run', async () => { + const insightsByPeriodRepository = mock({ + pruneOldData: async () => { + return { affected: 0 }; + }, + }); + const insightsPruningService = new InsightsPruningService( + insightsByPeriodRepository, + insightsConfig, + licenseState, + mockLogger(), + ); + const pruneSpy = jest.spyOn(insightsPruningService, 'pruneInsights'); + const scheduleNextPruneSpy = jest.spyOn(insightsPruningService as any, 'scheduleNextPrune'); + + insightsPruningService.startPruningTimer(); + + // Wait for pruning timer promise to resolve + await jest.advanceTimersToNextTimerAsync(); + + expect(pruneSpy).toHaveBeenCalledTimes(1); + expect(scheduleNextPruneSpy).toHaveBeenCalledTimes(2); + }); + + test('if stopped during prune, it does not reschedule the timeout', async () => { + const insightsByPeriodRepository = mock({ + pruneOldData: async () => { + return { affected: 0 }; + }, + }); + const insightsPruningService = new InsightsPruningService( + insightsByPeriodRepository, + insightsConfig, + licenseState, + mockLogger(), + ); + + let resolvePrune!: () => void; + const pruneInsightsMock = jest + .spyOn(insightsPruningService, 'pruneInsights') + .mockImplementation( + async () => + await new Promise((resolve) => { + resolvePrune = () => resolve(); + }), + ); + + insightsConfig.pruneCheckIntervalHours = 1; + + insightsPruningService.startPruningTimer(); + jest.advanceTimersByTime(Time.hours.toMilliseconds + 1); // 1h + 1min + + // Immediately stop while pruning is "in progress" + insightsPruningService.stopPruningTimer(); + resolvePrune(); // Now allow the fake pruning to complete + + // Wait for pruning timer promise and reschedule to resolve + await jest.runOnlyPendingTimersAsync(); + + expect(pruneInsightsMock).toHaveBeenCalledTimes(1); // Only from start, not re-scheduled + }); + + test('pruneInsights is retried up when failing', async () => { + const pruneOldDataSpy = jest + .spyOn(insightsByPeriodRepository, 'pruneOldData') + .mockRejectedValueOnce(new Error('Fail 1')) + .mockRejectedValueOnce(new Error('Fail 2')) + .mockResolvedValueOnce({ affected: 0 }); + + await insightsPruningService.pruneInsights(); + await jest.advanceTimersByTimeAsync(Time.seconds.toMilliseconds * 2 + 1); + + expect(pruneOldDataSpy).toHaveBeenCalledTimes(3); + }); + }); +});