Files
PriceUpdaterAppv2/tests/services/scheduleService.test.js

594 lines
17 KiB
JavaScript

/**
* Unit tests for ScheduleService (Schedule Management) functionality
* Tests Requirements 1.6, 5.1 from the tui-missing-screens spec
*/
const ScheduleService = require("../../src/services/scheduleManagement");
const fs = require("fs").promises;
const path = require("path");
describe("ScheduleService", () => {
let scheduleService;
let testSchedulesFile;
beforeEach(() => {
// Use a unique test file for each test to avoid conflicts
testSchedulesFile = `test-schedules-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}.json`;
// Create a custom ScheduleService instance that uses our test file
scheduleService = new ScheduleService();
scheduleService.schedulesFile = path.join(process.cwd(), testSchedulesFile);
});
afterEach(async () => {
// Clean up test file after each test
try {
await fs.unlink(testSchedulesFile);
} catch (error) {
// File might not exist, that's okay
}
});
describe("validateSchedule", () => {
test("should return null for valid schedule", () => {
const validSchedule = {
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
recurrence: "daily",
enabled: true,
config: { targetTag: "sale" },
status: "pending",
};
const result = scheduleService.validateSchedule(validSchedule);
expect(result).toBeNull();
});
test("should return error for missing schedule object", () => {
const result = scheduleService.validateSchedule(null);
expect(result).toBe("Schedule object is required");
});
test("should return error for missing operation type", () => {
const invalidSchedule = {
scheduledTime: new Date(Date.now() + 86400000),
};
const result = scheduleService.validateSchedule(invalidSchedule);
expect(result).toBe("Operation type is required");
});
test("should return error for invalid operation type", () => {
const invalidSchedule = {
operationType: "invalid",
scheduledTime: new Date(Date.now() + 86400000),
};
const result = scheduleService.validateSchedule(invalidSchedule);
expect(result).toBe('Operation type must be "update" or "rollback"');
});
test("should return error for missing scheduled time", () => {
const invalidSchedule = {
operationType: "update",
};
const result = scheduleService.validateSchedule(invalidSchedule);
expect(result).toBe("Scheduled time is required");
});
test("should return error for invalid scheduled time", () => {
const invalidSchedule = {
operationType: "update",
scheduledTime: "invalid date",
};
const result = scheduleService.validateSchedule(invalidSchedule);
expect(result).toBe("Scheduled time must be a valid date");
});
test("should return error for past scheduled time on new schedules", () => {
const invalidSchedule = {
operationType: "update",
scheduledTime: new Date(Date.now() - 86400000), // Yesterday
};
const result = scheduleService.validateSchedule(invalidSchedule);
expect(result).toBe("Scheduled time must be in the future");
});
test("should allow past scheduled time for existing schedules", () => {
const existingSchedule = {
id: "existing_schedule",
operationType: "update",
scheduledTime: new Date(Date.now() - 86400000), // Yesterday
};
const result = scheduleService.validateSchedule(existingSchedule);
expect(result).toBeNull();
});
test("should return error for invalid recurrence", () => {
const invalidSchedule = {
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
recurrence: "invalid",
};
const result = scheduleService.validateSchedule(invalidSchedule);
expect(result).toBe(
"Recurrence must be one of: once, daily, weekly, monthly"
);
});
test("should return error for invalid status", () => {
const invalidSchedule = {
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
status: "invalid",
};
const result = scheduleService.validateSchedule(invalidSchedule);
expect(result).toBe(
"Status must be one of: pending, completed, failed, cancelled"
);
});
test("should return error for invalid enabled flag", () => {
const invalidSchedule = {
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
enabled: "not boolean",
};
const result = scheduleService.validateSchedule(invalidSchedule);
expect(result).toBe("Enabled must be a boolean value");
});
test("should return error for invalid config", () => {
const invalidSchedule = {
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
config: "not an object",
};
const result = scheduleService.validateSchedule(invalidSchedule);
expect(result).toBe("Config must be an object");
});
});
describe("loadSchedules", () => {
test("should return empty array when schedules file does not exist", async () => {
const result = await scheduleService.loadSchedules();
expect(result).toEqual([]);
});
test("should load schedules from JSON file and convert date strings to Date objects", async () => {
const mockScheduleData = [
{
id: "schedule_1",
operationType: "update",
scheduledTime: "2024-12-01T10:00:00.000Z",
recurrence: "once",
enabled: true,
config: { targetTag: "sale" },
status: "pending",
createdAt: "2024-11-01T10:00:00.000Z",
lastExecuted: null,
nextExecution: null,
},
];
// Write test data to file
await fs.writeFile(
scheduleService.schedulesFile,
JSON.stringify(mockScheduleData),
"utf8"
);
const result = await scheduleService.loadSchedules();
expect(result).toHaveLength(1);
expect(result[0].scheduledTime).toBeInstanceOf(Date);
expect(result[0].createdAt).toBeInstanceOf(Date);
expect(result[0].lastExecuted).toBeNull();
expect(result[0].nextExecution).toBeNull();
});
test("should throw error for invalid JSON", async () => {
// Write invalid JSON to file
await fs.writeFile(scheduleService.schedulesFile, "invalid json", "utf8");
await expect(scheduleService.loadSchedules()).rejects.toThrow();
});
});
describe("saveSchedules", () => {
test("should save schedules to JSON file with date objects converted to ISO strings", async () => {
const schedules = [
{
id: "schedule_1",
operationType: "update",
scheduledTime: new Date("2024-12-01T10:00:00.000Z"),
recurrence: "once",
enabled: true,
config: { targetTag: "sale" },
status: "pending",
createdAt: new Date("2024-11-01T10:00:00.000Z"),
lastExecuted: null,
nextExecution: null,
},
];
await scheduleService.saveSchedules(schedules);
// Read the file back and verify content
const fileContent = await fs.readFile(
scheduleService.schedulesFile,
"utf8"
);
const savedData = JSON.parse(fileContent);
expect(savedData).toHaveLength(1);
expect(savedData[0].scheduledTime).toBe("2024-12-01T10:00:00.000Z");
expect(savedData[0].createdAt).toBe("2024-11-01T10:00:00.000Z");
});
});
describe("addSchedule", () => {
test("should add a valid schedule with generated ID and defaults", async () => {
const newSchedule = {
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000), // Tomorrow
recurrence: "daily",
config: { targetTag: "sale" },
};
const result = await scheduleService.addSchedule(newSchedule);
expect(result.id).toMatch(/^schedule_\d+_[a-z0-9]+$/);
expect(result.operationType).toBe("update");
expect(result.scheduledTime).toBeInstanceOf(Date);
expect(result.recurrence).toBe("daily");
expect(result.enabled).toBe(true);
expect(result.status).toBe("pending");
expect(result.createdAt).toBeInstanceOf(Date);
expect(result.nextExecution).toBeInstanceOf(Date);
});
test("should apply default values for optional fields", async () => {
const newSchedule = {
operationType: "rollback",
scheduledTime: new Date(Date.now() + 86400000),
};
const result = await scheduleService.addSchedule(newSchedule);
expect(result.recurrence).toBe("once");
expect(result.enabled).toBe(true);
expect(result.config).toEqual({});
expect(result.nextExecution).toBeNull();
});
test("should throw error for invalid schedule", async () => {
const invalidSchedule = {
operationType: "invalid",
scheduledTime: new Date(Date.now() + 86400000),
};
await expect(
scheduleService.addSchedule(invalidSchedule)
).rejects.toThrow(
'Invalid schedule: Operation type must be "update" or "rollback"'
);
});
});
describe("updateSchedule", () => {
test("should update existing schedule", async () => {
// First add a schedule
const newSchedule = {
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
recurrence: "once",
config: { targetTag: "sale" },
};
const addedSchedule = await scheduleService.addSchedule(newSchedule);
// Then update it
const updates = {
enabled: false,
recurrence: "weekly",
};
const result = await scheduleService.updateSchedule(
addedSchedule.id,
updates
);
expect(result.enabled).toBe(false);
expect(result.recurrence).toBe("weekly");
expect(result.id).toBe(addedSchedule.id);
});
test("should recalculate nextExecution when scheduledTime is updated", async () => {
// First add a schedule
const newSchedule = {
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
recurrence: "daily",
};
const addedSchedule = await scheduleService.addSchedule(newSchedule);
// Update with new scheduled time
const newScheduledTime = new Date(Date.now() + 172800000); // 2 days from now
const updates = {
scheduledTime: newScheduledTime,
recurrence: "daily",
};
const result = await scheduleService.updateSchedule(
addedSchedule.id,
updates
);
expect(result.scheduledTime).toEqual(newScheduledTime);
expect(result.nextExecution).toBeInstanceOf(Date);
expect(result.nextExecution.getTime()).toBeGreaterThan(
newScheduledTime.getTime()
);
});
test("should throw error for non-existent schedule", async () => {
await expect(
scheduleService.updateSchedule("non_existent", { enabled: false })
).rejects.toThrow("Schedule with ID non_existent not found");
});
test("should throw error for invalid updates", async () => {
// First add a schedule
const newSchedule = {
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
};
const addedSchedule = await scheduleService.addSchedule(newSchedule);
const invalidUpdates = {
operationType: "invalid",
};
await expect(
scheduleService.updateSchedule(addedSchedule.id, invalidUpdates)
).rejects.toThrow("Invalid schedule update");
});
});
describe("deleteSchedule", () => {
test("should delete existing schedule and return true", async () => {
// First add a schedule
const newSchedule = {
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
config: { targetTag: "sale" },
};
const addedSchedule = await scheduleService.addSchedule(newSchedule);
// Then delete it
const result = await scheduleService.deleteSchedule(addedSchedule.id);
expect(result).toBe(true);
// Verify it's gone
const schedules = await scheduleService.loadSchedules();
expect(schedules).toHaveLength(0);
});
test("should return false for non-existent schedule", async () => {
const result = await scheduleService.deleteSchedule("non_existent");
expect(result).toBe(false);
});
});
describe("helper methods", () => {
test("should get schedules by status", async () => {
// Add schedules with different statuses
const schedule1 = await scheduleService.addSchedule({
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
});
await scheduleService.updateSchedule(schedule1.id, {
status: "completed",
});
const schedule2 = await scheduleService.addSchedule({
operationType: "rollback",
scheduledTime: new Date(Date.now() + 172800000),
});
const pendingSchedules = await scheduleService.getSchedulesByStatus(
"pending"
);
const completedSchedules = await scheduleService.getSchedulesByStatus(
"completed"
);
expect(pendingSchedules).toHaveLength(1);
expect(completedSchedules).toHaveLength(1);
expect(pendingSchedules[0].id).toBe(schedule2.id);
expect(completedSchedules[0].id).toBe(schedule1.id);
});
test("should get schedules by operation type", async () => {
const schedule1 = await scheduleService.addSchedule({
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
});
const schedule2 = await scheduleService.addSchedule({
operationType: "rollback",
scheduledTime: new Date(Date.now() + 172800000),
});
const schedule3 = await scheduleService.addSchedule({
operationType: "update",
scheduledTime: new Date(Date.now() + 259200000),
});
const updateSchedules = await scheduleService.getSchedulesByOperationType(
"update"
);
const rollbackSchedules =
await scheduleService.getSchedulesByOperationType("rollback");
expect(updateSchedules).toHaveLength(2);
expect(rollbackSchedules).toHaveLength(1);
expect(rollbackSchedules[0].id).toBe(schedule2.id);
});
test("should get enabled schedules", async () => {
const schedule1 = await scheduleService.addSchedule({
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
enabled: true,
});
const schedule2 = await scheduleService.addSchedule({
operationType: "rollback",
scheduledTime: new Date(Date.now() + 172800000),
enabled: false,
});
const schedule3 = await scheduleService.addSchedule({
operationType: "update",
scheduledTime: new Date(Date.now() + 259200000),
enabled: true,
});
const enabledSchedules = await scheduleService.getEnabledSchedules();
expect(enabledSchedules).toHaveLength(2);
expect(enabledSchedules.every((s) => s.enabled === true)).toBe(true);
});
test("should mark schedule as completed", async () => {
const schedule = await scheduleService.addSchedule({
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
});
const result = await scheduleService.markScheduleCompleted(schedule.id);
expect(result.status).toBe("completed");
expect(result.lastExecuted).toBeInstanceOf(Date);
});
test("should mark schedule as failed", async () => {
const schedule = await scheduleService.addSchedule({
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000),
});
const result = await scheduleService.markScheduleFailed(
schedule.id,
"Test error"
);
expect(result.status).toBe("failed");
expect(result.lastExecuted).toBeInstanceOf(Date);
expect(result.errorMessage).toBe("Test error");
});
});
describe("private methods", () => {
test("should generate unique IDs", () => {
const existingSchedules = [
{ id: "schedule_123_abc" },
{ id: "schedule_456_def" },
];
const id1 = scheduleService._generateId(existingSchedules);
const id2 = scheduleService._generateId(existingSchedules);
expect(id1).toMatch(/^schedule_\d+_[a-z0-9]+$/);
expect(id2).toMatch(/^schedule_\d+_[a-z0-9]+$/);
expect(id1).not.toBe(id2);
expect(existingSchedules.some((s) => s.id === id1)).toBe(false);
expect(existingSchedules.some((s) => s.id === id2)).toBe(false);
});
test("should calculate next execution for daily recurrence", () => {
const scheduledTime = new Date("2024-12-01T10:00:00.000Z");
const nextExecution = scheduleService._calculateNextExecution(
scheduledTime,
"daily"
);
expect(nextExecution).toBeInstanceOf(Date);
expect(nextExecution.getDate()).toBe(scheduledTime.getDate() + 1);
});
test("should calculate next execution for weekly recurrence", () => {
const scheduledTime = new Date("2024-12-01T10:00:00.000Z");
const nextExecution = scheduleService._calculateNextExecution(
scheduledTime,
"weekly"
);
expect(nextExecution).toBeInstanceOf(Date);
expect(nextExecution.getDate()).toBe(scheduledTime.getDate() + 7);
});
test("should calculate next execution for monthly recurrence", () => {
const scheduledTime = new Date("2024-11-01T10:00:00.000Z"); // November instead of December
const nextExecution = scheduleService._calculateNextExecution(
scheduledTime,
"monthly"
);
expect(nextExecution).toBeInstanceOf(Date);
expect(nextExecution.getMonth()).toBe(scheduledTime.getMonth() + 1);
});
test("should return null for once recurrence", () => {
const scheduledTime = new Date("2024-12-01T10:00:00.000Z");
const nextExecution = scheduleService._calculateNextExecution(
scheduledTime,
"once"
);
expect(nextExecution).toBeNull();
});
test("should return null for invalid recurrence", () => {
const scheduledTime = new Date("2024-12-01T10:00:00.000Z");
const nextExecution = scheduleService._calculateNextExecution(
scheduledTime,
"invalid"
);
expect(nextExecution).toBeNull();
});
});
});