594 lines
17 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
});
|