const ProductService = require("../../src/services/product"); const ShopifyService = require("../../src/services/shopify"); const Logger = require("../../src/utils/logger"); // Mock dependencies jest.mock("../../src/services/shopify"); jest.mock("../../src/utils/logger"); describe("ProductService Integration Tests", () => { let productService; let mockShopifyService; let mockLogger; beforeEach(() => { // Create mock instances mockShopifyService = { executeQuery: jest.fn(), executeMutation: jest.fn(), executeWithRetry: jest.fn(), }; mockLogger = { info: jest.fn(), warning: jest.fn(), error: jest.fn(), logProductUpdate: jest.fn(), logRollbackUpdate: jest.fn(), logProductError: jest.fn(), }; // Mock constructors ShopifyService.mockImplementation(() => mockShopifyService); Logger.mockImplementation(() => mockLogger); productService = new ProductService(); }); afterEach(() => { jest.clearAllMocks(); }); describe("GraphQL Query Generation", () => { test("should generate correct products by tag query", () => { const query = productService.getProductsByTagQuery(); expect(query).toContain("query getProductsByTag"); expect(query).toContain("$query: String!"); expect(query).toContain("$first: Int!"); expect(query).toContain("$after: String"); expect(query).toContain( "products(first: $first, after: $after, query: $query)" ); expect(query).toContain("variants(first: 100)"); expect(query).toContain("pageInfo"); expect(query).toContain("hasNextPage"); expect(query).toContain("endCursor"); }); test("should generate correct product variant update mutation", () => { const mutation = productService.getProductVariantUpdateMutation(); expect(mutation).toContain("mutation productVariantsBulkUpdate"); expect(mutation).toContain("$productId: ID!"); expect(mutation).toContain("$variants: [ProductVariantsBulkInput!]!"); expect(mutation).toContain( "productVariantsBulkUpdate(productId: $productId, variants: $variants)" ); expect(mutation).toContain("productVariant"); expect(mutation).toContain("userErrors"); expect(mutation).toContain("field"); expect(mutation).toContain("message"); }); }); describe("Product Fetching with Pagination", () => { test("should fetch products with single page response", async () => { const mockResponse = { products: { edges: [ { node: { id: "gid://shopify/Product/123", title: "Test Product 1", tags: ["test-tag", "sale"], variants: { edges: [ { node: { id: "gid://shopify/ProductVariant/456", price: "29.99", compareAtPrice: null, title: "Default Title", }, }, ], }, }, }, ], pageInfo: { hasNextPage: false, endCursor: null, }, }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const products = await productService.fetchProductsByTag("test-tag"); expect(products).toHaveLength(1); expect(products[0]).toEqual({ id: "gid://shopify/Product/123", title: "Test Product 1", tags: ["test-tag", "sale"], variants: [ { id: "gid://shopify/ProductVariant/456", price: 29.99, compareAtPrice: null, title: "Default Title", }, ], }); expect(mockShopifyService.executeWithRetry).toHaveBeenCalledTimes(1); expect(mockLogger.info).toHaveBeenCalledWith( "Starting to fetch products with tag: test-tag" ); }); test("should handle multi-page responses with pagination", async () => { const firstPageResponse = { products: { edges: [ { node: { id: "gid://shopify/Product/123", title: "Product 1", tags: ["test-tag"], variants: { edges: [ { node: { id: "gid://shopify/ProductVariant/456", price: "19.99", compareAtPrice: "24.99", title: "Variant 1", }, }, ], }, }, }, ], pageInfo: { hasNextPage: true, endCursor: "eyJsYXN0X2lkIjoxMjN9", }, }, }; const secondPageResponse = { products: { edges: [ { node: { id: "gid://shopify/Product/789", title: "Product 2", tags: ["test-tag"], variants: { edges: [ { node: { id: "gid://shopify/ProductVariant/101112", price: "39.99", compareAtPrice: null, title: "Variant 2", }, }, ], }, }, }, ], pageInfo: { hasNextPage: false, endCursor: null, }, }, }; mockShopifyService.executeWithRetry .mockResolvedValueOnce(firstPageResponse) .mockResolvedValueOnce(secondPageResponse); const products = await productService.fetchProductsByTag("test-tag"); expect(products).toHaveLength(2); expect(products[0].id).toBe("gid://shopify/Product/123"); expect(products[1].id).toBe("gid://shopify/Product/789"); expect(mockShopifyService.executeWithRetry).toHaveBeenCalledTimes(2); // Check that pagination variables were passed correctly const firstCall = mockShopifyService.executeWithRetry.mock.calls[0][0]; const secondCall = mockShopifyService.executeWithRetry.mock.calls[1][0]; // Execute the functions to check the variables await firstCall(); await secondCall(); expect(mockShopifyService.executeQuery).toHaveBeenNthCalledWith( 1, expect.any(String), { query: "tag:test-tag", first: 50, after: null, } ); expect(mockShopifyService.executeQuery).toHaveBeenNthCalledWith( 2, expect.any(String), { query: "tag:test-tag", first: 50, after: "eyJsYXN0X2lkIjoxMjN9", } ); }); test("should handle empty product response", async () => { const mockResponse = { products: { edges: [], pageInfo: { hasNextPage: false, endCursor: null, }, }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const products = await productService.fetchProductsByTag( "nonexistent-tag" ); expect(products).toHaveLength(0); expect(mockLogger.info).toHaveBeenCalledWith( "Successfully fetched 0 products with tag: nonexistent-tag" ); }); test("should handle API errors during product fetching", async () => { const apiError = new Error("GraphQL API error: Invalid query"); mockShopifyService.executeWithRetry.mockRejectedValue(apiError); await expect( productService.fetchProductsByTag("test-tag") ).rejects.toThrow( "Product fetching failed: GraphQL API error: Invalid query" ); expect(mockLogger.error).toHaveBeenCalledWith( "Failed to fetch products with tag test-tag: GraphQL API error: Invalid query" ); }); test("should handle invalid response structure", async () => { const invalidResponse = { // Missing products field data: { shop: { name: "Test Shop" }, }, }; mockShopifyService.executeWithRetry.mockResolvedValue(invalidResponse); await expect( productService.fetchProductsByTag("test-tag") ).rejects.toThrow( "Product fetching failed: Invalid response structure: missing products field" ); }); }); describe("Product Validation", () => { test("should validate products with valid data", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Valid Product 1", variants: [ { id: "gid://shopify/ProductVariant/456", price: 29.99, title: "Variant 1", }, { id: "gid://shopify/ProductVariant/789", price: 39.99, title: "Variant 2", }, ], }, { id: "gid://shopify/Product/456", title: "Valid Product 2", variants: [ { id: "gid://shopify/ProductVariant/101112", price: 19.99, title: "Single Variant", }, ], }, ]; const validProducts = await productService.validateProducts(products); expect(validProducts).toHaveLength(2); expect(validProducts[0].variants).toHaveLength(2); expect(validProducts[1].variants).toHaveLength(1); expect(mockLogger.info).toHaveBeenCalledWith( "Validated 2 products for price updates" ); }); test("should skip products without variants", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product Without Variants", variants: [], }, { id: "gid://shopify/Product/456", title: "Product With Variants", variants: [ { id: "gid://shopify/ProductVariant/789", price: 29.99, title: "Valid Variant", }, ], }, ]; const validProducts = await productService.validateProducts(products); expect(validProducts).toHaveLength(1); expect(validProducts[0].title).toBe("Product With Variants"); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping product "Product Without Variants" - no variants found' ); }); test("should skip variants with invalid prices", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product With Mixed Variants", variants: [ { id: "gid://shopify/ProductVariant/456", price: 29.99, title: "Valid Variant", }, { id: "gid://shopify/ProductVariant/789", price: "invalid", title: "Invalid Price Variant", }, { id: "gid://shopify/ProductVariant/101112", price: -10.0, title: "Negative Price Variant", }, { id: "gid://shopify/ProductVariant/131415", price: NaN, title: "NaN Price Variant", }, ], }, ]; const validProducts = await productService.validateProducts(products); expect(validProducts).toHaveLength(1); expect(validProducts[0].variants).toHaveLength(1); expect(validProducts[0].variants[0].title).toBe("Valid Variant"); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "Invalid Price Variant" in product "Product With Mixed Variants" - invalid price: invalid' ); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "Negative Price Variant" in product "Product With Mixed Variants" - negative price: -10' ); }); test("should skip products with no valid variants", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product With All Invalid Variants", variants: [ { id: "gid://shopify/ProductVariant/456", price: "invalid", title: "Invalid Variant 1", }, { id: "gid://shopify/ProductVariant/789", price: -5.0, title: "Invalid Variant 2", }, ], }, ]; const validProducts = await productService.validateProducts(products); expect(validProducts).toHaveLength(0); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping product "Product With All Invalid Variants" - no variants with valid prices' ); }); }); describe("Product Summary Statistics", () => { test("should calculate correct summary for products", () => { const products = [ { variants: [{ price: 10.0 }, { price: 20.0 }], }, { variants: [{ price: 5.0 }, { price: 50.0 }, { price: 30.0 }], }, ]; const summary = productService.getProductSummary(products); expect(summary).toEqual({ totalProducts: 2, totalVariants: 5, priceRange: { min: 5.0, max: 50.0, }, }); }); test("should handle empty product list", () => { const products = []; const summary = productService.getProductSummary(products); expect(summary).toEqual({ totalProducts: 0, totalVariants: 0, priceRange: { min: 0, max: 0, }, }); }); test("should handle single product with single variant", () => { const products = [ { variants: [{ price: 25.99 }], }, ]; const summary = productService.getProductSummary(products); expect(summary).toEqual({ totalProducts: 1, totalVariants: 1, priceRange: { min: 25.99, max: 25.99, }, }); }); }); describe("Single Variant Price Updates", () => { test("should update variant price successfully", async () => { const mockResponse = { productVariantsBulkUpdate: { productVariants: [ { id: "gid://shopify/ProductVariant/123", price: "32.99", compareAtPrice: "29.99", }, ], userErrors: [], }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const variant = { id: "gid://shopify/ProductVariant/123", price: 29.99, }; const result = await productService.updateVariantPrice( variant, "gid://shopify/Product/123", 32.99, 29.99 ); expect(result.success).toBe(true); expect(result.updatedVariant.id).toBe("gid://shopify/ProductVariant/123"); expect(result.updatedVariant.price).toBe("32.99"); expect(mockShopifyService.executeWithRetry).toHaveBeenCalledWith( expect.any(Function), mockLogger ); }); test("should handle Shopify API user errors", async () => { const mockResponse = { productVariantsBulkUpdate: { productVariants: [], userErrors: [ { field: "price", message: "Price must be greater than 0", }, { field: "compareAtPrice", message: "Compare at price must be greater than price", }, ], }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const variant = { id: "gid://shopify/ProductVariant/123", price: 0, }; const result = await productService.updateVariantPrice( variant, "gid://shopify/Product/123", 0, 0 ); expect(result.success).toBe(false); expect(result.error).toContain("Shopify API errors:"); expect(result.error).toContain("price: Price must be greater than 0"); expect(result.error).toContain( "compareAtPrice: Compare at price must be greater than price" ); }); test("should handle network errors during variant update", async () => { const networkError = new Error("Network connection failed"); mockShopifyService.executeWithRetry.mockRejectedValue(networkError); const variant = { id: "gid://shopify/ProductVariant/123", price: 29.99, }; const result = await productService.updateVariantPrice( variant, "gid://shopify/Product/123", 32.99, 29.99 ); expect(result.success).toBe(false); expect(result.error).toBe("Network connection failed"); }); }); describe("Batch Product Price Updates", () => { test("should update multiple products successfully", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product 1", variants: [ { id: "gid://shopify/ProductVariant/456", price: 20.0, }, ], }, { id: "gid://shopify/Product/789", title: "Product 2", variants: [ { id: "gid://shopify/ProductVariant/101112", price: 30.0, }, { id: "gid://shopify/ProductVariant/131415", price: 40.0, }, ], }, ]; // Mock successful responses for all variants mockShopifyService.executeWithRetry.mockResolvedValue({ productVariantsBulkUpdate: { productVariants: [ { id: "test", price: "22.00", compareAtPrice: "20.00" }, ], userErrors: [], }, }); // Mock delay function jest.spyOn(productService, "delay").mockResolvedValue(); const results = await productService.updateProductPrices(products, 10); expect(results.totalProducts).toBe(2); expect(results.totalVariants).toBe(3); expect(results.successfulUpdates).toBe(3); expect(results.failedUpdates).toBe(0); expect(results.errors).toHaveLength(0); expect(mockLogger.logProductUpdate).toHaveBeenCalledTimes(3); expect(mockShopifyService.executeWithRetry).toHaveBeenCalledTimes(3); }); test("should handle mixed success and failure scenarios", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product 1", variants: [ { id: "gid://shopify/ProductVariant/456", price: 20.0, }, { id: "gid://shopify/ProductVariant/789", price: 30.0, }, ], }, ]; // Mock first call succeeds, second fails mockShopifyService.executeWithRetry .mockResolvedValueOnce({ productVariantsBulkUpdate: { productVariants: [ { id: "gid://shopify/ProductVariant/456", price: "22.00", compareAtPrice: "20.00", }, ], userErrors: [], }, }) .mockResolvedValueOnce({ productVariantsBulkUpdate: { productVariants: [], userErrors: [ { field: "price", message: "Invalid price format", }, ], }, }); jest.spyOn(productService, "delay").mockResolvedValue(); const results = await productService.updateProductPrices(products, 10); expect(results.totalProducts).toBe(1); expect(results.totalVariants).toBe(2); expect(results.successfulUpdates).toBe(1); expect(results.failedUpdates).toBe(1); expect(results.errors).toHaveLength(1); expect(results.errors[0]).toEqual({ productId: "gid://shopify/Product/123", productTitle: "Product 1", variantId: "gid://shopify/ProductVariant/789", errorMessage: "Shopify API errors: price: Invalid price format", }); expect(mockLogger.logProductUpdate).toHaveBeenCalledTimes(1); expect(mockLogger.logProductError).toHaveBeenCalledTimes(1); }); test("should handle price calculation errors", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product 1", variants: [ { id: "gid://shopify/ProductVariant/456", price: "invalid", // This will cause calculateNewPrice to throw }, ], }, ]; // Mock calculateNewPrice to throw an error const { calculateNewPrice } = require("../../src/utils/price"); jest.mock("../../src/utils/price"); require("../../src/utils/price").calculateNewPrice = jest .fn() .mockImplementation(() => { throw new Error("Invalid price format"); }); jest.spyOn(productService, "delay").mockResolvedValue(); const results = await productService.updateProductPrices(products, 10); expect(results.totalProducts).toBe(1); expect(results.totalVariants).toBe(1); expect(results.successfulUpdates).toBe(0); expect(results.failedUpdates).toBe(1); expect(results.errors).toHaveLength(1); expect(results.errors[0].errorMessage).toContain( "Price calculation failed" ); }); test("should process products in batches with delays", async () => { // Create products that exceed batch size const products = Array.from({ length: 25 }, (_, i) => ({ id: `gid://shopify/Product/${i}`, title: `Product ${i}`, variants: [ { id: `gid://shopify/ProductVariant/${i}`, price: 10.0 + i, }, ], })); mockShopifyService.executeWithRetry.mockResolvedValue({ productVariantUpdate: { productVariant: { id: "test", price: "11.00" }, userErrors: [], }, }); const delaySpy = jest.spyOn(productService, "delay").mockResolvedValue(); await productService.updateProductPrices(products, 10); // Should have delays between batches (batch size is 10, so 3 batches total) // Delays should be called 2 times (between batch 1-2 and 2-3) expect(delaySpy).toHaveBeenCalledTimes(2); expect(delaySpy).toHaveBeenCalledWith(500); }); }); describe("Error Scenarios", () => { test("should handle executeWithRetry failures gracefully", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product 1", variants: [ { id: "gid://shopify/ProductVariant/456", price: 20.0, }, ], }, ]; const retryError = new Error("Max retries exceeded"); retryError.errorHistory = [ { attempt: 1, error: "Rate limit", retryable: true }, { attempt: 2, error: "Rate limit", retryable: true }, { attempt: 3, error: "Rate limit", retryable: true }, ]; mockShopifyService.executeWithRetry.mockRejectedValue(retryError); jest.spyOn(productService, "delay").mockResolvedValue(); const results = await productService.updateProductPrices(products, 10); expect(results.successfulUpdates).toBe(0); expect(results.failedUpdates).toBe(1); expect(results.errors[0].errorMessage).toContain("Max retries exceeded"); }); test("should continue processing after individual failures", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product 1", variants: [ { id: "gid://shopify/ProductVariant/456", price: 20.0, }, { id: "gid://shopify/ProductVariant/789", price: 30.0, }, ], }, ]; // First call fails, second succeeds mockShopifyService.executeWithRetry .mockRejectedValueOnce(new Error("Network timeout")) .mockResolvedValueOnce({ productVariantsBulkUpdate: { productVariants: [ { id: "gid://shopify/ProductVariant/789", price: "33.00", compareAtPrice: "30.00", }, ], userErrors: [], }, }); jest.spyOn(productService, "delay").mockResolvedValue(); const results = await productService.updateProductPrices(products, 10); expect(results.successfulUpdates).toBe(1); expect(results.failedUpdates).toBe(1); expect(mockLogger.logProductUpdate).toHaveBeenCalledTimes(1); expect(mockLogger.logProductError).toHaveBeenCalledTimes(1); }); }); describe("Rollback Validation", () => { test("should validate products with compare-at prices for rollback", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product With Compare-At Prices", variants: [ { id: "gid://shopify/ProductVariant/456", price: 19.99, compareAtPrice: 24.99, title: "Variant 1", }, { id: "gid://shopify/ProductVariant/789", price: 29.99, compareAtPrice: 39.99, title: "Variant 2", }, ], }, { id: "gid://shopify/Product/456", title: "Another Product", variants: [ { id: "gid://shopify/ProductVariant/101112", price: 15.99, compareAtPrice: 19.99, title: "Single Variant", }, ], }, ]; const eligibleProducts = await productService.validateProductsForRollback( products ); expect(eligibleProducts).toHaveLength(2); expect(eligibleProducts[0].variants).toHaveLength(2); expect(eligibleProducts[1].variants).toHaveLength(1); expect(mockLogger.info).toHaveBeenCalledWith( "Starting rollback validation for 2 products" ); expect(mockLogger.info).toHaveBeenCalledWith( "Rollback validation completed: 2 products eligible (3/3 variants eligible)" ); }); test("should skip products without variants for rollback", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product Without Variants", variants: [], }, { id: "gid://shopify/Product/456", title: "Product With Variants", variants: [ { id: "gid://shopify/ProductVariant/789", price: 29.99, compareAtPrice: 39.99, title: "Valid Variant", }, ], }, ]; const eligibleProducts = await productService.validateProductsForRollback( products ); expect(eligibleProducts).toHaveLength(1); expect(eligibleProducts[0].title).toBe("Product With Variants"); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping product "Product Without Variants" for rollback - no variants found' ); expect(mockLogger.warning).toHaveBeenCalledWith( "Skipped 1 products during rollback validation" ); }); test("should skip variants without compare-at prices", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product With Mixed Variants", variants: [ { id: "gid://shopify/ProductVariant/456", price: 29.99, compareAtPrice: 39.99, title: "Valid Variant", }, { id: "gid://shopify/ProductVariant/789", price: 19.99, compareAtPrice: null, title: "No Compare-At Price", }, { id: "gid://shopify/ProductVariant/101112", price: 24.99, compareAtPrice: undefined, title: "Undefined Compare-At Price", }, ], }, ]; const eligibleProducts = await productService.validateProductsForRollback( products ); expect(eligibleProducts).toHaveLength(1); expect(eligibleProducts[0].variants).toHaveLength(1); expect(eligibleProducts[0].variants[0].title).toBe("Valid Variant"); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "No Compare-At Price" in product "Product With Mixed Variants" for rollback - No compare-at price available' ); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "Undefined Compare-At Price" in product "Product With Mixed Variants" for rollback - No compare-at price available' ); }); test("should skip variants with invalid compare-at prices", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product With Invalid Compare-At Prices", variants: [ { id: "gid://shopify/ProductVariant/456", price: 29.99, compareAtPrice: 39.99, title: "Valid Variant", }, { id: "gid://shopify/ProductVariant/789", price: 19.99, compareAtPrice: 0, title: "Zero Compare-At Price", }, { id: "gid://shopify/ProductVariant/101112", price: 24.99, compareAtPrice: -5.99, title: "Negative Compare-At Price", }, { id: "gid://shopify/ProductVariant/131415", price: 14.99, compareAtPrice: "invalid", title: "Invalid Compare-At Price", }, ], }, ]; const eligibleProducts = await productService.validateProductsForRollback( products ); expect(eligibleProducts).toHaveLength(1); expect(eligibleProducts[0].variants).toHaveLength(1); expect(eligibleProducts[0].variants[0].title).toBe("Valid Variant"); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "Zero Compare-At Price" in product "Product With Invalid Compare-At Prices" for rollback - Compare-at price must be greater than zero' ); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "Negative Compare-At Price" in product "Product With Invalid Compare-At Prices" for rollback - Compare-at price must be greater than zero' ); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "Invalid Compare-At Price" in product "Product With Invalid Compare-At Prices" for rollback - Invalid compare-at price' ); }); test("should skip variants where current price equals compare-at price", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product With Same Prices", variants: [ { id: "gid://shopify/ProductVariant/456", price: 29.99, compareAtPrice: 39.99, title: "Valid Variant", }, { id: "gid://shopify/ProductVariant/789", price: 24.99, compareAtPrice: 24.99, title: "Same Price Variant", }, { id: "gid://shopify/ProductVariant/101112", price: 19.995, compareAtPrice: 19.99, title: "Nearly Same Price Variant", }, ], }, ]; const eligibleProducts = await productService.validateProductsForRollback( products ); expect(eligibleProducts).toHaveLength(1); expect(eligibleProducts[0].variants).toHaveLength(1); expect(eligibleProducts[0].variants[0].title).toBe("Valid Variant"); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "Same Price Variant" in product "Product With Same Prices" for rollback - Compare-at price is the same as current price' ); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "Nearly Same Price Variant" in product "Product With Same Prices" for rollback - Compare-at price is the same as current price' ); }); test("should skip variants with invalid current prices", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product With Invalid Current Prices", variants: [ { id: "gid://shopify/ProductVariant/456", price: 29.99, compareAtPrice: 39.99, title: "Valid Variant", }, { id: "gid://shopify/ProductVariant/789", price: "invalid", compareAtPrice: 24.99, title: "Invalid Current Price", }, { id: "gid://shopify/ProductVariant/101112", price: -10.0, compareAtPrice: 19.99, title: "Negative Current Price", }, ], }, ]; const eligibleProducts = await productService.validateProductsForRollback( products ); expect(eligibleProducts).toHaveLength(1); expect(eligibleProducts[0].variants).toHaveLength(1); expect(eligibleProducts[0].variants[0].title).toBe("Valid Variant"); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "Invalid Current Price" in product "Product With Invalid Current Prices" for rollback - Invalid current price' ); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "Negative Current Price" in product "Product With Invalid Current Prices" for rollback - Invalid current price' ); }); test("should skip products with no eligible variants", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product With No Eligible Variants", variants: [ { id: "gid://shopify/ProductVariant/456", price: 29.99, compareAtPrice: null, title: "No Compare-At Price", }, { id: "gid://shopify/ProductVariant/789", price: 19.99, compareAtPrice: 19.99, title: "Same Price", }, ], }, ]; const eligibleProducts = await productService.validateProductsForRollback( products ); expect(eligibleProducts).toHaveLength(0); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping product "Product With No Eligible Variants" for rollback - no variants with valid compare-at prices' ); }); test("should handle empty products array", async () => { const products = []; const eligibleProducts = await productService.validateProductsForRollback( products ); expect(eligibleProducts).toHaveLength(0); expect(mockLogger.info).toHaveBeenCalledWith( "Starting rollback validation for 0 products" ); expect(mockLogger.info).toHaveBeenCalledWith( "Rollback validation completed: 0 products eligible (0/0 variants eligible)" ); }); }); describe("Rollback Variant Price Updates", () => { test("should rollback variant price successfully", async () => { const mockResponse = { productVariantsBulkUpdate: { productVariants: [ { id: "gid://shopify/ProductVariant/123", price: "75.00", compareAtPrice: null, }, ], userErrors: [], }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const variant = { id: "gid://shopify/ProductVariant/123", price: 50.0, compareAtPrice: 75.0, title: "Test Variant", }; const result = await productService.rollbackVariantPrice( variant, "gid://shopify/Product/123" ); expect(result.success).toBe(true); expect(result.rollbackDetails.oldPrice).toBe(50.0); expect(result.rollbackDetails.compareAtPrice).toBe(75.0); expect(result.rollbackDetails.newPrice).toBe(75.0); expect(result.updatedVariant.price).toBe("75.00"); expect(result.updatedVariant.compareAtPrice).toBe(null); expect(mockShopifyService.executeWithRetry).toHaveBeenCalledWith( expect.any(Function), mockLogger ); }); test("should handle rollback validation failure", async () => { const variant = { id: "gid://shopify/ProductVariant/123", price: 50.0, compareAtPrice: null, // No compare-at price title: "Invalid Variant", }; const result = await productService.rollbackVariantPrice( variant, "gid://shopify/Product/123" ); expect(result.success).toBe(false); expect(result.error).toBe( "Rollback not eligible: No compare-at price available" ); expect(result.errorType).toBe("validation"); expect(result.retryable).toBe(false); expect(result.rollbackDetails.oldPrice).toBe(50.0); expect(result.rollbackDetails.newPrice).toBe(null); // Should not call Shopify API for invalid variants expect(mockShopifyService.executeWithRetry).not.toHaveBeenCalled(); }); test("should handle Shopify API user errors during rollback", async () => { const mockResponse = { productVariantsBulkUpdate: { productVariants: [], userErrors: [ { field: "price", message: "Price cannot be null", }, ], }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const variant = { id: "gid://shopify/ProductVariant/123", price: 50.0, compareAtPrice: 75.0, title: "Test Variant", }; const result = await productService.rollbackVariantPrice( variant, "gid://shopify/Product/123" ); expect(result.success).toBe(false); expect(result.error).toContain("Shopify API errors:"); expect(result.error).toContain("price: Price cannot be null"); }); test("should handle network errors during rollback", async () => { const networkError = new Error("Network connection failed"); networkError.errorHistory = [ { attempt: 1, error: "Timeout", retryable: true }, { attempt: 2, error: "Connection refused", retryable: true }, ]; mockShopifyService.executeWithRetry.mockRejectedValue(networkError); const variant = { id: "gid://shopify/ProductVariant/123", price: 50.0, compareAtPrice: 75.0, title: "Test Variant", }; const result = await productService.rollbackVariantPrice( variant, "gid://shopify/Product/123" ); expect(result.success).toBe(false); expect(result.error).toBe("Network connection failed"); expect(result.errorHistory).toEqual(networkError.errorHistory); }); }); describe("Batch Rollback Operations", () => { test("should rollback multiple products successfully", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product 1", variants: [ { id: "gid://shopify/ProductVariant/456", price: 20.0, compareAtPrice: 25.0, title: "Variant 1", }, ], }, { id: "gid://shopify/Product/789", title: "Product 2", variants: [ { id: "gid://shopify/ProductVariant/101112", price: 30.0, compareAtPrice: 40.0, title: "Variant 2", }, { id: "gid://shopify/ProductVariant/131415", price: 15.0, compareAtPrice: 20.0, title: "Variant 3", }, ], }, ]; // Mock successful responses for all variants mockShopifyService.executeWithRetry.mockResolvedValue({ productVariantsBulkUpdate: { productVariants: [ { id: "test", price: "25.00", compareAtPrice: null }, ], userErrors: [], }, }); // Mock delay function jest.spyOn(productService, "delay").mockResolvedValue(); const results = await productService.rollbackProductPrices(products); expect(results.totalProducts).toBe(2); expect(results.totalVariants).toBe(3); expect(results.eligibleVariants).toBe(3); expect(results.successfulRollbacks).toBe(3); expect(results.failedRollbacks).toBe(0); expect(results.skippedVariants).toBe(0); expect(results.errors).toHaveLength(0); expect(mockLogger.logRollbackUpdate).toHaveBeenCalledTimes(3); expect(mockShopifyService.executeWithRetry).toHaveBeenCalledTimes(3); }); test("should handle mixed success and failure scenarios in rollback", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product 1", variants: [ { id: "gid://shopify/ProductVariant/456", price: 20.0, compareAtPrice: 25.0, title: "Success Variant", }, { id: "gid://shopify/ProductVariant/789", price: 30.0, compareAtPrice: 40.0, title: "Failure Variant", }, ], }, ]; // Mock first call succeeds, second fails mockShopifyService.executeWithRetry .mockResolvedValueOnce({ productVariantsBulkUpdate: { productVariants: [ { id: "gid://shopify/ProductVariant/456", price: "25.00", compareAtPrice: null, }, ], userErrors: [], }, }) .mockResolvedValueOnce({ productVariantsBulkUpdate: { productVariants: [], userErrors: [ { field: "price", message: "Invalid price format", }, ], }, }); jest.spyOn(productService, "delay").mockResolvedValue(); const results = await productService.rollbackProductPrices(products); expect(results.totalProducts).toBe(1); expect(results.totalVariants).toBe(2); expect(results.successfulRollbacks).toBe(1); expect(results.failedRollbacks).toBe(1); expect(results.errors).toHaveLength(1); expect(results.errors[0]).toEqual( expect.objectContaining({ productId: "gid://shopify/Product/123", productTitle: "Product 1", variantId: "gid://shopify/ProductVariant/789", errorMessage: "Shopify API errors: price: Invalid price format", }) ); expect(mockLogger.logRollbackUpdate).toHaveBeenCalledTimes(1); expect(mockLogger.logProductError).toHaveBeenCalledTimes(1); }); test("should handle variants that are skipped due to validation", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product 1", variants: [ { id: "gid://shopify/ProductVariant/456", price: 20.0, compareAtPrice: 25.0, title: "Valid Variant", }, { id: "gid://shopify/ProductVariant/789", price: 30.0, compareAtPrice: null, // Will be skipped title: "Invalid Variant", }, ], }, ]; // Mock successful response for valid variant mockShopifyService.executeWithRetry.mockResolvedValue({ productVariantsBulkUpdate: { productVariants: [ { id: "gid://shopify/ProductVariant/456", price: "25.00", compareAtPrice: null, }, ], userErrors: [], }, }); jest.spyOn(productService, "delay").mockResolvedValue(); const results = await productService.rollbackProductPrices(products); expect(results.totalProducts).toBe(1); expect(results.totalVariants).toBe(2); expect(results.successfulRollbacks).toBe(1); expect(results.failedRollbacks).toBe(0); expect(results.skippedVariants).toBe(1); expect(mockLogger.logRollbackUpdate).toHaveBeenCalledTimes(1); expect(mockLogger.warning).toHaveBeenCalledWith( expect.stringContaining("Skipped variant") ); // Only one API call should be made (for the valid variant) expect(mockShopifyService.executeWithRetry).toHaveBeenCalledTimes(1); }); test("should handle consecutive errors and stop processing", async () => { // Create products that will all fail const products = Array.from({ length: 10 }, (_, i) => ({ id: `gid://shopify/Product/${i}`, title: `Product ${i}`, variants: [ { id: `gid://shopify/ProductVariant/${i}`, price: 10.0 + i, compareAtPrice: 15.0 + i, title: `Variant ${i}`, }, ], })); // Mock all calls to fail mockShopifyService.executeWithRetry.mockRejectedValue( new Error("Persistent API error") ); jest.spyOn(productService, "delay").mockResolvedValue(); const results = await productService.rollbackProductPrices(products); // Should stop after 5 consecutive errors expect(results.failedRollbacks).toBeLessThanOrEqual(5); expect(results.errors.length).toBeGreaterThan(0); // Should log about stopping due to consecutive errors expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining("consecutive errors") ); }); test("should process products in batches with delays", async () => { // Create products that exceed batch size const products = Array.from({ length: 25 }, (_, i) => ({ id: `gid://shopify/Product/${i}`, title: `Product ${i}`, variants: [ { id: `gid://shopify/ProductVariant/${i}`, price: 10.0 + i, compareAtPrice: 15.0 + i, title: `Variant ${i}`, }, ], })); mockShopifyService.executeWithRetry.mockResolvedValue({ productVariantsBulkUpdate: { productVariants: [ { id: "test", price: "15.00", compareAtPrice: null }, ], userErrors: [], }, }); const delaySpy = jest.spyOn(productService, "delay").mockResolvedValue(); await productService.rollbackProductPrices(products); // Should have delays between batches (batch size is 10, so 3 batches total) // Delays should be called 2 times (between batch 1-2 and 2-3) expect(delaySpy).toHaveBeenCalledTimes(2); expect(delaySpy).toHaveBeenCalledWith(500); }); test("should handle empty products array for rollback", async () => { const products = []; const results = await productService.rollbackProductPrices(products); expect(results).toEqual({ totalProducts: 0, totalVariants: 0, eligibleVariants: 0, successfulRollbacks: 0, failedRollbacks: 0, skippedVariants: 0, errors: [], }); expect(mockShopifyService.executeWithRetry).not.toHaveBeenCalled(); }); }); describe("Error Analysis and Categorization", () => { test("should categorize different types of rollback errors", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Test Product", variants: [ { id: "gid://shopify/ProductVariant/456", price: 20.0, compareAtPrice: 25.0, title: "Test Variant", }, ], }, ]; // Mock different types of errors const rateLimitError = new Error("Rate limit exceeded"); rateLimitError.errorHistory = [ { attempt: 1, error: "Rate limit", retryable: true }, { attempt: 2, error: "Rate limit", retryable: true }, ]; mockShopifyService.executeWithRetry.mockRejectedValue(rateLimitError); jest.spyOn(productService, "delay").mockResolvedValue(); const results = await productService.rollbackProductPrices(products); expect(results.errors).toHaveLength(1); expect(results.errors[0].errorMessage).toContain("Rate limit exceeded"); expect(results.errors[0].errorHistory).toBeDefined(); }); }); describe("Progress Logging for Rollback", () => { test("should log rollback progress correctly", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Test Product", variants: [ { id: "gid://shopify/ProductVariant/456", price: 20.0, compareAtPrice: 25.0, title: "Test Variant", }, ], }, ]; mockShopifyService.executeWithRetry.mockResolvedValue({ productVariantsBulkUpdate: { productVariants: [ { id: "gid://shopify/ProductVariant/456", price: "25.00", compareAtPrice: null, }, ], userErrors: [], }, }); jest.spyOn(productService, "delay").mockResolvedValue(); await productService.rollbackProductPrices(products); expect(mockLogger.logRollbackUpdate).toHaveBeenCalledWith({ productId: "gid://shopify/Product/123", productTitle: "Test Product", variantId: "gid://shopify/ProductVariant/456", oldPrice: 20.0, newPrice: 25.0, compareAtPrice: 25.0, }); }); }); describe("Error Handling Edge Cases", () => { test("should handle product-level processing errors", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Test Product", variants: [ { id: "gid://shopify/ProductVariant/456", price: 20.0, compareAtPrice: 25.0, title: "Test Variant", }, ], }, ]; // Mock processProductForRollback to throw an error jest .spyOn(productService, "processProductForRollback") .mockRejectedValue(new Error("Product processing failed")); jest.spyOn(productService, "delay").mockResolvedValue(); const results = await productService.rollbackProductPrices(products); expect(results.errors).toHaveLength(1); expect(results.errors[0].errorMessage).toContain( "Product processing failed" ); expect(results.errors[0].errorType).toBe("product_processing_error"); }); test("should handle unexpected errors in variant processing", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Test Product", variants: [ { id: "gid://shopify/ProductVariant/456", price: 20.0, compareAtPrice: 25.0, title: "Test Variant", }, ], }, ]; // Mock rollbackVariantPrice to throw an unexpected error jest .spyOn(productService, "rollbackVariantPrice") .mockRejectedValue(new Error("Unexpected error")); jest.spyOn(productService, "delay").mockResolvedValue(); const results = await productService.rollbackProductPrices(products); expect(results.errors).toHaveLength(1); expect(results.errors[0].errorMessage).toContain( "Unexpected rollback error" ); }); }); describe("Existing Tests", () => { test("should skip variants with invalid current prices", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product With Invalid Current Prices", variants: [ { id: "gid://shopify/ProductVariant/456", price: 29.99, compareAtPrice: 39.99, title: "Valid Variant", }, { id: "gid://shopify/ProductVariant/789", price: "invalid", compareAtPrice: 24.99, title: "Invalid Current Price", }, { id: "gid://shopify/ProductVariant/101112", price: NaN, compareAtPrice: 19.99, title: "NaN Current Price", }, ], }, ]; const eligibleProducts = await productService.validateProductsForRollback( products ); expect(eligibleProducts).toHaveLength(1); expect(eligibleProducts[0].variants).toHaveLength(1); expect(eligibleProducts[0].variants[0].title).toBe("Valid Variant"); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "Invalid Current Price" in product "Product With Invalid Current Prices" for rollback - Invalid current price' ); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping variant "NaN Current Price" in product "Product With Invalid Current Prices" for rollback - Invalid current price' ); }); test("should skip products with no eligible variants", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Product With No Eligible Variants", variants: [ { id: "gid://shopify/ProductVariant/456", price: 29.99, compareAtPrice: null, title: "No Compare-At Price", }, { id: "gid://shopify/ProductVariant/789", price: 24.99, compareAtPrice: 24.99, title: "Same Price", }, ], }, { id: "gid://shopify/Product/456", title: "Product With Eligible Variants", variants: [ { id: "gid://shopify/ProductVariant/101112", price: 15.99, compareAtPrice: 19.99, title: "Valid Variant", }, ], }, ]; const eligibleProducts = await productService.validateProductsForRollback( products ); expect(eligibleProducts).toHaveLength(1); expect(eligibleProducts[0].title).toBe("Product With Eligible Variants"); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipping product "Product With No Eligible Variants" for rollback - no variants with valid compare-at prices' ); expect(mockLogger.warning).toHaveBeenCalledWith( "Skipped 1 products during rollback validation" ); }); test("should handle empty product list for rollback validation", async () => { const products = []; const eligibleProducts = await productService.validateProductsForRollback( products ); expect(eligibleProducts).toHaveLength(0); expect(mockLogger.info).toHaveBeenCalledWith( "Starting rollback validation for 0 products" ); expect(mockLogger.info).toHaveBeenCalledWith( "Rollback validation completed: 0 products eligible (0/0 variants eligible)" ); }); test("should provide detailed validation statistics", async () => { const products = [ { id: "gid://shopify/Product/123", title: "Mixed Product", variants: [ { id: "gid://shopify/ProductVariant/456", price: 29.99, compareAtPrice: 39.99, title: "Valid Variant 1", }, { id: "gid://shopify/ProductVariant/789", price: 19.99, compareAtPrice: 24.99, title: "Valid Variant 2", }, { id: "gid://shopify/ProductVariant/101112", price: 14.99, compareAtPrice: null, title: "Invalid Variant", }, ], }, ]; const eligibleProducts = await productService.validateProductsForRollback( products ); expect(eligibleProducts).toHaveLength(1); expect(eligibleProducts[0].variants).toHaveLength(2); expect(mockLogger.info).toHaveBeenCalledWith( "Rollback validation completed: 1 products eligible (2/3 variants eligible)" ); }); }); describe("Rollback Operations", () => { describe("rollbackVariantPrice", () => { test("should rollback variant price successfully", async () => { const variant = { id: "gid://shopify/ProductVariant/123", price: 15.99, compareAtPrice: 19.99, title: "Test Variant", }; const productId = "gid://shopify/Product/456"; const mockResponse = { productVariantsBulkUpdate: { productVariants: [ { id: variant.id, price: "19.99", compareAtPrice: null, }, ], userErrors: [], }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const result = await productService.rollbackVariantPrice( variant, productId ); expect(result.success).toBe(true); expect(result.rollbackDetails.oldPrice).toBe(15.99); expect(result.rollbackDetails.compareAtPrice).toBe(19.99); expect(result.rollbackDetails.newPrice).toBe(19.99); expect(mockShopifyService.executeWithRetry).toHaveBeenCalledTimes(1); }); test("should handle Shopify API errors during rollback", async () => { const variant = { id: "gid://shopify/ProductVariant/123", price: 15.99, compareAtPrice: 19.99, title: "Test Variant", }; const productId = "gid://shopify/Product/456"; const mockResponse = { productVariantsBulkUpdate: { productVariants: [], userErrors: [ { field: "price", message: "Price must be positive", }, ], }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const result = await productService.rollbackVariantPrice( variant, productId ); expect(result.success).toBe(false); expect(result.error).toContain("Shopify API errors"); expect(result.rollbackDetails.oldPrice).toBe(15.99); expect(result.rollbackDetails.newPrice).toBe(null); }); test("should handle network errors during rollback", async () => { const variant = { id: "gid://shopify/ProductVariant/123", price: 15.99, compareAtPrice: 19.99, title: "Test Variant", }; const productId = "gid://shopify/Product/456"; mockShopifyService.executeWithRetry.mockRejectedValue( new Error("Network timeout") ); const result = await productService.rollbackVariantPrice( variant, productId ); expect(result.success).toBe(false); expect(result.error).toBe("Network timeout"); }); test("should handle invalid variant for rollback", async () => { const variant = { id: "gid://shopify/ProductVariant/123", price: 15.99, compareAtPrice: null, // No compare-at price title: "Test Variant", }; const productId = "gid://shopify/Product/456"; const result = await productService.rollbackVariantPrice( variant, productId ); expect(result.success).toBe(false); expect(result.error).toContain("No compare-at price available"); }); }); describe("processProductForRollback", () => { test("should process product with successful rollbacks", async () => { const product = { id: "gid://shopify/Product/456", title: "Test Product", variants: [ { id: "gid://shopify/ProductVariant/123", price: 15.99, compareAtPrice: 19.99, title: "Variant 1", }, { id: "gid://shopify/ProductVariant/124", price: 25.99, compareAtPrice: 29.99, title: "Variant 2", }, ], }; const results = { totalVariants: 0, successfulRollbacks: 0, failedRollbacks: 0, errors: [], }; // Mock successful rollback responses const mockResponse = { productVariantsBulkUpdate: { productVariants: [ { id: "test", price: "19.99", compareAtPrice: null }, ], userErrors: [], }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); await productService.processProductForRollback(product, results); expect(results.totalVariants).toBe(2); expect(results.successfulRollbacks).toBe(2); expect(results.failedRollbacks).toBe(0); expect(results.errors).toHaveLength(0); expect(mockLogger.logRollbackUpdate).toHaveBeenCalledTimes(2); }); test("should handle mixed success and failure scenarios", async () => { const product = { id: "gid://shopify/Product/456", title: "Test Product", variants: [ { id: "gid://shopify/ProductVariant/123", price: 15.99, compareAtPrice: 19.99, title: "Valid Variant", }, { id: "gid://shopify/ProductVariant/124", price: 25.99, compareAtPrice: null, // Invalid for rollback title: "Invalid Variant", }, ], }; const results = { totalVariants: 0, successfulRollbacks: 0, failedRollbacks: 0, skippedVariants: 0, errors: [], }; // Mock first call succeeds, second variant will be skipped due to validation const successResponse = { productVariantsBulkUpdate: { productVariants: [ { id: "test", price: "19.99", compareAtPrice: null }, ], userErrors: [], }, }; mockShopifyService.executeWithRetry.mockResolvedValueOnce( successResponse ); await productService.processProductForRollback(product, results); expect(results.totalVariants).toBe(2); expect(results.successfulRollbacks).toBe(1); expect(results.failedRollbacks).toBe(0); expect(results.skippedVariants).toBe(1); expect(results.errors).toHaveLength(0); expect(mockLogger.logRollbackUpdate).toHaveBeenCalledTimes(1); expect(mockLogger.warning).toHaveBeenCalledWith( 'Skipped variant "Invalid Variant" in product "Test Product": Rollback not eligible: No compare-at price available' ); }); }); describe("rollbackProductPrices", () => { test("should rollback multiple products successfully", async () => { const products = [ { id: "gid://shopify/Product/456", title: "Product 1", variants: [ { id: "gid://shopify/ProductVariant/123", price: 15.99, compareAtPrice: 19.99, title: "Variant 1", }, ], }, { id: "gid://shopify/Product/789", title: "Product 2", variants: [ { id: "gid://shopify/ProductVariant/124", price: 25.99, compareAtPrice: 29.99, title: "Variant 2", }, ], }, ]; // Mock successful responses const mockResponse = { productVariantsBulkUpdate: { productVariants: [ { id: "test", price: "19.99", compareAtPrice: null }, ], userErrors: [], }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const results = await productService.rollbackProductPrices(products); expect(results.totalProducts).toBe(2); expect(results.totalVariants).toBe(2); expect(results.eligibleVariants).toBe(2); expect(results.successfulRollbacks).toBe(2); expect(results.failedRollbacks).toBe(0); expect(results.errors).toHaveLength(0); expect(mockLogger.info).toHaveBeenCalledWith( "Starting price rollback for 2 products" ); expect(mockLogger.info).toHaveBeenCalledWith( "Price rollback completed. Success: 2, Failed: 0, Skipped: 0, Success Rate: 100.0%" ); }); test("should handle empty product list", async () => { const products = []; const results = await productService.rollbackProductPrices(products); expect(results.totalProducts).toBe(0); expect(results.totalVariants).toBe(0); expect(results.eligibleVariants).toBe(0); expect(results.successfulRollbacks).toBe(0); expect(results.failedRollbacks).toBe(0); expect(mockShopifyService.executeWithRetry).not.toHaveBeenCalled(); }); test("should process products in batches", async () => { // Create more products than batch size (10) const products = Array.from({ length: 15 }, (_, i) => ({ id: `gid://shopify/Product/${i}`, title: `Product ${i}`, variants: [ { id: `gid://shopify/ProductVariant/${i}`, price: 15.99, compareAtPrice: 19.99, title: `Variant ${i}`, }, ], })); const mockResponse = { productVariantsBulkUpdate: { productVariants: [ { id: "test", price: "19.99", compareAtPrice: null }, ], userErrors: [], }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const results = await productService.rollbackProductPrices(products); expect(results.totalProducts).toBe(15); expect(results.successfulRollbacks).toBe(15); // Should log batch processing expect(mockLogger.info).toHaveBeenCalledWith( "Processing rollback batch 1 of 2 (10 products)" ); expect(mockLogger.info).toHaveBeenCalledWith( "Processing rollback batch 2 of 2 (5 products)" ); }); }); }); });