Files
PriceUpdaterAppv2/tests/services/schedule-signal-handling.test.js

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);
});
});
});