/** * Unit tests for enhanced signal handling in scheduled operations * Tests Requirements 3.1, 3.2, 3.3 from the scheduled-price-updates spec */ const ScheduleService = require("../../src/services/schedule"); const Logger = require("../../src/utils/logger"); // Mock logger to avoid file operations during tests jest.mock("../../src/utils/logger"); describe("Enhanced Signal Handling for Scheduled Operations", () => { let scheduleService; let mockLogger; let originalProcessOn; let originalProcessExit; let signalHandlers; beforeEach(() => { // Mock logger mockLogger = { info: jest.fn().mockResolvedValue(), warning: jest.fn().mockResolvedValue(), error: jest.fn().mockResolvedValue(), }; Logger.mockImplementation(() => mockLogger); scheduleService = new ScheduleService(mockLogger); // Mock process methods signalHandlers = {}; originalProcessOn = process.on; originalProcessExit = process.exit; process.on = jest.fn((signal, handler) => { signalHandlers[signal] = handler; }); process.exit = jest.fn(); // Clear any existing timers jest.clearAllTimers(); jest.useFakeTimers(); }); afterEach(() => { // Restore original process methods process.on = originalProcessOn; process.exit = originalProcessExit; // Clean up schedule service scheduleService.cleanup(); jest.useRealTimers(); jest.clearAllMocks(); }); describe("Requirement 3.1: Cancellation during wait period", () => { test("should support cancellation during scheduled wait period", async () => { // Arrange const scheduledTime = new Date(Date.now() + 5000); // 5 seconds from now let cancelCallbackExecuted = false; const onCancel = () => { cancelCallbackExecuted = true; }; // Act const waitPromise = scheduleService.waitUntilScheduledTime( scheduledTime, onCancel ); // Simulate cancellation after 1 second setTimeout(() => { scheduleService.cleanup(); // This triggers cancellation }, 1000); jest.advanceTimersByTime(1000); const result = await waitPromise; // Assert expect(result).toBe(false); expect(cancelCallbackExecuted).toBe(true); expect(scheduleService.cancelRequested).toBe(true); }); test("should clean up countdown display on cancellation", async () => { // Arrange const scheduledTime = new Date(Date.now() + 5000); const stopCountdownSpy = jest.spyOn( scheduleService, "stopCountdownDisplay" ); // Act const waitPromise = scheduleService.waitUntilScheduledTime( scheduledTime, () => {} ); // Advance time slightly to let the cancellation check start jest.advanceTimersByTime(150); scheduleService.cleanup(); // Advance time to trigger cancellation check jest.advanceTimersByTime(150); const result = await waitPromise; // Assert expect(result).toBe(false); expect(stopCountdownSpy).toHaveBeenCalled(); }, 10000); }); describe("Requirement 3.2: Clear cancellation confirmation messages", () => { test("should provide clear cancellation confirmation through callback", async () => { // Arrange const scheduledTime = new Date(Date.now() + 3000); let cancellationMessage = ""; const onCancel = () => { cancellationMessage = "Operation cancelled by user"; }; // Act const waitPromise = scheduleService.waitUntilScheduledTime( scheduledTime, onCancel ); // Advance time slightly to let the cancellation check start jest.advanceTimersByTime(150); scheduleService.cleanup(); // Advance time to trigger cancellation check jest.advanceTimersByTime(150); const result = await waitPromise; // Assert expect(result).toBe(false); expect(cancellationMessage).toBe("Operation cancelled by user"); }, 10000); test("should clean up resources properly on cancellation", () => { // Arrange const scheduledTime = new Date(Date.now() + 5000); scheduleService.waitUntilScheduledTime(scheduledTime, () => {}); // Act scheduleService.cleanup(); // Assert expect(scheduleService.cancelRequested).toBe(true); expect(scheduleService.countdownInterval).toBe(null); expect(scheduleService.currentTimeoutId).toBe(null); }); }); describe("Requirement 3.3: No interruption once operations begin", () => { test("should complete wait period when not cancelled", async () => { // Arrange const scheduledTime = new Date(Date.now() + 2000); // 2 seconds from now // Act const waitPromise = scheduleService.waitUntilScheduledTime( scheduledTime, () => {} ); // Fast-forward time to scheduled time jest.advanceTimersByTime(2000); const result = await waitPromise; // Assert expect(result).toBe(true); expect(scheduleService.cancelRequested).toBe(false); }); test("should handle immediate execution when scheduled time is now or past", async () => { // Arrange const scheduledTime = new Date(Date.now() - 1000); // 1 second ago // Act const result = await scheduleService.waitUntilScheduledTime( scheduledTime, () => {} ); // Assert expect(result).toBe(true); }); test("should not cancel if cleanup is called after timeout completes", async () => { // Arrange const scheduledTime = new Date(Date.now() + 1000); // 1 second from now // Act const waitPromise = scheduleService.waitUntilScheduledTime( scheduledTime, () => {} ); // Let the timeout complete first jest.advanceTimersByTime(1000); // Then try to cleanup (should not affect the result) scheduleService.cleanup(); const result = await waitPromise; // Assert expect(result).toBe(true); // Should still proceed since timeout completed first }); }); describe("Resource management", () => { test("should properly initialize and reset state", () => { // Assert initial state expect(scheduleService.cancelRequested).toBe(false); expect(scheduleService.countdownInterval).toBe(null); expect(scheduleService.currentTimeoutId).toBe(null); // Test reset functionality scheduleService.cancelRequested = true; scheduleService.reset(); expect(scheduleService.cancelRequested).toBe(false); expect(scheduleService.countdownInterval).toBe(null); expect(scheduleService.currentTimeoutId).toBe(null); }); test("should handle multiple cleanup calls safely", () => { // Arrange const scheduledTime = new Date(Date.now() + 5000); scheduleService.waitUntilScheduledTime(scheduledTime, () => {}); // Act - multiple cleanup calls should not throw errors expect(() => { scheduleService.cleanup(); scheduleService.cleanup(); scheduleService.cleanup(); }).not.toThrow(); // Assert expect(scheduleService.cancelRequested).toBe(true); }); }); describe("Integration with main signal handlers", () => { test("should coordinate with external signal handling", async () => { // This test verifies that the ScheduleService works properly when // signal handling is managed externally (as in the main application) // Arrange const scheduledTime = new Date(Date.now() + 3000); let externalCancellationTriggered = false; // Simulate external signal handler calling cleanup const simulateExternalSignalHandler = () => { externalCancellationTriggered = true; scheduleService.cleanup(); }; // Act const waitPromise = scheduleService.waitUntilScheduledTime( scheduledTime, () => {} ); // Simulate external signal after 1 second setTimeout(simulateExternalSignalHandler, 1000); jest.advanceTimersByTime(1000); const result = await waitPromise; // Assert expect(result).toBe(false); expect(externalCancellationTriggered).toBe(true); expect(scheduleService.cancelRequested).toBe(true); }); }); });