const ShopifyPriceUpdater = require("../src/index"); const { getConfig } = require("../src/config/environment"); const ProductService = require("../src/services/product"); const Logger = require("../src/utils/logger"); // Mock dependencies jest.mock("../src/config/environment"); jest.mock("../src/services/product"); jest.mock("../src/utils/logger"); describe("ShopifyPriceUpdater - Rollback Functionality", () => { let app; let mockConfig; let mockProductService; let mockLogger; beforeEach(() => { // Mock configuration mockConfig = { shopDomain: "test-shop.myshopify.com", accessToken: "test-token", targetTag: "test-tag", priceAdjustmentPercentage: 10, operationMode: "rollback", }; // Mock product service mockProductService = { shopifyService: { testConnection: jest.fn(), }, fetchProductsByTag: jest.fn(), validateProductsForRollback: jest.fn(), rollbackProductPrices: jest.fn(), getProductSummary: jest.fn(), }; // Mock logger mockLogger = { logRollbackStart: jest.fn(), logOperationStart: jest.fn(), logProductCount: jest.fn(), logRollbackSummary: jest.fn(), logCompletionSummary: jest.fn(), logErrorAnalysis: jest.fn(), info: jest.fn(), warning: jest.fn(), error: jest.fn(), }; // Mock constructors getConfig.mockReturnValue(mockConfig); ProductService.mockImplementation(() => mockProductService); Logger.mockImplementation(() => mockLogger); app = new ShopifyPriceUpdater(); }); afterEach(() => { jest.clearAllMocks(); }); describe("Rollback Mode Initialization", () => { test("should initialize with rollback configuration", async () => { const result = await app.initialize(); expect(result).toBe(true); expect(getConfig).toHaveBeenCalled(); expect(mockLogger.logRollbackStart).toHaveBeenCalledWith(mockConfig); expect(mockLogger.logOperationStart).not.toHaveBeenCalled(); }); test("should handle initialization failure", async () => { getConfig.mockImplementation(() => { throw new Error("Configuration error"); }); const result = await app.initialize(); expect(result).toBe(false); expect(mockLogger.error).toHaveBeenCalledWith( "Initialization failed: Configuration error" ); }); }); describe("Rollback Product Fetching and Validation", () => { test("should fetch and validate products for rollback", async () => { const mockProducts = [ { id: "gid://shopify/Product/123", title: "Test Product", variants: [ { id: "gid://shopify/ProductVariant/456", price: 50.0, compareAtPrice: 75.0, }, ], }, ]; const mockEligibleProducts = [mockProducts[0]]; const mockSummary = { totalProducts: 1, totalVariants: 1, priceRange: { min: 50, max: 50 }, }; mockProductService.fetchProductsByTag.mockResolvedValue(mockProducts); mockProductService.validateProductsForRollback.mockResolvedValue( mockEligibleProducts ); mockProductService.getProductSummary.mockReturnValue(mockSummary); // Initialize app first await app.initialize(); const result = await app.fetchAndValidateProductsForRollback(); expect(result).toEqual(mockEligibleProducts); expect(mockProductService.fetchProductsByTag).toHaveBeenCalledWith( "test-tag" ); expect( mockProductService.validateProductsForRollback ).toHaveBeenCalledWith(mockProducts); expect(mockLogger.logProductCount).toHaveBeenCalledWith(1); expect(mockLogger.info).toHaveBeenCalledWith("Rollback Product Summary:"); }); test("should handle empty product results", async () => { mockProductService.fetchProductsByTag.mockResolvedValue([]); // Initialize app first await app.initialize(); const result = await app.fetchAndValidateProductsForRollback(); expect(result).toEqual([]); expect(mockLogger.info).toHaveBeenCalledWith( "No products found with the specified tag. Operation completed." ); }); test("should handle product fetching errors", async () => { mockProductService.fetchProductsByTag.mockRejectedValue( new Error("API error") ); // Initialize app first await app.initialize(); const result = await app.fetchAndValidateProductsForRollback(); expect(result).toBe(null); expect(mockLogger.error).toHaveBeenCalledWith( "Failed to fetch products for rollback: API error" ); }); }); describe("Rollback Price Operations", () => { test("should execute rollback operations successfully", async () => { const mockProducts = [ { id: "gid://shopify/Product/123", title: "Test Product", variants: [ { id: "gid://shopify/ProductVariant/456", price: 50.0, compareAtPrice: 75.0, }, ], }, ]; const mockResults = { totalProducts: 1, totalVariants: 1, eligibleVariants: 1, successfulRollbacks: 1, failedRollbacks: 0, skippedVariants: 0, errors: [], }; mockProductService.rollbackProductPrices.mockResolvedValue(mockResults); const result = await app.rollbackPrices(mockProducts); expect(result).toEqual(mockResults); expect(mockProductService.rollbackProductPrices).toHaveBeenCalledWith( mockProducts ); expect(mockLogger.info).toHaveBeenCalledWith( "Starting price rollback operations" ); }); test("should handle empty products array", async () => { const result = await app.rollbackPrices([]); expect(result).toEqual({ totalProducts: 0, totalVariants: 0, eligibleVariants: 0, successfulRollbacks: 0, failedRollbacks: 0, skippedVariants: 0, errors: [], }); }); test("should handle rollback operation errors", async () => { const mockProducts = [ { id: "gid://shopify/Product/123", title: "Test Product", variants: [{ price: 50.0, compareAtPrice: 75.0 }], }, ]; mockProductService.rollbackProductPrices.mockRejectedValue( new Error("Rollback failed") ); const result = await app.rollbackPrices(mockProducts); expect(result).toBe(null); expect(mockLogger.error).toHaveBeenCalledWith( "Price rollback failed: Rollback failed" ); }); }); describe("Rollback Summary Display", () => { test("should display successful rollback summary", async () => { const mockResults = { totalProducts: 5, totalVariants: 8, eligibleVariants: 6, successfulRollbacks: 6, failedRollbacks: 0, skippedVariants: 2, errors: [], }; app.startTime = new Date(); const exitCode = await app.displayRollbackSummary(mockResults); expect(exitCode).toBe(0); expect(mockLogger.logRollbackSummary).toHaveBeenCalledWith( expect.objectContaining({ totalProducts: 5, totalVariants: 8, eligibleVariants: 6, successfulRollbacks: 6, failedRollbacks: 0, skippedVariants: 2, }) ); expect(mockLogger.info).toHaveBeenCalledWith( "🎉 All rollback operations completed successfully!" ); }); test("should display partial success rollback summary", async () => { const mockResults = { totalProducts: 5, totalVariants: 8, eligibleVariants: 6, successfulRollbacks: 5, failedRollbacks: 1, skippedVariants: 2, errors: [ { productId: "gid://shopify/Product/123", errorMessage: "Test error", }, ], }; app.startTime = new Date(); const exitCode = await app.displayRollbackSummary(mockResults); expect(exitCode).toBe(1); // Moderate success rate (83.3%) expect(mockLogger.logRollbackSummary).toHaveBeenCalled(); expect(mockLogger.logErrorAnalysis).toHaveBeenCalledWith( mockResults.errors, expect.any(Object) ); expect(mockLogger.warning).toHaveBeenCalledWith( expect.stringContaining("moderate success rate") ); }); test("should display moderate success rollback summary", async () => { const mockResults = { totalProducts: 5, totalVariants: 8, eligibleVariants: 6, successfulRollbacks: 3, failedRollbacks: 3, skippedVariants: 2, errors: [ { productId: "1", errorMessage: "Error 1" }, { productId: "2", errorMessage: "Error 2" }, { productId: "3", errorMessage: "Error 3" }, ], }; app.startTime = new Date(); const exitCode = await app.displayRollbackSummary(mockResults); expect(exitCode).toBe(1); // Moderate success rate (50%) expect(mockLogger.warning).toHaveBeenCalledWith( expect.stringContaining("moderate success rate") ); }); test("should display low success rollback summary", async () => { const mockResults = { totalProducts: 5, totalVariants: 8, eligibleVariants: 6, successfulRollbacks: 1, failedRollbacks: 5, skippedVariants: 2, errors: Array.from({ length: 5 }, (_, i) => ({ productId: `${i}`, errorMessage: `Error ${i}`, })), }; app.startTime = new Date(); const exitCode = await app.displayRollbackSummary(mockResults); expect(exitCode).toBe(2); // Low success rate (16.7%) expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining("low success rate") ); }); test("should display complete failure rollback summary", async () => { const mockResults = { totalProducts: 5, totalVariants: 8, eligibleVariants: 6, successfulRollbacks: 0, failedRollbacks: 6, skippedVariants: 2, errors: Array.from({ length: 6 }, (_, i) => ({ productId: `${i}`, errorMessage: `Error ${i}`, })), }; app.startTime = new Date(); const exitCode = await app.displayRollbackSummary(mockResults); expect(exitCode).toBe(2); expect(mockLogger.error).toHaveBeenCalledWith( "❌ All rollback operations failed. Please check your configuration and try again." ); }); }); describe("Operation Mode Header Display", () => { test("should display rollback mode header", async () => { const consoleSpy = jest.spyOn(console, "log").mockImplementation(); // Initialize app first await app.initialize(); await app.displayOperationModeHeader(); expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining("SHOPIFY PRICE ROLLBACK MODE") ); expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining( "Reverting prices from compare-at to main price" ) ); expect(mockLogger.info).toHaveBeenCalledWith("Operation Mode: ROLLBACK"); consoleSpy.mockRestore(); }); test("should display update mode header when not in rollback mode", async () => { mockConfig.operationMode = "update"; app.config = mockConfig; const consoleSpy = jest.spyOn(console, "log").mockImplementation(); await app.displayOperationModeHeader(); expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining("SHOPIFY PRICE UPDATE MODE") ); expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining("Adjusting prices by 10%") ); expect(mockLogger.info).toHaveBeenCalledWith("Operation Mode: UPDATE"); consoleSpy.mockRestore(); }); }); describe("Complete Rollback Workflow", () => { test("should execute complete rollback workflow successfully", async () => { // Mock successful initialization mockProductService.shopifyService.testConnection.mockResolvedValue(true); // Mock successful product fetching and validation const mockProducts = [ { id: "gid://shopify/Product/123", title: "Test Product", variants: [ { id: "gid://shopify/ProductVariant/456", price: 50.0, compareAtPrice: 75.0, }, ], }, ]; mockProductService.fetchProductsByTag.mockResolvedValue(mockProducts); mockProductService.validateProductsForRollback.mockResolvedValue( mockProducts ); mockProductService.getProductSummary.mockReturnValue({ totalProducts: 1, totalVariants: 1, priceRange: { min: 50, max: 50 }, }); // Mock successful rollback const mockResults = { totalProducts: 1, totalVariants: 1, eligibleVariants: 1, successfulRollbacks: 1, failedRollbacks: 0, skippedVariants: 0, errors: [], }; mockProductService.rollbackProductPrices.mockResolvedValue(mockResults); const exitCode = await app.run(); expect(exitCode).toBe(0); expect(mockLogger.logRollbackStart).toHaveBeenCalledWith(mockConfig); expect(mockProductService.fetchProductsByTag).toHaveBeenCalledWith( "test-tag" ); expect( mockProductService.validateProductsForRollback ).toHaveBeenCalledWith(mockProducts); expect(mockProductService.rollbackProductPrices).toHaveBeenCalledWith( mockProducts ); expect(mockLogger.logRollbackSummary).toHaveBeenCalled(); }); test("should handle rollback workflow with initialization failure", async () => { getConfig.mockImplementation(() => { throw new Error("Config error"); }); const exitCode = await app.run(); expect(exitCode).toBe(1); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining("Initialization failed") ); }); test("should handle rollback workflow with connection failure", async () => { mockProductService.shopifyService.testConnection.mockResolvedValue(false); const exitCode = await app.run(); expect(exitCode).toBe(1); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining("API connection failed") ); }); test("should handle rollback workflow with product fetching failure", async () => { mockProductService.shopifyService.testConnection.mockResolvedValue(true); mockProductService.fetchProductsByTag.mockRejectedValue( new Error("Fetch error") ); const exitCode = await app.run(); expect(exitCode).toBe(1); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining("Product fetching for rollback failed") ); }); test("should handle rollback workflow with rollback operation failure", async () => { mockProductService.shopifyService.testConnection.mockResolvedValue(true); mockProductService.fetchProductsByTag.mockResolvedValue([ { id: "gid://shopify/Product/123", variants: [{ price: 50, compareAtPrice: 75 }], }, ]); mockProductService.validateProductsForRollback.mockResolvedValue([ { id: "gid://shopify/Product/123", variants: [{ price: 50, compareAtPrice: 75 }], }, ]); mockProductService.getProductSummary.mockReturnValue({ totalProducts: 1, totalVariants: 1, priceRange: { min: 50, max: 50 }, }); mockProductService.rollbackProductPrices.mockRejectedValue( new Error("Rollback error") ); const exitCode = await app.run(); expect(exitCode).toBe(1); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining("Price rollback process failed") ); }); }); describe("Dual Operation Mode Support", () => { test("should route to update workflow when operation mode is update", async () => { mockConfig.operationMode = "update"; app.config = mockConfig; // Mock update-specific methods app.safeFetchAndValidateProducts = jest.fn().mockResolvedValue([]); app.safeUpdatePrices = jest.fn().mockResolvedValue({ totalProducts: 0, totalVariants: 0, successfulUpdates: 0, failedUpdates: 0, errors: [], }); app.displaySummaryAndGetExitCode = jest.fn().mockResolvedValue(0); mockProductService.shopifyService.testConnection.mockResolvedValue(true); const exitCode = await app.run(); expect(exitCode).toBe(0); expect(mockLogger.logOperationStart).toHaveBeenCalledWith(mockConfig); expect(mockLogger.logRollbackStart).not.toHaveBeenCalled(); expect(app.safeFetchAndValidateProducts).toHaveBeenCalled(); expect(app.safeUpdatePrices).toHaveBeenCalled(); expect(app.displaySummaryAndGetExitCode).toHaveBeenCalled(); }); test("should route to rollback workflow when operation mode is rollback", async () => { mockConfig.operationMode = "rollback"; app.config = mockConfig; mockProductService.shopifyService.testConnection.mockResolvedValue(true); mockProductService.fetchProductsByTag.mockResolvedValue([]); mockProductService.validateProductsForRollback.mockResolvedValue([]); mockProductService.getProductSummary.mockReturnValue({ totalProducts: 0, totalVariants: 0, priceRange: { min: 0, max: 0 }, }); const exitCode = await app.run(); expect(exitCode).toBe(0); expect(mockLogger.logRollbackStart).toHaveBeenCalledWith(mockConfig); expect(mockLogger.logOperationStart).not.toHaveBeenCalled(); }); }); describe("Error Handling and Recovery", () => { test("should handle unexpected errors gracefully", async () => { mockProductService.shopifyService.testConnection.mockResolvedValue(true); mockProductService.fetchProductsByTag.mockImplementation(() => { throw new Error("Unexpected error"); }); const exitCode = await app.run(); expect(exitCode).toBe(1); // Critical failure exit code expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining("Product fetching for rollback failed") ); }); test("should handle critical failures with proper logging", async () => { // Initialize app first await app.initialize(); const exitCode = await app.handleCriticalFailure("Test failure", 1); expect(exitCode).toBe(1); expect(mockLogger.error).toHaveBeenCalledWith( "Critical failure in rollback mode: Test failure" ); expect(mockLogger.logRollbackSummary).toHaveBeenCalledWith( expect.objectContaining({ totalProducts: 0, errors: expect.arrayContaining([ expect.objectContaining({ errorMessage: "Test failure", }), ]), }) ); }); test("should handle unexpected errors with partial results", async () => { const partialResults = { totalProducts: 2, totalVariants: 3, eligibleVariants: 2, successfulRollbacks: 1, failedRollbacks: 1, skippedVariants: 1, errors: [{ errorMessage: "Previous error" }], }; const error = new Error("Unexpected error"); error.stack = "Error stack trace"; // Initialize app first await app.initialize(); await app.handleUnexpectedError(error, partialResults); expect(mockLogger.error).toHaveBeenCalledWith( "Unexpected error occurred in rollback mode: Unexpected error" ); expect(mockLogger.logRollbackSummary).toHaveBeenCalledWith( expect.objectContaining({ totalProducts: 2, totalVariants: 3, eligibleVariants: 2, successfulRollbacks: 1, failedRollbacks: 1, skippedVariants: 1, }) ); }); }); describe("Backward Compatibility", () => { test("should default to update mode when operation mode is not specified", async () => { mockConfig.operationMode = "update"; app.config = mockConfig; // Mock update workflow methods app.safeFetchAndValidateProducts = jest.fn().mockResolvedValue([]); app.safeUpdatePrices = jest.fn().mockResolvedValue({ totalProducts: 0, totalVariants: 0, successfulUpdates: 0, failedUpdates: 0, errors: [], }); app.displaySummaryAndGetExitCode = jest.fn().mockResolvedValue(0); mockProductService.shopifyService.testConnection.mockResolvedValue(true); const exitCode = await app.run(); expect(exitCode).toBe(0); expect(mockLogger.logOperationStart).toHaveBeenCalledWith(mockConfig); expect(mockLogger.logRollbackStart).not.toHaveBeenCalled(); }); }); });