289 lines
7.6 KiB
JavaScript
289 lines
7.6 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|
|
});
|
|
});
|