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

429 lines
13 KiB
JavaScript

const fs = require("fs").promises;
const LogReaderService = require("../../src/services/logReader");
// Mock fs module
jest.mock("fs", () => ({
promises: {
stat: jest.fn(),
readFile: jest.fn(),
access: jest.fn(),
},
watchFile: jest.fn(),
unwatchFile: jest.fn(),
}));
describe("LogReaderService", () => {
let logReader;
let mockLogContent;
beforeEach(() => {
jest.clearAllMocks();
logReader = new LogReaderService("test-progress.md");
// Mock log content
mockLogContent = `# Shopify Price Update Progress Log
This file tracks the progress of price update operations.
## Price Update Operation - 2025-08-06 20:30:39 UTC
**Configuration:**
- Target Tag: Collection-Snowboard
- Price Adjustment: -10%
- Started: 2025-08-06 20:30:39 UTC
**Progress:**
- ✅ **The Collection Snowboard: Hydrogen** (gid://shopify/Product/8116504625443)
- Variant: gid://shopify/ProductVariant/44236769263907
- Price: $600 → $540
- Compare At Price: $600
- Updated: 2025-08-06 20:30:40 UTC
- ❌ **Failed Product** (gid://shopify/Product/failed123)
- Variant: gid://shopify/ProductVariant/failed456
- Error: Rate limit exceeded
- Failed: 2025-08-06 20:30:41 UTC
**Summary:**
- Total Products Processed: 2
- Successful Updates: 1
- Failed Updates: 1
- Duration: 2 seconds
- Completed: 2025-08-06 20:30:42 UTC
---
## Price Rollback Operation - 2025-08-06 20:31:06 UTC
**Configuration:**
- Target Tag: Collection-Snowboard
- Operation Mode: rollback
- Started: 2025-08-06 20:31:06 UTC
**Progress:**
- 🔄 **The Collection Snowboard: Hydrogen** (gid://shopify/Product/8116504625443)
- Variant: gid://shopify/ProductVariant/44236769263907
- Price: $540 → $600 (from Compare At: $600)
- Rolled back: 2025-08-06 20:31:07 UTC
**Rollback Summary:**
- Total Products Processed: 1
- Total Variants Processed: 1
- Eligible Variants: 1
- Successful Rollbacks: 1
- Failed Rollbacks: 0
- Skipped Variants: 0 (no compare-at price)
- Duration: 1 seconds
- Completed: 2025-08-06 20:31:07 UTC
---
**Error Analysis - 2025-08-06 20:31:10 UTC**
**Error Summary by Category:**
- Rate Limiting: 1 error
**Detailed Error Log:**
1. **Failed Product** (gid://shopify/Product/failed123)
- Variant: gid://shopify/ProductVariant/failed456
- Category: Rate Limiting
- Error: Rate limit exceeded (429)
`;
});
describe("File Reading", () => {
test("reads and parses log entries successfully", async () => {
const mockStats = { mtime: new Date("2025-08-06T20:32:00Z") };
fs.stat.mockResolvedValue(mockStats);
fs.readFile.mockResolvedValue(mockLogContent);
const entries = await logReader.readLogEntries();
expect(fs.stat).toHaveBeenCalledWith("test-progress.md");
expect(fs.readFile).toHaveBeenCalledWith("test-progress.md", "utf8");
expect(entries).toHaveLength(2); // Two main operations
expect(entries[0].type).toBe("rollback"); // Newest first
expect(entries[1].type).toBe("update");
});
test("returns empty array when file doesn't exist", async () => {
const error = new Error("File not found");
error.code = "ENOENT";
fs.stat.mockRejectedValue(error);
const entries = await logReader.readLogEntries();
expect(entries).toEqual([]);
});
test("throws error for other file system errors", async () => {
const error = new Error("Permission denied");
error.code = "EACCES";
fs.stat.mockRejectedValue(error);
await expect(logReader.readLogEntries()).rejects.toThrow(
"Permission denied"
);
});
test("uses cache when file hasn't changed", async () => {
const mockStats = { mtime: new Date("2025-08-06T20:32:00Z") };
fs.stat.mockResolvedValue(mockStats);
fs.readFile.mockResolvedValue(mockLogContent);
// First call
await logReader.readLogEntries();
expect(fs.readFile).toHaveBeenCalledTimes(1);
// Second call with same mtime
await logReader.readLogEntries();
expect(fs.readFile).toHaveBeenCalledTimes(1); // Should use cache
});
test("refreshes cache when file has changed", async () => {
const oldStats = { mtime: new Date("2025-08-06T20:32:00Z") };
const newStats = { mtime: new Date("2025-08-06T20:33:00Z") };
fs.stat.mockResolvedValueOnce(oldStats).mockResolvedValueOnce(newStats);
fs.readFile.mockResolvedValue(mockLogContent);
// First call
await logReader.readLogEntries();
expect(fs.readFile).toHaveBeenCalledTimes(1);
// Second call with different mtime
await logReader.readLogEntries();
expect(fs.readFile).toHaveBeenCalledTimes(2); // Should refresh cache
});
});
describe("Log Parsing", () => {
beforeEach(async () => {
const mockStats = { mtime: new Date("2025-08-06T20:32:00Z") };
fs.stat.mockResolvedValue(mockStats);
fs.readFile.mockResolvedValue(mockLogContent);
});
test("parses operation headers correctly", async () => {
const entries = await logReader.readLogEntries();
const updateOp = entries.find((e) => e.type === "update");
expect(updateOp.title).toBe(
"Price Update Operation - 2025-08-06 20:30:39 UTC"
);
expect(updateOp.level).toBe("INFO");
expect(updateOp.message).toBe(
"Started: Price Update Operation - 2025-08-06 20:30:39 UTC"
);
const rollbackOp = entries.find((e) => e.type === "rollback");
expect(rollbackOp.title).toBe(
"Price Rollback Operation - 2025-08-06 20:31:06 UTC"
);
});
test("parses configuration sections correctly", async () => {
const entries = await logReader.readLogEntries();
const updateOp = entries.find((e) => e.type === "update");
expect(updateOp.configuration["Target Tag"]).toBe("Collection-Snowboard");
expect(updateOp.configuration["Price Adjustment"]).toBe("-10%");
expect(updateOp.details).toContain("Target Tag: Collection-Snowboard");
});
test("parses timestamps correctly", async () => {
const entries = await logReader.readLogEntries();
const updateOp = entries.find((e) => e.type === "update");
expect(updateOp.timestamp).toEqual(new Date("2025-08-06T20:30:39Z"));
expect(updateOp.rawTimestamp).toBe("2025-08-06 20:30:39 UTC");
});
test("identifies operation types correctly", async () => {
const entries = await logReader.readLogEntries();
expect(entries.some((e) => e.type === "update")).toBe(true);
expect(entries.some((e) => e.type === "rollback")).toBe(true);
});
test("sorts entries by timestamp (newest first)", async () => {
const entries = await logReader.readLogEntries();
// Rollback operation (2025-08-06 20:31:06) should come before update (2025-08-06 20:30:39)
expect(entries[0].type).toBe("rollback");
expect(entries[1].type).toBe("update");
});
});
describe("Pagination", () => {
beforeEach(async () => {
const mockStats = { mtime: new Date("2025-08-06T20:32:00Z") };
fs.stat.mockResolvedValue(mockStats);
fs.readFile.mockResolvedValue(mockLogContent);
});
test("returns paginated results correctly", async () => {
const result = await logReader.getPaginatedEntries({
page: 0,
pageSize: 1,
levelFilter: "ALL",
searchTerm: "",
});
expect(result.entries).toHaveLength(1);
expect(result.pagination.currentPage).toBe(0);
expect(result.pagination.pageSize).toBe(1);
expect(result.pagination.totalEntries).toBe(2);
expect(result.pagination.totalPages).toBe(2);
expect(result.pagination.hasNextPage).toBe(true);
expect(result.pagination.hasPreviousPage).toBe(false);
});
test("handles second page correctly", async () => {
const result = await logReader.getPaginatedEntries({
page: 1,
pageSize: 1,
levelFilter: "ALL",
searchTerm: "",
});
expect(result.entries).toHaveLength(1);
expect(result.pagination.currentPage).toBe(1);
expect(result.pagination.hasNextPage).toBe(false);
expect(result.pagination.hasPreviousPage).toBe(true);
});
test("uses default pagination options", async () => {
const result = await logReader.getPaginatedEntries();
expect(result.pagination.pageSize).toBe(20);
expect(result.pagination.currentPage).toBe(0);
expect(result.filters.levelFilter).toBe("ALL");
expect(result.filters.searchTerm).toBe("");
});
});
describe("Filtering", () => {
beforeEach(async () => {
const mockStats = { mtime: new Date("2025-08-06T20:32:00Z") };
fs.stat.mockResolvedValue(mockStats);
fs.readFile.mockResolvedValue(mockLogContent);
});
test("filters by log level correctly", async () => {
const result = await logReader.getPaginatedEntries({
levelFilter: "INFO",
});
expect(result.entries.every((e) => e.level === "INFO")).toBe(true);
expect(result.filters.levelFilter).toBe("INFO");
});
test("filters by search term in message", async () => {
const result = await logReader.getPaginatedEntries({
searchTerm: "rollback",
});
expect(result.entries.length).toBeGreaterThan(0);
expect(
result.entries.some(
(e) =>
e.message.toLowerCase().includes("rollback") ||
e.title.toLowerCase().includes("rollback")
)
).toBe(true);
});
test("filters by search term in details", async () => {
const result = await logReader.getPaginatedEntries({
searchTerm: "Collection-Snowboard",
});
expect(result.entries.length).toBeGreaterThan(0);
expect(
result.entries.some((e) => e.details.includes("Collection-Snowboard"))
).toBe(true);
});
test("returns empty results for non-matching filters", async () => {
const result = await logReader.getPaginatedEntries({
searchTerm: "nonexistent-term-xyz",
});
expect(result.entries).toHaveLength(0);
expect(result.pagination.totalEntries).toBe(0);
});
});
describe("Statistics", () => {
beforeEach(async () => {
const mockStats = { mtime: new Date("2025-08-06T20:32:00Z") };
fs.stat.mockResolvedValue(mockStats);
fs.readFile.mockResolvedValue(mockLogContent);
});
test("calculates log statistics correctly", async () => {
const stats = await logReader.getLogStatistics();
expect(stats.totalEntries).toBe(2);
expect(stats.byLevel.INFO).toBe(2);
expect(stats.byType.update).toBe(1);
expect(stats.byType.rollback).toBe(1);
expect(stats.operations.total).toBe(2);
});
test("tracks date range correctly", async () => {
const stats = await logReader.getLogStatistics();
expect(stats.dateRange.oldest).toEqual(new Date("2025-08-06T20:30:39Z"));
expect(stats.dateRange.newest).toEqual(new Date("2025-08-06T20:31:06Z"));
});
});
describe("Cache Management", () => {
test("clears cache when requested", async () => {
const mockStats = { mtime: new Date("2025-08-06T20:32:00Z") };
fs.stat.mockResolvedValue(mockStats);
fs.readFile.mockResolvedValue(mockLogContent);
// Load data to populate cache
await logReader.readLogEntries();
expect(fs.readFile).toHaveBeenCalledTimes(1);
// Clear cache and load again
logReader.clearCache();
await logReader.readLogEntries();
expect(fs.readFile).toHaveBeenCalledTimes(2);
});
});
describe("File Watching", () => {
test("sets up file watching correctly", () => {
const mockCallback = jest.fn();
const mockCleanup = jest.fn();
require("fs").watchFile.mockReturnValue(mockCleanup);
const cleanup = logReader.watchFile(mockCallback);
expect(require("fs").watchFile).toHaveBeenCalledWith(
"test-progress.md",
expect.any(Function)
);
expect(typeof cleanup).toBe("function");
});
test("returns no-op cleanup function when watching fails", () => {
require("fs").watchFile.mockImplementation(() => {
throw new Error("Watch failed");
});
const cleanup = logReader.watchFile(() => {});
expect(typeof cleanup).toBe("function");
// Should not throw when called
expect(() => cleanup()).not.toThrow();
});
});
describe("Error Handling", () => {
test("handles malformed log content gracefully", async () => {
const malformedContent =
"This is not a valid log format\nRandom text\n## Invalid header";
const mockStats = { mtime: new Date("2025-08-06T20:32:00Z") };
fs.stat.mockResolvedValue(mockStats);
fs.readFile.mockResolvedValue(malformedContent);
const entries = await logReader.readLogEntries();
// Should return empty array or minimal parsed data without throwing
expect(Array.isArray(entries)).toBe(true);
});
test("handles invalid timestamps gracefully", async () => {
const invalidTimestampContent = `## Price Update Operation - invalid-timestamp
**Configuration:**
- Target Tag: test
**Progress:**
**Summary:**
- Total Products Processed: 0
`;
const mockStats = { mtime: new Date("2025-08-06T20:32:00Z") };
fs.stat.mockResolvedValue(mockStats);
fs.readFile.mockResolvedValue(invalidTimestampContent);
const entries = await logReader.readLogEntries();
expect(entries).toHaveLength(1);
expect(entries[0].timestamp).toBeInstanceOf(Date);
});
});
});