2195 lines
58 KiB
JavaScript
2195 lines
58 KiB
JavaScript
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)"
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|