const Logger = require("../../src/utils/logger"); const ProgressService = require("../../src/services/progress"); // Mock the ProgressService jest.mock("../../src/services/progress"); describe("Logger", () => { let logger; let mockProgressService; let consoleSpy; beforeEach(() => { // Create mock progress service mockProgressService = { logOperationStart: jest.fn(), logRollbackStart: jest.fn(), logProductUpdate: jest.fn(), logRollbackUpdate: jest.fn(), logCompletionSummary: jest.fn(), logRollbackSummary: jest.fn(), logError: jest.fn(), logErrorAnalysis: jest.fn(), }; // Mock the ProgressService constructor ProgressService.mockImplementation(() => mockProgressService); logger = new Logger(); // Spy on console methods consoleSpy = { log: jest.spyOn(console, "log").mockImplementation(() => {}), warn: jest.spyOn(console, "warn").mockImplementation(() => {}), error: jest.spyOn(console, "error").mockImplementation(() => {}), }; }); afterEach(() => { jest.clearAllMocks(); consoleSpy.log.mockRestore(); consoleSpy.warn.mockRestore(); consoleSpy.error.mockRestore(); }); describe("Rollback Logging Methods", () => { describe("logRollbackStart", () => { it("should log rollback operation start to console and progress file", async () => { const config = { targetTag: "test-tag", shopDomain: "test-shop.myshopify.com", }; await logger.logRollbackStart(config); // Check console output expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining( "Starting price rollback operation with configuration:" ) ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Target Tag: test-tag") ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Operation Mode: rollback") ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Shop Domain: test-shop.myshopify.com") ); // Check progress service was called expect(mockProgressService.logRollbackStart).toHaveBeenCalledWith( config ); }); }); describe("logRollbackUpdate", () => { it("should log successful rollback operations to console and progress file", async () => { const entry = { productTitle: "Test Product", productId: "gid://shopify/Product/123", variantId: "gid://shopify/ProductVariant/456", oldPrice: 1000.0, compareAtPrice: 750.0, newPrice: 750.0, }; await logger.logRollbackUpdate(entry); // Check console output contains rollback-specific formatting expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("🔄") ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining('Rolled back "Test Product"') ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Price: 1000 → 750") ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("from Compare At: 750") ); // Check progress service was called expect(mockProgressService.logRollbackUpdate).toHaveBeenCalledWith( entry ); }); }); describe("logRollbackSummary", () => { it("should log rollback completion summary to console and progress file", async () => { const summary = { totalProducts: 5, totalVariants: 8, eligibleVariants: 6, successfulRollbacks: 5, failedRollbacks: 1, skippedVariants: 2, startTime: new Date(Date.now() - 30000), // 30 seconds ago }; await logger.logRollbackSummary(summary); // Check console output contains rollback-specific formatting expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("ROLLBACK OPERATION COMPLETE") ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Total Products Processed: 5") ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Total Variants Processed: 8") ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Eligible Variants: 6") ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Successful Rollbacks: ") ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Failed Rollbacks: ") ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Skipped Variants: ") ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("no compare-at price") ); // Check progress service was called expect(mockProgressService.logRollbackSummary).toHaveBeenCalledWith( summary ); }); it("should handle zero failed rollbacks without red coloring", async () => { const summary = { totalProducts: 3, totalVariants: 5, eligibleVariants: 5, successfulRollbacks: 5, failedRollbacks: 0, skippedVariants: 0, startTime: new Date(Date.now() - 15000), }; await logger.logRollbackSummary(summary); // Should show failed rollbacks without red coloring when zero expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Failed Rollbacks: 0") ); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Skipped Variants: 0") ); }); it("should show colored output for failed rollbacks and skipped variants when greater than zero", async () => { const summary = { totalProducts: 5, totalVariants: 8, eligibleVariants: 6, successfulRollbacks: 4, failedRollbacks: 2, skippedVariants: 2, startTime: new Date(Date.now() - 45000), }; await logger.logRollbackSummary(summary); // Should show colored output for non-zero values const logCalls = consoleSpy.log.mock.calls.map((call) => call[0]); const failedRollbacksCall = logCalls.find((call) => call.includes("Failed Rollbacks:") ); const skippedVariantsCall = logCalls.find((call) => call.includes("Skipped Variants:") ); expect(failedRollbacksCall).toContain("\x1b[31m"); // Red color code expect(skippedVariantsCall).toContain("\x1b[33m"); // Yellow color code }); }); }); describe("Rollback vs Update Distinction", () => { it("should distinguish rollback logs from update logs in console output", async () => { const updateEntry = { productTitle: "Test Product", oldPrice: 750.0, newPrice: 1000.0, compareAtPrice: 1000.0, }; const rollbackEntry = { productTitle: "Test Product", oldPrice: 1000.0, compareAtPrice: 750.0, newPrice: 750.0, }; await logger.logProductUpdate(updateEntry); await logger.logRollbackUpdate(rollbackEntry); const logCalls = consoleSpy.log.mock.calls.map((call) => call[0]); // Update should use checkmark emoji const updateCall = logCalls.find((call) => call.includes("Updated")); expect(updateCall).toContain("✅"); // Rollback should use rollback emoji const rollbackCall = logCalls.find((call) => call.includes("Rolled back") ); expect(rollbackCall).toContain("🔄"); }); it("should call different progress service methods for updates vs rollbacks", async () => { const updateEntry = { productTitle: "Test", oldPrice: 750, newPrice: 1000, }; const rollbackEntry = { productTitle: "Test", oldPrice: 1000, newPrice: 750, compareAtPrice: 750, }; await logger.logProductUpdate(updateEntry); await logger.logRollbackUpdate(rollbackEntry); expect(mockProgressService.logProductUpdate).toHaveBeenCalledWith( updateEntry ); expect(mockProgressService.logRollbackUpdate).toHaveBeenCalledWith( rollbackEntry ); }); }); describe("Error Handling", () => { it("should handle progress service errors gracefully", async () => { mockProgressService.logRollbackStart.mockRejectedValue( new Error("Progress service error") ); const config = { targetTag: "test-tag", shopDomain: "test-shop.myshopify.com", }; // Should not throw even if progress service fails await expect(logger.logRollbackStart(config)).resolves.not.toThrow(); // Console output should still work expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Starting price rollback operation") ); }); it("should handle rollback update logging errors gracefully", async () => { mockProgressService.logRollbackUpdate.mockRejectedValue( new Error("Progress service error") ); const entry = { productTitle: "Test Product", oldPrice: 1000, newPrice: 750, compareAtPrice: 750, }; // Should not throw even if progress service fails await expect(logger.logRollbackUpdate(entry)).resolves.not.toThrow(); // Console output should still work expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Rolled back") ); }); it("should handle rollback summary logging errors gracefully", async () => { mockProgressService.logRollbackSummary.mockRejectedValue( new Error("Progress service error") ); const summary = { totalProducts: 5, successfulRollbacks: 4, failedRollbacks: 1, skippedVariants: 0, startTime: new Date(), }; // Should not throw even if progress service fails await expect(logger.logRollbackSummary(summary)).resolves.not.toThrow(); // Console output should still work expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("ROLLBACK OPERATION COMPLETE") ); }); }); describe("Existing Logger Methods", () => { describe("Basic logging methods", () => { it("should log info messages to console", async () => { await logger.info("Test info message"); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Test info message") ); }); it("should log warning messages to console", async () => { await logger.warning("Test warning message"); expect(consoleSpy.warn).toHaveBeenCalledWith( expect.stringContaining("Test warning message") ); }); it("should log error messages to console", async () => { await logger.error("Test error message"); expect(consoleSpy.error).toHaveBeenCalledWith( expect.stringContaining("Test error message") ); }); }); describe("Operation start logging", () => { it("should log operation start for update mode", async () => { const config = { targetTag: "test-tag", priceAdjustmentPercentage: 10, shopDomain: "test-shop.myshopify.com", }; await logger.logOperationStart(config); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Starting price update operation") ); expect(mockProgressService.logOperationStart).toHaveBeenCalledWith( config ); }); }); describe("Product update logging", () => { it("should log product updates", async () => { const entry = { productTitle: "Test Product", oldPrice: 100, newPrice: 110, compareAtPrice: 100, }; await logger.logProductUpdate(entry); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Updated") ); expect(mockProgressService.logProductUpdate).toHaveBeenCalledWith( entry ); }); }); describe("Completion summary logging", () => { it("should log completion summary", async () => { const summary = { totalProducts: 5, successfulUpdates: 4, failedUpdates: 1, startTime: new Date(), }; await logger.logCompletionSummary(summary); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("OPERATION COMPLETE") ); expect(mockProgressService.logCompletionSummary).toHaveBeenCalledWith( summary ); }); }); describe("Error logging", () => { it("should log product errors", async () => { const errorEntry = { productTitle: "Test Product", errorMessage: "Test error", }; await logger.logProductError(errorEntry); expect(mockProgressService.logError).toHaveBeenCalledWith(errorEntry); }); it("should log error analysis", async () => { const errors = [ { errorMessage: "Error 1" }, { errorMessage: "Error 2" }, ]; const summary = { totalProducts: 2 }; await logger.logErrorAnalysis(errors, summary); expect(mockProgressService.logErrorAnalysis).toHaveBeenCalledWith( errors, summary ); }); }); describe("Product count logging", () => { it("should log product count", async () => { await logger.logProductCount(5); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Found 5 products") ); }); it("should handle zero products", async () => { await logger.logProductCount(0); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining("Found 0 products") ); }); }); }); });