798 lines
20 KiB
JavaScript
798 lines
20 KiB
JavaScript
/**
|
|
* End-to-End Integration Tests for Rollback Workflow
|
|
* These tests verify the complete rollback functionality works together
|
|
*/
|
|
|
|
const ShopifyPriceUpdater = require("../../src/index");
|
|
const { getConfig } = require("../../src/config/environment");
|
|
const ProductService = require("../../src/services/product");
|
|
const Logger = require("../../src/utils/logger");
|
|
const ProgressService = require("../../src/services/progress");
|
|
|
|
// Mock external dependencies but test internal integration
|
|
jest.mock("../../src/config/environment");
|
|
jest.mock("../../src/services/shopify");
|
|
jest.mock("../../src/services/progress");
|
|
|
|
describe("Rollback Workflow Integration Tests", () => {
|
|
let mockConfig;
|
|
let mockShopifyService;
|
|
let mockProgressService;
|
|
|
|
beforeEach(() => {
|
|
// Mock configuration for rollback mode
|
|
mockConfig = {
|
|
shopDomain: "test-shop.myshopify.com",
|
|
accessToken: "test-token",
|
|
targetTag: "rollback-test",
|
|
priceAdjustmentPercentage: 10, // Not used in rollback but required
|
|
operationMode: "rollback",
|
|
};
|
|
|
|
// Mock Shopify service responses
|
|
mockShopifyService = {
|
|
testConnection: jest.fn().mockResolvedValue(true),
|
|
executeQuery: jest.fn(),
|
|
executeMutation: jest.fn(),
|
|
executeWithRetry: jest.fn(),
|
|
};
|
|
|
|
// Mock progress service
|
|
mockProgressService = {
|
|
logRollbackStart: jest.fn(),
|
|
logRollbackUpdate: jest.fn(),
|
|
logRollbackSummary: jest.fn(),
|
|
logError: jest.fn(),
|
|
logErrorAnalysis: jest.fn(),
|
|
};
|
|
|
|
getConfig.mockReturnValue(mockConfig);
|
|
ProgressService.mockImplementation(() => mockProgressService);
|
|
|
|
// Mock ShopifyService constructor
|
|
const ShopifyService = require("../../src/services/shopify");
|
|
ShopifyService.mockImplementation(() => mockShopifyService);
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe("Complete Rollback Workflow", () => {
|
|
test("should execute complete rollback workflow with successful operations", async () => {
|
|
// Mock product fetching response
|
|
const mockProductsResponse = {
|
|
products: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/Product/123",
|
|
title: "Test Product 1",
|
|
tags: ["rollback-test"],
|
|
variants: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/456",
|
|
price: "50.00",
|
|
compareAtPrice: "75.00",
|
|
title: "Variant 1",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
node: {
|
|
id: "gid://shopify/Product/789",
|
|
title: "Test Product 2",
|
|
tags: ["rollback-test"],
|
|
variants: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/101112",
|
|
price: "30.00",
|
|
compareAtPrice: "40.00",
|
|
title: "Variant 2",
|
|
},
|
|
},
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/131415",
|
|
price: "20.00",
|
|
compareAtPrice: "25.00",
|
|
title: "Variant 3",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
pageInfo: {
|
|
hasNextPage: false,
|
|
endCursor: null,
|
|
},
|
|
},
|
|
};
|
|
|
|
// Mock successful rollback mutation responses
|
|
const mockRollbackResponse = {
|
|
productVariantsBulkUpdate: {
|
|
productVariants: [
|
|
{
|
|
id: "test-variant",
|
|
price: "75.00",
|
|
compareAtPrice: null,
|
|
},
|
|
],
|
|
userErrors: [],
|
|
},
|
|
};
|
|
|
|
mockShopifyService.executeWithRetry
|
|
.mockResolvedValueOnce(mockProductsResponse) // Product fetching
|
|
.mockResolvedValue(mockRollbackResponse); // All rollback operations
|
|
|
|
const app = new ShopifyPriceUpdater();
|
|
const exitCode = await app.run();
|
|
|
|
// Verify successful completion
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Verify rollback start was logged
|
|
expect(mockProgressService.logRollbackStart).toHaveBeenCalledWith(
|
|
mockConfig
|
|
);
|
|
|
|
// Verify rollback operations were logged (3 variants)
|
|
expect(mockProgressService.logRollbackUpdate).toHaveBeenCalledTimes(3);
|
|
|
|
// Verify rollback summary was logged
|
|
expect(mockProgressService.logRollbackSummary).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
totalProducts: 2,
|
|
totalVariants: 3,
|
|
eligibleVariants: 3,
|
|
successfulRollbacks: 3,
|
|
failedRollbacks: 0,
|
|
skippedVariants: 0,
|
|
})
|
|
);
|
|
|
|
// Verify Shopify API calls
|
|
expect(mockShopifyService.executeWithRetry).toHaveBeenCalledTimes(4); // 1 fetch + 3 rollbacks
|
|
});
|
|
|
|
test("should handle mixed success and failure scenarios", async () => {
|
|
// Mock product fetching response with mixed variants
|
|
const mockProductsResponse = {
|
|
products: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/Product/123",
|
|
title: "Mixed Product",
|
|
tags: ["rollback-test"],
|
|
variants: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/456",
|
|
price: "50.00",
|
|
compareAtPrice: "75.00",
|
|
title: "Valid Variant",
|
|
},
|
|
},
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/789",
|
|
price: "30.00",
|
|
compareAtPrice: null, // Will be skipped
|
|
title: "No Compare-At Price",
|
|
},
|
|
},
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/101112",
|
|
price: "20.00",
|
|
compareAtPrice: "25.00",
|
|
title: "Will Fail",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
pageInfo: {
|
|
hasNextPage: false,
|
|
endCursor: null,
|
|
},
|
|
},
|
|
};
|
|
|
|
mockShopifyService.executeWithRetry
|
|
.mockResolvedValueOnce(mockProductsResponse) // Product fetching
|
|
.mockResolvedValueOnce({
|
|
// First rollback succeeds
|
|
productVariantsBulkUpdate: {
|
|
productVariants: [
|
|
{
|
|
id: "gid://shopify/ProductVariant/456",
|
|
price: "75.00",
|
|
compareAtPrice: null,
|
|
},
|
|
],
|
|
userErrors: [],
|
|
},
|
|
})
|
|
.mockResolvedValueOnce({
|
|
// Second rollback fails
|
|
productVariantsBulkUpdate: {
|
|
productVariants: [],
|
|
userErrors: [
|
|
{
|
|
field: "price",
|
|
message: "Invalid price format",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const app = new ShopifyPriceUpdater();
|
|
const exitCode = await app.run();
|
|
|
|
// Should still complete but with partial success (50% success rate = moderate)
|
|
expect(exitCode).toBe(1); // Moderate success rate
|
|
|
|
// Verify rollback summary reflects mixed results
|
|
expect(mockProgressService.logRollbackSummary).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
totalProducts: 1,
|
|
totalVariants: 2, // Only eligible variants are processed
|
|
eligibleVariants: 2, // Only 2 variants were eligible
|
|
successfulRollbacks: 1,
|
|
failedRollbacks: 1,
|
|
skippedVariants: 0, // Skipped variants are filtered out during validation
|
|
})
|
|
);
|
|
|
|
// Verify error logging
|
|
expect(mockProgressService.logError).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
productId: "gid://shopify/Product/123",
|
|
productTitle: "Mixed Product",
|
|
variantId: "gid://shopify/ProductVariant/101112",
|
|
errorMessage: "Shopify API errors: price: Invalid price format",
|
|
})
|
|
);
|
|
});
|
|
|
|
test("should handle products with no eligible variants", async () => {
|
|
// Mock product fetching response with no eligible variants
|
|
const mockProductsResponse = {
|
|
products: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/Product/123",
|
|
title: "No Eligible Variants",
|
|
tags: ["rollback-test"],
|
|
variants: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/456",
|
|
price: "50.00",
|
|
compareAtPrice: null, // No compare-at price
|
|
title: "Variant 1",
|
|
},
|
|
},
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/789",
|
|
price: "30.00",
|
|
compareAtPrice: "30.00", // Same as current price
|
|
title: "Variant 2",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
pageInfo: {
|
|
hasNextPage: false,
|
|
endCursor: null,
|
|
},
|
|
},
|
|
};
|
|
|
|
mockShopifyService.executeWithRetry.mockResolvedValueOnce(
|
|
mockProductsResponse
|
|
);
|
|
|
|
const app = new ShopifyPriceUpdater();
|
|
const exitCode = await app.run();
|
|
|
|
// Should complete successfully with no operations
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Verify no rollback operations were attempted
|
|
expect(mockProgressService.logRollbackUpdate).not.toHaveBeenCalled();
|
|
|
|
// Verify summary reflects no eligible variants
|
|
expect(mockProgressService.logRollbackSummary).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
totalProducts: 0, // No products with eligible variants
|
|
totalVariants: 0,
|
|
eligibleVariants: 0,
|
|
successfulRollbacks: 0,
|
|
failedRollbacks: 0,
|
|
skippedVariants: 0,
|
|
})
|
|
);
|
|
});
|
|
|
|
test("should handle API connection failures", async () => {
|
|
mockShopifyService.testConnection.mockResolvedValue(false);
|
|
|
|
const app = new ShopifyPriceUpdater();
|
|
const exitCode = await app.run();
|
|
|
|
expect(exitCode).toBe(1);
|
|
|
|
// Should log critical failure
|
|
expect(mockProgressService.logRollbackSummary).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
totalProducts: 0,
|
|
errors: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
errorMessage: "API connection failed",
|
|
}),
|
|
]),
|
|
})
|
|
);
|
|
});
|
|
|
|
test("should handle product fetching failures", async () => {
|
|
mockShopifyService.executeWithRetry.mockRejectedValue(
|
|
new Error("GraphQL API error")
|
|
);
|
|
|
|
const app = new ShopifyPriceUpdater();
|
|
const exitCode = await app.run();
|
|
|
|
expect(exitCode).toBe(1);
|
|
|
|
// Should log critical failure
|
|
expect(mockProgressService.logRollbackSummary).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
totalProducts: 0,
|
|
errors: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
errorMessage: "Product fetching for rollback failed",
|
|
}),
|
|
]),
|
|
})
|
|
);
|
|
});
|
|
|
|
test("should handle rate limiting with retry logic", async () => {
|
|
// Mock product fetching response
|
|
const mockProductsResponse = {
|
|
products: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/Product/123",
|
|
title: "Test Product",
|
|
tags: ["rollback-test"],
|
|
variants: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/456",
|
|
price: "50.00",
|
|
compareAtPrice: "75.00",
|
|
title: "Variant 1",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
pageInfo: {
|
|
hasNextPage: false,
|
|
endCursor: null,
|
|
},
|
|
},
|
|
};
|
|
|
|
// Mock rate limit error followed by success
|
|
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
|
|
.mockResolvedValueOnce(mockProductsResponse) // Product fetching succeeds
|
|
.mockRejectedValueOnce(rateLimitError) // First rollback fails with rate limit
|
|
.mockResolvedValueOnce({
|
|
// Retry succeeds
|
|
productVariantsBulkUpdate: {
|
|
productVariants: [
|
|
{
|
|
id: "gid://shopify/ProductVariant/456",
|
|
price: "75.00",
|
|
compareAtPrice: null,
|
|
},
|
|
],
|
|
userErrors: [],
|
|
},
|
|
});
|
|
|
|
const app = new ShopifyPriceUpdater();
|
|
const exitCode = await app.run();
|
|
|
|
// Should fail due to rate limit error
|
|
expect(exitCode).toBe(2);
|
|
|
|
// Should not have successful rollback due to rate limit error
|
|
expect(mockProgressService.logRollbackUpdate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should handle large datasets with pagination", async () => {
|
|
// Mock first page response
|
|
const firstPageResponse = {
|
|
products: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/Product/1",
|
|
title: "Product 1",
|
|
tags: ["rollback-test"],
|
|
variants: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/1",
|
|
price: "10.00",
|
|
compareAtPrice: "15.00",
|
|
title: "Variant 1",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
pageInfo: {
|
|
hasNextPage: true,
|
|
endCursor: "cursor1",
|
|
},
|
|
},
|
|
};
|
|
|
|
// Mock second page response
|
|
const secondPageResponse = {
|
|
products: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/Product/2",
|
|
title: "Product 2",
|
|
tags: ["rollback-test"],
|
|
variants: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/2",
|
|
price: "20.00",
|
|
compareAtPrice: "25.00",
|
|
title: "Variant 2",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
pageInfo: {
|
|
hasNextPage: false,
|
|
endCursor: null,
|
|
},
|
|
},
|
|
};
|
|
|
|
// Mock successful rollback responses
|
|
const mockRollbackResponse = {
|
|
productVariantsBulkUpdate: {
|
|
productVariants: [
|
|
{
|
|
id: "test-variant",
|
|
price: "15.00",
|
|
compareAtPrice: null,
|
|
},
|
|
],
|
|
userErrors: [],
|
|
},
|
|
};
|
|
|
|
mockShopifyService.executeWithRetry
|
|
.mockResolvedValueOnce(firstPageResponse) // First page
|
|
.mockResolvedValueOnce(secondPageResponse) // Second page
|
|
.mockResolvedValue(mockRollbackResponse); // All rollback operations
|
|
|
|
const app = new ShopifyPriceUpdater();
|
|
const exitCode = await app.run();
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Verify both products were processed
|
|
expect(mockProgressService.logRollbackSummary).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
totalProducts: 2,
|
|
totalVariants: 2,
|
|
eligibleVariants: 2,
|
|
successfulRollbacks: 2,
|
|
failedRollbacks: 0,
|
|
skippedVariants: 0,
|
|
})
|
|
);
|
|
|
|
// Verify pagination calls + rollback calls
|
|
expect(mockShopifyService.executeWithRetry).toHaveBeenCalledTimes(4); // 2 pages + 2 rollbacks
|
|
});
|
|
});
|
|
|
|
describe("Rollback vs Update Mode Integration", () => {
|
|
test("should execute update workflow when operation mode is update", async () => {
|
|
// Change config to update mode
|
|
mockConfig.operationMode = "update";
|
|
getConfig.mockReturnValue(mockConfig);
|
|
|
|
// Mock product fetching and update responses
|
|
const mockProductsResponse = {
|
|
products: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/Product/123",
|
|
title: "Test Product",
|
|
tags: ["rollback-test"],
|
|
variants: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/456",
|
|
price: "50.00",
|
|
compareAtPrice: null,
|
|
title: "Variant 1",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
pageInfo: {
|
|
hasNextPage: false,
|
|
endCursor: null,
|
|
},
|
|
},
|
|
};
|
|
|
|
const mockUpdateResponse = {
|
|
productVariantsBulkUpdate: {
|
|
productVariants: [
|
|
{
|
|
id: "gid://shopify/ProductVariant/456",
|
|
price: "55.00",
|
|
compareAtPrice: "50.00",
|
|
},
|
|
],
|
|
userErrors: [],
|
|
},
|
|
};
|
|
|
|
mockShopifyService.executeWithRetry
|
|
.mockResolvedValueOnce(mockProductsResponse)
|
|
.mockResolvedValue(mockUpdateResponse);
|
|
|
|
// Mock progress service for update operations
|
|
mockProgressService.logOperationStart = jest.fn();
|
|
mockProgressService.logProductUpdate = jest.fn();
|
|
mockProgressService.logCompletionSummary = jest.fn();
|
|
|
|
const app = new ShopifyPriceUpdater();
|
|
const exitCode = await app.run();
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
// Verify update workflow was used, not rollback
|
|
expect(mockProgressService.logOperationStart).toHaveBeenCalledWith(
|
|
mockConfig
|
|
);
|
|
expect(mockProgressService.logRollbackStart).not.toHaveBeenCalled();
|
|
expect(mockProgressService.logProductUpdate).toHaveBeenCalled();
|
|
expect(mockProgressService.logRollbackUpdate).not.toHaveBeenCalled();
|
|
expect(mockProgressService.logCompletionSummary).toHaveBeenCalled();
|
|
expect(mockProgressService.logRollbackSummary).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Error Recovery and Resilience", () => {
|
|
test("should continue processing after individual variant failures", async () => {
|
|
const mockProductsResponse = {
|
|
products: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/Product/123",
|
|
title: "Test Product",
|
|
tags: ["rollback-test"],
|
|
variants: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/456",
|
|
price: "50.00",
|
|
compareAtPrice: "75.00",
|
|
title: "Success Variant",
|
|
},
|
|
},
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/789",
|
|
price: "30.00",
|
|
compareAtPrice: "40.00",
|
|
title: "Failure Variant",
|
|
},
|
|
},
|
|
{
|
|
node: {
|
|
id: "gid://shopify/ProductVariant/101112",
|
|
price: "20.00",
|
|
compareAtPrice: "25.00",
|
|
title: "Another Success Variant",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
pageInfo: {
|
|
hasNextPage: false,
|
|
endCursor: null,
|
|
},
|
|
},
|
|
};
|
|
|
|
mockShopifyService.executeWithRetry
|
|
.mockResolvedValueOnce(mockProductsResponse) // Product fetching
|
|
.mockResolvedValueOnce({
|
|
// First rollback succeeds
|
|
productVariantsBulkUpdate: {
|
|
productVariants: [
|
|
{
|
|
id: "gid://shopify/ProductVariant/456",
|
|
price: "75.00",
|
|
compareAtPrice: null,
|
|
},
|
|
],
|
|
userErrors: [],
|
|
},
|
|
})
|
|
.mockResolvedValueOnce({
|
|
// Second rollback fails
|
|
productVariantsBulkUpdate: {
|
|
productVariants: [],
|
|
userErrors: [
|
|
{
|
|
field: "price",
|
|
message: "Invalid price",
|
|
},
|
|
],
|
|
},
|
|
})
|
|
.mockResolvedValueOnce({
|
|
// Third rollback succeeds
|
|
productVariantsBulkUpdate: {
|
|
productVariants: [
|
|
{
|
|
id: "gid://shopify/ProductVariant/101112",
|
|
price: "25.00",
|
|
compareAtPrice: null,
|
|
},
|
|
],
|
|
userErrors: [],
|
|
},
|
|
});
|
|
|
|
const app = new ShopifyPriceUpdater();
|
|
const exitCode = await app.run();
|
|
|
|
expect(exitCode).toBe(1); // Moderate success rate (2/3 = 66.7%)
|
|
|
|
// Verify mixed results
|
|
expect(mockProgressService.logRollbackSummary).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
totalProducts: 1,
|
|
totalVariants: 3,
|
|
eligibleVariants: 3,
|
|
successfulRollbacks: 2,
|
|
failedRollbacks: 1,
|
|
skippedVariants: 0,
|
|
})
|
|
);
|
|
|
|
// Verify both successful operations were logged
|
|
expect(mockProgressService.logRollbackUpdate).toHaveBeenCalledTimes(2);
|
|
|
|
// Verify error was logged
|
|
expect(mockProgressService.logError).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
variantId: "gid://shopify/ProductVariant/789",
|
|
errorMessage: "Shopify API errors: price: Invalid price",
|
|
})
|
|
);
|
|
});
|
|
|
|
test("should stop processing after consecutive errors", async () => {
|
|
// Create products that will all fail
|
|
const mockProductsResponse = {
|
|
products: {
|
|
edges: Array.from({ length: 10 }, (_, i) => ({
|
|
node: {
|
|
id: `gid://shopify/Product/${i}`,
|
|
title: `Product ${i}`,
|
|
tags: ["rollback-test"],
|
|
variants: {
|
|
edges: [
|
|
{
|
|
node: {
|
|
id: `gid://shopify/ProductVariant/${i}`,
|
|
price: "50.00",
|
|
compareAtPrice: "75.00",
|
|
title: `Variant ${i}`,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
})),
|
|
pageInfo: {
|
|
hasNextPage: false,
|
|
endCursor: null,
|
|
},
|
|
},
|
|
};
|
|
|
|
// Mock all rollback operations to fail
|
|
mockShopifyService.executeWithRetry
|
|
.mockResolvedValueOnce(mockProductsResponse) // Product fetching succeeds
|
|
.mockRejectedValue(new Error("Persistent API error")); // All rollbacks fail
|
|
|
|
const app = new ShopifyPriceUpdater();
|
|
const exitCode = await app.run();
|
|
|
|
expect(exitCode).toBe(2); // Complete failure
|
|
|
|
// Should stop after 5 consecutive errors
|
|
const rollbackSummaryCall =
|
|
mockProgressService.logRollbackSummary.mock.calls[0][0];
|
|
expect(rollbackSummaryCall.failedRollbacks).toBeLessThanOrEqual(5);
|
|
|
|
// Should have logged multiple errors (5 individual errors, system error is added to results but not logged separately)
|
|
expect(mockProgressService.logError).toHaveBeenCalledTimes(5);
|
|
});
|
|
});
|
|
});
|