const ProgressService = require("../../src/services/progress"); const fs = require("fs").promises; const path = require("path"); describe("ProgressService", () => { let progressService; let testFilePath; beforeEach(() => { // Use a unique test file for each test to avoid conflicts testFilePath = `test-progress-${Date.now()}-${Math.random() .toString(36) .substr(2, 9)}.md`; progressService = new ProgressService(testFilePath); }); afterEach(async () => { // Clean up test file after each test try { await fs.unlink(testFilePath); } catch (error) { // File might not exist, that's okay } }); describe("formatTimestamp", () => { test("should format timestamp correctly", () => { const testDate = new Date("2024-01-15T14:30:45.123Z"); const formatted = progressService.formatTimestamp(testDate); expect(formatted).toBe("2024-01-15 14:30:45 UTC"); }); test("should use current date when no date provided", () => { const formatted = progressService.formatTimestamp(); // Should be a valid timestamp format expect(formatted).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC$/); }); test("should handle different dates correctly", () => { const testCases = [ { input: new Date("2023-12-31T23:59:59.999Z"), expected: "2023-12-31 23:59:59 UTC", }, { input: new Date("2024-01-01T00:00:00.000Z"), expected: "2024-01-01 00:00:00 UTC", }, { input: new Date("2024-06-15T12:00:00.500Z"), expected: "2024-06-15 12:00:00 UTC", }, ]; testCases.forEach(({ input, expected }) => { expect(progressService.formatTimestamp(input)).toBe(expected); }); }); }); describe("logOperationStart", () => { test("should create progress file and log operation start", async () => { const config = { targetTag: "test-tag", priceAdjustmentPercentage: 10, }; await progressService.logOperationStart(config); // Check that file was created const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("# Shopify Price Update Progress Log"); expect(content).toContain("## Price Update Operation -"); expect(content).toContain("Target Tag: test-tag"); expect(content).toContain("Price Adjustment: 10%"); expect(content).toContain("**Configuration:**"); expect(content).toContain("**Progress:**"); }); test("should handle negative percentage", async () => { const config = { targetTag: "clearance", priceAdjustmentPercentage: -25, }; await progressService.logOperationStart(config); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("Target Tag: clearance"); expect(content).toContain("Price Adjustment: -25%"); }); test("should handle special characters in tag", async () => { const config = { targetTag: "sale-2024_special!", priceAdjustmentPercentage: 15.5, }; await progressService.logOperationStart(config); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("Target Tag: sale-2024_special!"); expect(content).toContain("Price Adjustment: 15.5%"); }); }); describe("logRollbackStart", () => { test("should create progress file and log rollback operation start", async () => { const config = { targetTag: "rollback-tag", }; await progressService.logRollbackStart(config); // Check that file was created const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("# Shopify Price Update Progress Log"); expect(content).toContain("## Price Rollback Operation -"); expect(content).toContain("Target Tag: rollback-tag"); expect(content).toContain("Operation Mode: rollback"); expect(content).toContain("**Configuration:**"); expect(content).toContain("**Progress:**"); }); test("should distinguish rollback from update operations in logs", async () => { const updateConfig = { targetTag: "update-tag", priceAdjustmentPercentage: 10, }; const rollbackConfig = { targetTag: "rollback-tag", }; await progressService.logOperationStart(updateConfig); await progressService.logRollbackStart(rollbackConfig); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("## Price Update Operation -"); expect(content).toContain("Price Adjustment: 10%"); expect(content).toContain("## Price Rollback Operation -"); expect(content).toContain("Operation Mode: rollback"); }); }); describe("logProductUpdate", () => { test("should log successful product update", async () => { // First create the file await progressService.logOperationStart({ targetTag: "test", priceAdjustmentPercentage: 10, }); const entry = { productId: "gid://shopify/Product/123456789", productTitle: "Test Product", variantId: "gid://shopify/ProductVariant/987654321", oldPrice: 29.99, newPrice: 32.99, }; await progressService.logProductUpdate(entry); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain( "✅ **Test Product** (gid://shopify/Product/123456789)" ); expect(content).toContain( "Variant: gid://shopify/ProductVariant/987654321" ); expect(content).toContain("Price: $29.99 → $32.99"); expect(content).toContain("Updated:"); }); test("should handle products with special characters in title", async () => { await progressService.logOperationStart({ targetTag: "test", priceAdjustmentPercentage: 10, }); const entry = { productId: "gid://shopify/Product/123", productTitle: 'Product with "Quotes" & Special Chars!', variantId: "gid://shopify/ProductVariant/456", oldPrice: 10.0, newPrice: 11.0, }; await progressService.logProductUpdate(entry); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain('**Product with "Quotes" & Special Chars!**'); }); test("should handle decimal prices correctly", async () => { await progressService.logOperationStart({ targetTag: "test", priceAdjustmentPercentage: 5.5, }); const entry = { productId: "gid://shopify/Product/123", productTitle: "Decimal Price Product", variantId: "gid://shopify/ProductVariant/456", oldPrice: 19.95, newPrice: 21.05, }; await progressService.logProductUpdate(entry); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("Price: $19.95 → $21.05"); }); }); describe("logRollbackUpdate", () => { test("should log successful rollback operation", async () => { // First create the file await progressService.logRollbackStart({ targetTag: "rollback-test", }); const entry = { productId: "gid://shopify/Product/123456789", productTitle: "Rollback Test Product", variantId: "gid://shopify/ProductVariant/987654321", oldPrice: 1000.0, compareAtPrice: 750.0, newPrice: 750.0, }; await progressService.logRollbackUpdate(entry); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain( "🔄 **Rollback Test Product** (gid://shopify/Product/123456789)" ); expect(content).toContain( "Variant: gid://shopify/ProductVariant/987654321" ); expect(content).toContain("Price: $1000 → $750 (from Compare At: $750)"); expect(content).toContain("Rolled back:"); }); test("should distinguish rollback from update entries", async () => { await progressService.logRollbackStart({ targetTag: "test", }); const updateEntry = { productId: "gid://shopify/Product/123", productTitle: "Update Product", variantId: "gid://shopify/ProductVariant/456", oldPrice: 750.0, newPrice: 1000.0, }; const rollbackEntry = { productId: "gid://shopify/Product/789", productTitle: "Rollback Product", variantId: "gid://shopify/ProductVariant/012", oldPrice: 1000.0, compareAtPrice: 750.0, newPrice: 750.0, }; await progressService.logProductUpdate(updateEntry); await progressService.logRollbackUpdate(rollbackEntry); const content = await fs.readFile(testFilePath, "utf8"); // Update entry should use checkmark expect(content).toContain("✅ **Update Product**"); expect(content).toContain("Updated:"); // Rollback entry should use rollback emoji expect(content).toContain("🔄 **Rollback Product**"); expect(content).toContain("from Compare At:"); expect(content).toContain("Rolled back:"); }); test("should handle products with special characters in rollback", async () => { await progressService.logRollbackStart({ targetTag: "test", }); const entry = { productId: "gid://shopify/Product/123", productTitle: 'Rollback Product with "Quotes" & Special Chars!', variantId: "gid://shopify/ProductVariant/456", oldPrice: 500.0, compareAtPrice: 400.0, newPrice: 400.0, }; await progressService.logRollbackUpdate(entry); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain( '**Rollback Product with "Quotes" & Special Chars!**' ); }); }); describe("logError", () => { test("should log error with all details", async () => { await progressService.logOperationStart({ targetTag: "test", priceAdjustmentPercentage: 10, }); const entry = { productId: "gid://shopify/Product/123", productTitle: "Failed Product", variantId: "gid://shopify/ProductVariant/456", errorMessage: "Invalid price data", }; await progressService.logError(entry); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain( "❌ **Failed Product** (gid://shopify/Product/123)" ); expect(content).toContain("Variant: gid://shopify/ProductVariant/456"); expect(content).toContain("Error: Invalid price data"); expect(content).toContain("Failed:"); }); test("should handle error without variant ID", async () => { await progressService.logOperationStart({ targetTag: "test", priceAdjustmentPercentage: 10, }); const entry = { productId: "gid://shopify/Product/123", productTitle: "Failed Product", errorMessage: "Product not found", }; await progressService.logError(entry); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain( "❌ **Failed Product** (gid://shopify/Product/123)" ); expect(content).not.toContain("Variant:"); expect(content).toContain("Error: Product not found"); }); test("should handle complex error messages", async () => { await progressService.logOperationStart({ targetTag: "test", priceAdjustmentPercentage: 10, }); const entry = { productId: "gid://shopify/Product/123", productTitle: "Complex Error Product", variantId: "gid://shopify/ProductVariant/456", errorMessage: "GraphQL error: Field 'price' of type 'Money!' must not be null", }; await progressService.logError(entry); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain( "Error: GraphQL error: Field 'price' of type 'Money!' must not be null" ); }); }); describe("logCompletionSummary", () => { test("should log completion summary with all statistics", async () => { await progressService.logOperationStart({ targetTag: "test", priceAdjustmentPercentage: 10, }); const startTime = new Date(Date.now() - 5000); // 5 seconds ago const summary = { totalProducts: 10, successfulUpdates: 8, failedUpdates: 2, startTime: startTime, }; await progressService.logCompletionSummary(summary); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("**Summary:**"); expect(content).toContain("Total Products Processed: 10"); expect(content).toContain("Successful Updates: 8"); expect(content).toContain("Failed Updates: 2"); expect(content).toContain("Duration: 5 seconds"); expect(content).toContain("Completed:"); expect(content).toContain("---"); }); test("should handle summary without start time", async () => { await progressService.logOperationStart({ targetTag: "test", priceAdjustmentPercentage: 10, }); const summary = { totalProducts: 5, successfulUpdates: 5, failedUpdates: 0, }; await progressService.logCompletionSummary(summary); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("Duration: Unknown seconds"); }); test("should handle zero statistics", async () => { await progressService.logOperationStart({ targetTag: "test", priceAdjustmentPercentage: 10, }); const summary = { totalProducts: 0, successfulUpdates: 0, failedUpdates: 0, startTime: new Date(), }; await progressService.logCompletionSummary(summary); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("Total Products Processed: 0"); expect(content).toContain("Successful Updates: 0"); expect(content).toContain("Failed Updates: 0"); }); }); describe("logRollbackSummary", () => { test("should log rollback completion summary with all statistics", async () => { await progressService.logRollbackStart({ targetTag: "rollback-test", }); const startTime = new Date(Date.now() - 8000); // 8 seconds ago const summary = { totalProducts: 5, totalVariants: 8, eligibleVariants: 6, successfulRollbacks: 5, failedRollbacks: 1, skippedVariants: 2, startTime: startTime, }; await progressService.logRollbackSummary(summary); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("**Rollback Summary:**"); expect(content).toContain("Total Products Processed: 5"); expect(content).toContain("Total Variants Processed: 8"); expect(content).toContain("Eligible Variants: 6"); expect(content).toContain("Successful Rollbacks: 5"); expect(content).toContain("Failed Rollbacks: 1"); expect(content).toContain("Skipped Variants: 2 (no compare-at price)"); expect(content).toContain("Duration: 8 seconds"); expect(content).toContain("Completed:"); expect(content).toContain("---"); }); test("should handle rollback summary without start time", async () => { await progressService.logRollbackStart({ targetTag: "test", }); const summary = { totalProducts: 3, totalVariants: 5, eligibleVariants: 5, successfulRollbacks: 5, failedRollbacks: 0, skippedVariants: 0, }; await progressService.logRollbackSummary(summary); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("Duration: Unknown seconds"); }); test("should distinguish rollback summary from update summary", async () => { await progressService.logRollbackStart({ targetTag: "test", }); const updateSummary = { totalProducts: 5, successfulUpdates: 4, failedUpdates: 1, startTime: new Date(Date.now() - 5000), }; const rollbackSummary = { totalProducts: 3, totalVariants: 6, eligibleVariants: 4, successfulRollbacks: 3, failedRollbacks: 1, skippedVariants: 2, startTime: new Date(Date.now() - 3000), }; await progressService.logCompletionSummary(updateSummary); await progressService.logRollbackSummary(rollbackSummary); const content = await fs.readFile(testFilePath, "utf8"); // Should contain both summary types expect(content).toContain("**Summary:**"); expect(content).toContain("Successful Updates: 4"); expect(content).toContain("**Rollback Summary:**"); expect(content).toContain("Successful Rollbacks: 3"); expect(content).toContain("Skipped Variants: 2 (no compare-at price)"); }); test("should handle zero rollback statistics", async () => { await progressService.logRollbackStart({ targetTag: "test", }); const summary = { totalProducts: 0, totalVariants: 0, eligibleVariants: 0, successfulRollbacks: 0, failedRollbacks: 0, skippedVariants: 0, startTime: new Date(), }; await progressService.logRollbackSummary(summary); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("Total Products Processed: 0"); expect(content).toContain("Total Variants Processed: 0"); expect(content).toContain("Eligible Variants: 0"); expect(content).toContain("Successful Rollbacks: 0"); expect(content).toContain("Failed Rollbacks: 0"); expect(content).toContain("Skipped Variants: 0"); }); }); describe("categorizeError", () => { test("should categorize rate limiting errors", () => { const testCases = [ "Rate limit exceeded", "HTTP 429 Too Many Requests", "Request was throttled", ]; testCases.forEach((errorMessage) => { expect(progressService.categorizeError(errorMessage)).toBe( "Rate Limiting" ); }); }); test("should categorize network errors", () => { const testCases = [ "Network connection failed", "Connection timeout", "Network error occurred", ]; testCases.forEach((errorMessage) => { expect(progressService.categorizeError(errorMessage)).toBe( "Network Issues" ); }); }); test("should categorize authentication errors", () => { const testCases = [ "Authentication failed", "HTTP 401 Unauthorized", "Invalid authentication credentials", ]; testCases.forEach((errorMessage) => { expect(progressService.categorizeError(errorMessage)).toBe( "Authentication" ); }); }); test("should categorize permission errors", () => { const testCases = [ "Permission denied", "HTTP 403 Forbidden", "Insufficient permissions", ]; testCases.forEach((errorMessage) => { expect(progressService.categorizeError(errorMessage)).toBe( "Permissions" ); }); }); test("should categorize not found errors", () => { const testCases = [ "Product not found", "HTTP 404 Not Found", "Resource not found", ]; testCases.forEach((errorMessage) => { expect(progressService.categorizeError(errorMessage)).toBe( "Resource Not Found" ); }); }); test("should categorize validation errors", () => { const testCases = [ "Validation error: Invalid price", "Invalid product data", "Price validation failed", ]; testCases.forEach((errorMessage) => { expect(progressService.categorizeError(errorMessage)).toBe( "Data Validation" ); }); }); test("should categorize server errors", () => { const testCases = [ "Internal server error", "HTTP 500 Server Error", "HTTP 502 Bad Gateway", "HTTP 503 Service Unavailable", ]; testCases.forEach((errorMessage) => { expect(progressService.categorizeError(errorMessage)).toBe( "Server Errors" ); }); }); test("should categorize Shopify API errors", () => { const testCases = [ "Shopify API error occurred", "Shopify API request failed", ]; testCases.forEach((errorMessage) => { expect(progressService.categorizeError(errorMessage)).toBe( "Shopify API" ); }); }); test("should categorize unknown errors as Other", () => { const testCases = [ "Something went wrong", "Unexpected error", "Random failure message", ]; testCases.forEach((errorMessage) => { expect(progressService.categorizeError(errorMessage)).toBe("Other"); }); }); test("should handle case insensitive categorization", () => { expect(progressService.categorizeError("RATE LIMIT EXCEEDED")).toBe( "Rate Limiting" ); expect(progressService.categorizeError("Network Connection Failed")).toBe( "Network Issues" ); expect(progressService.categorizeError("AUTHENTICATION FAILED")).toBe( "Authentication" ); }); }); describe("createProgressEntry", () => { test("should create progress entry with timestamp", () => { const data = { productId: "gid://shopify/Product/123", productTitle: "Test Product", status: "success", }; const entry = progressService.createProgressEntry(data); expect(entry).toHaveProperty("timestamp"); expect(entry.timestamp).toBeInstanceOf(Date); expect(entry.productId).toBe("gid://shopify/Product/123"); expect(entry.productTitle).toBe("Test Product"); expect(entry.status).toBe("success"); }); test("should preserve all original data", () => { const data = { productId: "gid://shopify/Product/456", productTitle: "Another Product", variantId: "gid://shopify/ProductVariant/789", oldPrice: 10.0, newPrice: 11.0, errorMessage: "Some error", }; const entry = progressService.createProgressEntry(data); expect(entry.productId).toBe(data.productId); expect(entry.productTitle).toBe(data.productTitle); expect(entry.variantId).toBe(data.variantId); expect(entry.oldPrice).toBe(data.oldPrice); expect(entry.newPrice).toBe(data.newPrice); expect(entry.errorMessage).toBe(data.errorMessage); }); }); describe("appendToProgressFile", () => { test("should create file with header when file doesn't exist", async () => { await progressService.appendToProgressFile("Test content"); const content = await fs.readFile(testFilePath, "utf8"); expect(content).toContain("# Shopify Price Update Progress Log"); expect(content).toContain( "This file tracks the progress of price update operations." ); expect(content).toContain("Test content"); }); test("should append to existing file without adding header", async () => { // Create file first await progressService.appendToProgressFile("First content"); // Append more content await progressService.appendToProgressFile("Second content"); const content = await fs.readFile(testFilePath, "utf8"); // Should only have one header const headerCount = ( content.match(/# Shopify Price Update Progress Log/g) || [] ).length; expect(headerCount).toBe(1); expect(content).toContain("First content"); expect(content).toContain("Second content"); }); test("should handle file write errors gracefully", async () => { // Mock fs.appendFile to throw an error const originalAppendFile = fs.appendFile; const mockAppendFile = jest .fn() .mockRejectedValue(new Error("Permission denied")); fs.appendFile = mockAppendFile; // Should not throw an error, but should log warnings const consoleSpy = jest .spyOn(console, "warn") .mockImplementation(() => {}); await progressService.appendToProgressFile("Test content"); expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining("Warning: Failed to write to progress file") ); consoleSpy.mockRestore(); fs.appendFile = originalAppendFile; }); }); });