Implemented Rollback Functionality
This commit is contained in:
@@ -13,6 +13,7 @@ describe("Environment Configuration", () => {
|
||||
delete process.env.SHOPIFY_ACCESS_TOKEN;
|
||||
delete process.env.TARGET_TAG;
|
||||
delete process.env.PRICE_ADJUSTMENT_PERCENTAGE;
|
||||
delete process.env.OPERATION_MODE;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@@ -35,6 +36,7 @@ describe("Environment Configuration", () => {
|
||||
accessToken: "shpat_1234567890abcdef",
|
||||
targetTag: "sale",
|
||||
priceAdjustmentPercentage: 10,
|
||||
operationMode: "update",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -247,5 +249,164 @@ describe("Environment Configuration", () => {
|
||||
expect(config.targetTag).toBe("sale-2024_special!");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Operation Mode", () => {
|
||||
test("should default to 'update' when OPERATION_MODE is not set", () => {
|
||||
process.env.SHOPIFY_SHOP_DOMAIN = "test-shop.myshopify.com";
|
||||
process.env.SHOPIFY_ACCESS_TOKEN = "shpat_1234567890abcdef";
|
||||
process.env.TARGET_TAG = "sale";
|
||||
process.env.PRICE_ADJUSTMENT_PERCENTAGE = "10";
|
||||
// OPERATION_MODE is not set
|
||||
|
||||
const config = loadEnvironmentConfig();
|
||||
|
||||
expect(config.operationMode).toBe("update");
|
||||
});
|
||||
|
||||
test("should accept 'update' operation mode", () => {
|
||||
process.env.SHOPIFY_SHOP_DOMAIN = "test-shop.myshopify.com";
|
||||
process.env.SHOPIFY_ACCESS_TOKEN = "shpat_1234567890abcdef";
|
||||
process.env.TARGET_TAG = "sale";
|
||||
process.env.PRICE_ADJUSTMENT_PERCENTAGE = "10";
|
||||
process.env.OPERATION_MODE = "update";
|
||||
|
||||
const config = loadEnvironmentConfig();
|
||||
|
||||
expect(config.operationMode).toBe("update");
|
||||
});
|
||||
|
||||
test("should accept 'rollback' operation mode", () => {
|
||||
process.env.SHOPIFY_SHOP_DOMAIN = "test-shop.myshopify.com";
|
||||
process.env.SHOPIFY_ACCESS_TOKEN = "shpat_1234567890abcdef";
|
||||
process.env.TARGET_TAG = "sale";
|
||||
process.env.PRICE_ADJUSTMENT_PERCENTAGE = "10";
|
||||
process.env.OPERATION_MODE = "rollback";
|
||||
|
||||
const config = loadEnvironmentConfig();
|
||||
|
||||
expect(config.operationMode).toBe("rollback");
|
||||
});
|
||||
|
||||
test("should throw error for invalid operation mode", () => {
|
||||
process.env.SHOPIFY_SHOP_DOMAIN = "test-shop.myshopify.com";
|
||||
process.env.SHOPIFY_ACCESS_TOKEN = "shpat_1234567890abcdef";
|
||||
process.env.TARGET_TAG = "sale";
|
||||
process.env.PRICE_ADJUSTMENT_PERCENTAGE = "10";
|
||||
process.env.OPERATION_MODE = "invalid";
|
||||
|
||||
expect(() => loadEnvironmentConfig()).toThrow(
|
||||
'Invalid OPERATION_MODE: "invalid". Must be either "update" or "rollback".'
|
||||
);
|
||||
});
|
||||
|
||||
test("should throw error for empty operation mode", () => {
|
||||
process.env.SHOPIFY_SHOP_DOMAIN = "test-shop.myshopify.com";
|
||||
process.env.SHOPIFY_ACCESS_TOKEN = "shpat_1234567890abcdef";
|
||||
process.env.TARGET_TAG = "sale";
|
||||
process.env.PRICE_ADJUSTMENT_PERCENTAGE = "10";
|
||||
process.env.OPERATION_MODE = "";
|
||||
|
||||
const config = loadEnvironmentConfig();
|
||||
|
||||
// Empty string should default to "update"
|
||||
expect(config.operationMode).toBe("update");
|
||||
});
|
||||
|
||||
test("should handle case sensitivity in operation mode", () => {
|
||||
process.env.SHOPIFY_SHOP_DOMAIN = "test-shop.myshopify.com";
|
||||
process.env.SHOPIFY_ACCESS_TOKEN = "shpat_1234567890abcdef";
|
||||
process.env.TARGET_TAG = "sale";
|
||||
process.env.PRICE_ADJUSTMENT_PERCENTAGE = "10";
|
||||
process.env.OPERATION_MODE = "UPDATE";
|
||||
|
||||
expect(() => loadEnvironmentConfig()).toThrow(
|
||||
'Invalid OPERATION_MODE: "UPDATE". Must be either "update" or "rollback".'
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle whitespace in operation mode", () => {
|
||||
process.env.SHOPIFY_SHOP_DOMAIN = "test-shop.myshopify.com";
|
||||
process.env.SHOPIFY_ACCESS_TOKEN = "shpat_1234567890abcdef";
|
||||
process.env.TARGET_TAG = "sale";
|
||||
process.env.PRICE_ADJUSTMENT_PERCENTAGE = "10";
|
||||
process.env.OPERATION_MODE = " rollback ";
|
||||
|
||||
expect(() => loadEnvironmentConfig()).toThrow(
|
||||
'Invalid OPERATION_MODE: " rollback ". Must be either "update" or "rollback".'
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle null operation mode", () => {
|
||||
process.env.SHOPIFY_SHOP_DOMAIN = "test-shop.myshopify.com";
|
||||
process.env.SHOPIFY_ACCESS_TOKEN = "shpat_1234567890abcdef";
|
||||
process.env.TARGET_TAG = "sale";
|
||||
process.env.PRICE_ADJUSTMENT_PERCENTAGE = "10";
|
||||
process.env.OPERATION_MODE = null;
|
||||
|
||||
const config = loadEnvironmentConfig();
|
||||
|
||||
// Null should default to "update"
|
||||
expect(config.operationMode).toBe("update");
|
||||
});
|
||||
|
||||
test("should handle undefined operation mode", () => {
|
||||
process.env.SHOPIFY_SHOP_DOMAIN = "test-shop.myshopify.com";
|
||||
process.env.SHOPIFY_ACCESS_TOKEN = "shpat_1234567890abcdef";
|
||||
process.env.TARGET_TAG = "sale";
|
||||
process.env.PRICE_ADJUSTMENT_PERCENTAGE = "10";
|
||||
process.env.OPERATION_MODE = undefined;
|
||||
|
||||
const config = loadEnvironmentConfig();
|
||||
|
||||
// Undefined should default to "update"
|
||||
expect(config.operationMode).toBe("update");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rollback Mode Specific Validation", () => {
|
||||
test("should validate rollback mode with all required variables", () => {
|
||||
process.env.SHOPIFY_SHOP_DOMAIN = "test-shop.myshopify.com";
|
||||
process.env.SHOPIFY_ACCESS_TOKEN = "shpat_1234567890abcdef";
|
||||
process.env.TARGET_TAG = "sale";
|
||||
process.env.PRICE_ADJUSTMENT_PERCENTAGE = "10";
|
||||
process.env.OPERATION_MODE = "rollback";
|
||||
|
||||
const config = loadEnvironmentConfig();
|
||||
|
||||
expect(config).toEqual({
|
||||
shopDomain: "test-shop.myshopify.com",
|
||||
accessToken: "shpat_1234567890abcdef",
|
||||
targetTag: "sale",
|
||||
priceAdjustmentPercentage: 10,
|
||||
operationMode: "rollback",
|
||||
});
|
||||
});
|
||||
|
||||
test("should validate rollback mode even with zero percentage", () => {
|
||||
process.env.SHOPIFY_SHOP_DOMAIN = "test-shop.myshopify.com";
|
||||
process.env.SHOPIFY_ACCESS_TOKEN = "shpat_1234567890abcdef";
|
||||
process.env.TARGET_TAG = "sale";
|
||||
process.env.PRICE_ADJUSTMENT_PERCENTAGE = "0";
|
||||
process.env.OPERATION_MODE = "rollback";
|
||||
|
||||
const config = loadEnvironmentConfig();
|
||||
|
||||
expect(config.operationMode).toBe("rollback");
|
||||
expect(config.priceAdjustmentPercentage).toBe(0);
|
||||
});
|
||||
|
||||
test("should validate rollback mode with negative percentage", () => {
|
||||
process.env.SHOPIFY_SHOP_DOMAIN = "test-shop.myshopify.com";
|
||||
process.env.SHOPIFY_ACCESS_TOKEN = "shpat_1234567890abcdef";
|
||||
process.env.TARGET_TAG = "sale";
|
||||
process.env.PRICE_ADJUSTMENT_PERCENTAGE = "-20";
|
||||
process.env.OPERATION_MODE = "rollback";
|
||||
|
||||
const config = loadEnvironmentConfig();
|
||||
|
||||
expect(config.operationMode).toBe("rollback");
|
||||
expect(config.priceAdjustmentPercentage).toBe(-20);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
686
tests/index.test.js
Normal file
686
tests/index.test.js
Normal file
@@ -0,0 +1,686 @@
|
||||
const ShopifyPriceUpdater = require("../src/index");
|
||||
const { getConfig } = require("../src/config/environment");
|
||||
const ProductService = require("../src/services/product");
|
||||
const Logger = require("../src/utils/logger");
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock("../src/config/environment");
|
||||
jest.mock("../src/services/product");
|
||||
jest.mock("../src/utils/logger");
|
||||
|
||||
describe("ShopifyPriceUpdater - Rollback Functionality", () => {
|
||||
let app;
|
||||
let mockConfig;
|
||||
let mockProductService;
|
||||
let mockLogger;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock configuration
|
||||
mockConfig = {
|
||||
shopDomain: "test-shop.myshopify.com",
|
||||
accessToken: "test-token",
|
||||
targetTag: "test-tag",
|
||||
priceAdjustmentPercentage: 10,
|
||||
operationMode: "rollback",
|
||||
};
|
||||
|
||||
// Mock product service
|
||||
mockProductService = {
|
||||
shopifyService: {
|
||||
testConnection: jest.fn(),
|
||||
},
|
||||
fetchProductsByTag: jest.fn(),
|
||||
validateProductsForRollback: jest.fn(),
|
||||
rollbackProductPrices: jest.fn(),
|
||||
getProductSummary: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock logger
|
||||
mockLogger = {
|
||||
logRollbackStart: jest.fn(),
|
||||
logOperationStart: jest.fn(),
|
||||
logProductCount: jest.fn(),
|
||||
logRollbackSummary: jest.fn(),
|
||||
logCompletionSummary: jest.fn(),
|
||||
logErrorAnalysis: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock constructors
|
||||
getConfig.mockReturnValue(mockConfig);
|
||||
ProductService.mockImplementation(() => mockProductService);
|
||||
Logger.mockImplementation(() => mockLogger);
|
||||
|
||||
app = new ShopifyPriceUpdater();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Rollback Mode Initialization", () => {
|
||||
test("should initialize with rollback configuration", async () => {
|
||||
const result = await app.initialize();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(getConfig).toHaveBeenCalled();
|
||||
expect(mockLogger.logRollbackStart).toHaveBeenCalledWith(mockConfig);
|
||||
expect(mockLogger.logOperationStart).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should handle initialization failure", async () => {
|
||||
getConfig.mockImplementation(() => {
|
||||
throw new Error("Configuration error");
|
||||
});
|
||||
|
||||
const result = await app.initialize();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
"Initialization failed: Configuration error"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rollback Product Fetching and Validation", () => {
|
||||
test("should fetch and validate products for rollback", async () => {
|
||||
const mockProducts = [
|
||||
{
|
||||
id: "gid://shopify/Product/123",
|
||||
title: "Test Product",
|
||||
variants: [
|
||||
{
|
||||
id: "gid://shopify/ProductVariant/456",
|
||||
price: 50.0,
|
||||
compareAtPrice: 75.0,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockEligibleProducts = [mockProducts[0]];
|
||||
const mockSummary = {
|
||||
totalProducts: 1,
|
||||
totalVariants: 1,
|
||||
priceRange: { min: 50, max: 50 },
|
||||
};
|
||||
|
||||
mockProductService.fetchProductsByTag.mockResolvedValue(mockProducts);
|
||||
mockProductService.validateProductsForRollback.mockResolvedValue(
|
||||
mockEligibleProducts
|
||||
);
|
||||
mockProductService.getProductSummary.mockReturnValue(mockSummary);
|
||||
|
||||
// Initialize app first
|
||||
await app.initialize();
|
||||
const result = await app.fetchAndValidateProductsForRollback();
|
||||
|
||||
expect(result).toEqual(mockEligibleProducts);
|
||||
expect(mockProductService.fetchProductsByTag).toHaveBeenCalledWith(
|
||||
"test-tag"
|
||||
);
|
||||
expect(
|
||||
mockProductService.validateProductsForRollback
|
||||
).toHaveBeenCalledWith(mockProducts);
|
||||
expect(mockLogger.logProductCount).toHaveBeenCalledWith(1);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith("Rollback Product Summary:");
|
||||
});
|
||||
|
||||
test("should handle empty product results", async () => {
|
||||
mockProductService.fetchProductsByTag.mockResolvedValue([]);
|
||||
|
||||
// Initialize app first
|
||||
await app.initialize();
|
||||
const result = await app.fetchAndValidateProductsForRollback();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"No products found with the specified tag. Operation completed."
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle product fetching errors", async () => {
|
||||
mockProductService.fetchProductsByTag.mockRejectedValue(
|
||||
new Error("API error")
|
||||
);
|
||||
|
||||
// Initialize app first
|
||||
await app.initialize();
|
||||
const result = await app.fetchAndValidateProductsForRollback();
|
||||
|
||||
expect(result).toBe(null);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
"Failed to fetch products for rollback: API error"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rollback Price Operations", () => {
|
||||
test("should execute rollback operations successfully", async () => {
|
||||
const mockProducts = [
|
||||
{
|
||||
id: "gid://shopify/Product/123",
|
||||
title: "Test Product",
|
||||
variants: [
|
||||
{
|
||||
id: "gid://shopify/ProductVariant/456",
|
||||
price: 50.0,
|
||||
compareAtPrice: 75.0,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockResults = {
|
||||
totalProducts: 1,
|
||||
totalVariants: 1,
|
||||
eligibleVariants: 1,
|
||||
successfulRollbacks: 1,
|
||||
failedRollbacks: 0,
|
||||
skippedVariants: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
mockProductService.rollbackProductPrices.mockResolvedValue(mockResults);
|
||||
|
||||
const result = await app.rollbackPrices(mockProducts);
|
||||
|
||||
expect(result).toEqual(mockResults);
|
||||
expect(mockProductService.rollbackProductPrices).toHaveBeenCalledWith(
|
||||
mockProducts
|
||||
);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"Starting price rollback operations"
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle empty products array", async () => {
|
||||
const result = await app.rollbackPrices([]);
|
||||
|
||||
expect(result).toEqual({
|
||||
totalProducts: 0,
|
||||
totalVariants: 0,
|
||||
eligibleVariants: 0,
|
||||
successfulRollbacks: 0,
|
||||
failedRollbacks: 0,
|
||||
skippedVariants: 0,
|
||||
errors: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle rollback operation errors", async () => {
|
||||
const mockProducts = [
|
||||
{
|
||||
id: "gid://shopify/Product/123",
|
||||
title: "Test Product",
|
||||
variants: [{ price: 50.0, compareAtPrice: 75.0 }],
|
||||
},
|
||||
];
|
||||
|
||||
mockProductService.rollbackProductPrices.mockRejectedValue(
|
||||
new Error("Rollback failed")
|
||||
);
|
||||
|
||||
const result = await app.rollbackPrices(mockProducts);
|
||||
|
||||
expect(result).toBe(null);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
"Price rollback failed: Rollback failed"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rollback Summary Display", () => {
|
||||
test("should display successful rollback summary", async () => {
|
||||
const mockResults = {
|
||||
totalProducts: 5,
|
||||
totalVariants: 8,
|
||||
eligibleVariants: 6,
|
||||
successfulRollbacks: 6,
|
||||
failedRollbacks: 0,
|
||||
skippedVariants: 2,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
app.startTime = new Date();
|
||||
|
||||
const exitCode = await app.displayRollbackSummary(mockResults);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockLogger.logRollbackSummary).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
totalProducts: 5,
|
||||
totalVariants: 8,
|
||||
eligibleVariants: 6,
|
||||
successfulRollbacks: 6,
|
||||
failedRollbacks: 0,
|
||||
skippedVariants: 2,
|
||||
})
|
||||
);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"🎉 All rollback operations completed successfully!"
|
||||
);
|
||||
});
|
||||
|
||||
test("should display partial success rollback summary", async () => {
|
||||
const mockResults = {
|
||||
totalProducts: 5,
|
||||
totalVariants: 8,
|
||||
eligibleVariants: 6,
|
||||
successfulRollbacks: 5,
|
||||
failedRollbacks: 1,
|
||||
skippedVariants: 2,
|
||||
errors: [
|
||||
{
|
||||
productId: "gid://shopify/Product/123",
|
||||
errorMessage: "Test error",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
app.startTime = new Date();
|
||||
|
||||
const exitCode = await app.displayRollbackSummary(mockResults);
|
||||
|
||||
expect(exitCode).toBe(1); // Moderate success rate (83.3%)
|
||||
expect(mockLogger.logRollbackSummary).toHaveBeenCalled();
|
||||
expect(mockLogger.logErrorAnalysis).toHaveBeenCalledWith(
|
||||
mockResults.errors,
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(mockLogger.warning).toHaveBeenCalledWith(
|
||||
expect.stringContaining("moderate success rate")
|
||||
);
|
||||
});
|
||||
|
||||
test("should display moderate success rollback summary", async () => {
|
||||
const mockResults = {
|
||||
totalProducts: 5,
|
||||
totalVariants: 8,
|
||||
eligibleVariants: 6,
|
||||
successfulRollbacks: 3,
|
||||
failedRollbacks: 3,
|
||||
skippedVariants: 2,
|
||||
errors: [
|
||||
{ productId: "1", errorMessage: "Error 1" },
|
||||
{ productId: "2", errorMessage: "Error 2" },
|
||||
{ productId: "3", errorMessage: "Error 3" },
|
||||
],
|
||||
};
|
||||
|
||||
app.startTime = new Date();
|
||||
|
||||
const exitCode = await app.displayRollbackSummary(mockResults);
|
||||
|
||||
expect(exitCode).toBe(1); // Moderate success rate (50%)
|
||||
expect(mockLogger.warning).toHaveBeenCalledWith(
|
||||
expect.stringContaining("moderate success rate")
|
||||
);
|
||||
});
|
||||
|
||||
test("should display low success rollback summary", async () => {
|
||||
const mockResults = {
|
||||
totalProducts: 5,
|
||||
totalVariants: 8,
|
||||
eligibleVariants: 6,
|
||||
successfulRollbacks: 1,
|
||||
failedRollbacks: 5,
|
||||
skippedVariants: 2,
|
||||
errors: Array.from({ length: 5 }, (_, i) => ({
|
||||
productId: `${i}`,
|
||||
errorMessage: `Error ${i}`,
|
||||
})),
|
||||
};
|
||||
|
||||
app.startTime = new Date();
|
||||
|
||||
const exitCode = await app.displayRollbackSummary(mockResults);
|
||||
|
||||
expect(exitCode).toBe(2); // Low success rate (16.7%)
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("low success rate")
|
||||
);
|
||||
});
|
||||
|
||||
test("should display complete failure rollback summary", async () => {
|
||||
const mockResults = {
|
||||
totalProducts: 5,
|
||||
totalVariants: 8,
|
||||
eligibleVariants: 6,
|
||||
successfulRollbacks: 0,
|
||||
failedRollbacks: 6,
|
||||
skippedVariants: 2,
|
||||
errors: Array.from({ length: 6 }, (_, i) => ({
|
||||
productId: `${i}`,
|
||||
errorMessage: `Error ${i}`,
|
||||
})),
|
||||
};
|
||||
|
||||
app.startTime = new Date();
|
||||
|
||||
const exitCode = await app.displayRollbackSummary(mockResults);
|
||||
|
||||
expect(exitCode).toBe(2);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
"❌ All rollback operations failed. Please check your configuration and try again."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Operation Mode Header Display", () => {
|
||||
test("should display rollback mode header", async () => {
|
||||
const consoleSpy = jest.spyOn(console, "log").mockImplementation();
|
||||
|
||||
// Initialize app first
|
||||
await app.initialize();
|
||||
await app.displayOperationModeHeader();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("SHOPIFY PRICE ROLLBACK MODE")
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"Reverting prices from compare-at to main price"
|
||||
)
|
||||
);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith("Operation Mode: ROLLBACK");
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("should display update mode header when not in rollback mode", async () => {
|
||||
mockConfig.operationMode = "update";
|
||||
app.config = mockConfig;
|
||||
|
||||
const consoleSpy = jest.spyOn(console, "log").mockImplementation();
|
||||
|
||||
await app.displayOperationModeHeader();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("SHOPIFY PRICE UPDATE MODE")
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Adjusting prices by 10%")
|
||||
);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith("Operation Mode: UPDATE");
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Complete Rollback Workflow", () => {
|
||||
test("should execute complete rollback workflow successfully", async () => {
|
||||
// Mock successful initialization
|
||||
mockProductService.shopifyService.testConnection.mockResolvedValue(true);
|
||||
|
||||
// Mock successful product fetching and validation
|
||||
const mockProducts = [
|
||||
{
|
||||
id: "gid://shopify/Product/123",
|
||||
title: "Test Product",
|
||||
variants: [
|
||||
{
|
||||
id: "gid://shopify/ProductVariant/456",
|
||||
price: 50.0,
|
||||
compareAtPrice: 75.0,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
mockProductService.fetchProductsByTag.mockResolvedValue(mockProducts);
|
||||
mockProductService.validateProductsForRollback.mockResolvedValue(
|
||||
mockProducts
|
||||
);
|
||||
mockProductService.getProductSummary.mockReturnValue({
|
||||
totalProducts: 1,
|
||||
totalVariants: 1,
|
||||
priceRange: { min: 50, max: 50 },
|
||||
});
|
||||
|
||||
// Mock successful rollback
|
||||
const mockResults = {
|
||||
totalProducts: 1,
|
||||
totalVariants: 1,
|
||||
eligibleVariants: 1,
|
||||
successfulRollbacks: 1,
|
||||
failedRollbacks: 0,
|
||||
skippedVariants: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
mockProductService.rollbackProductPrices.mockResolvedValue(mockResults);
|
||||
|
||||
const exitCode = await app.run();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockLogger.logRollbackStart).toHaveBeenCalledWith(mockConfig);
|
||||
expect(mockProductService.fetchProductsByTag).toHaveBeenCalledWith(
|
||||
"test-tag"
|
||||
);
|
||||
expect(
|
||||
mockProductService.validateProductsForRollback
|
||||
).toHaveBeenCalledWith(mockProducts);
|
||||
expect(mockProductService.rollbackProductPrices).toHaveBeenCalledWith(
|
||||
mockProducts
|
||||
);
|
||||
expect(mockLogger.logRollbackSummary).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should handle rollback workflow with initialization failure", async () => {
|
||||
getConfig.mockImplementation(() => {
|
||||
throw new Error("Config error");
|
||||
});
|
||||
|
||||
const exitCode = await app.run();
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Initialization failed")
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle rollback workflow with connection failure", async () => {
|
||||
mockProductService.shopifyService.testConnection.mockResolvedValue(false);
|
||||
|
||||
const exitCode = await app.run();
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("API connection failed")
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle rollback workflow with product fetching failure", async () => {
|
||||
mockProductService.shopifyService.testConnection.mockResolvedValue(true);
|
||||
mockProductService.fetchProductsByTag.mockRejectedValue(
|
||||
new Error("Fetch error")
|
||||
);
|
||||
|
||||
const exitCode = await app.run();
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Product fetching for rollback failed")
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle rollback workflow with rollback operation failure", async () => {
|
||||
mockProductService.shopifyService.testConnection.mockResolvedValue(true);
|
||||
mockProductService.fetchProductsByTag.mockResolvedValue([
|
||||
{
|
||||
id: "gid://shopify/Product/123",
|
||||
variants: [{ price: 50, compareAtPrice: 75 }],
|
||||
},
|
||||
]);
|
||||
mockProductService.validateProductsForRollback.mockResolvedValue([
|
||||
{
|
||||
id: "gid://shopify/Product/123",
|
||||
variants: [{ price: 50, compareAtPrice: 75 }],
|
||||
},
|
||||
]);
|
||||
mockProductService.getProductSummary.mockReturnValue({
|
||||
totalProducts: 1,
|
||||
totalVariants: 1,
|
||||
priceRange: { min: 50, max: 50 },
|
||||
});
|
||||
mockProductService.rollbackProductPrices.mockRejectedValue(
|
||||
new Error("Rollback error")
|
||||
);
|
||||
|
||||
const exitCode = await app.run();
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Price rollback process failed")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dual Operation Mode Support", () => {
|
||||
test("should route to update workflow when operation mode is update", async () => {
|
||||
mockConfig.operationMode = "update";
|
||||
app.config = mockConfig;
|
||||
|
||||
// Mock update-specific methods
|
||||
app.safeFetchAndValidateProducts = jest.fn().mockResolvedValue([]);
|
||||
app.safeUpdatePrices = jest.fn().mockResolvedValue({
|
||||
totalProducts: 0,
|
||||
totalVariants: 0,
|
||||
successfulUpdates: 0,
|
||||
failedUpdates: 0,
|
||||
errors: [],
|
||||
});
|
||||
app.displaySummaryAndGetExitCode = jest.fn().mockResolvedValue(0);
|
||||
|
||||
mockProductService.shopifyService.testConnection.mockResolvedValue(true);
|
||||
|
||||
const exitCode = await app.run();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockLogger.logOperationStart).toHaveBeenCalledWith(mockConfig);
|
||||
expect(mockLogger.logRollbackStart).not.toHaveBeenCalled();
|
||||
expect(app.safeFetchAndValidateProducts).toHaveBeenCalled();
|
||||
expect(app.safeUpdatePrices).toHaveBeenCalled();
|
||||
expect(app.displaySummaryAndGetExitCode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should route to rollback workflow when operation mode is rollback", async () => {
|
||||
mockConfig.operationMode = "rollback";
|
||||
app.config = mockConfig;
|
||||
|
||||
mockProductService.shopifyService.testConnection.mockResolvedValue(true);
|
||||
mockProductService.fetchProductsByTag.mockResolvedValue([]);
|
||||
mockProductService.validateProductsForRollback.mockResolvedValue([]);
|
||||
mockProductService.getProductSummary.mockReturnValue({
|
||||
totalProducts: 0,
|
||||
totalVariants: 0,
|
||||
priceRange: { min: 0, max: 0 },
|
||||
});
|
||||
|
||||
const exitCode = await app.run();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockLogger.logRollbackStart).toHaveBeenCalledWith(mockConfig);
|
||||
expect(mockLogger.logOperationStart).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling and Recovery", () => {
|
||||
test("should handle unexpected errors gracefully", async () => {
|
||||
mockProductService.shopifyService.testConnection.mockResolvedValue(true);
|
||||
mockProductService.fetchProductsByTag.mockImplementation(() => {
|
||||
throw new Error("Unexpected error");
|
||||
});
|
||||
|
||||
const exitCode = await app.run();
|
||||
|
||||
expect(exitCode).toBe(1); // Critical failure exit code
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Product fetching for rollback failed")
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle critical failures with proper logging", async () => {
|
||||
// Initialize app first
|
||||
await app.initialize();
|
||||
const exitCode = await app.handleCriticalFailure("Test failure", 1);
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
"Critical failure in rollback mode: Test failure"
|
||||
);
|
||||
expect(mockLogger.logRollbackSummary).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
totalProducts: 0,
|
||||
errors: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
errorMessage: "Test failure",
|
||||
}),
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle unexpected errors with partial results", async () => {
|
||||
const partialResults = {
|
||||
totalProducts: 2,
|
||||
totalVariants: 3,
|
||||
eligibleVariants: 2,
|
||||
successfulRollbacks: 1,
|
||||
failedRollbacks: 1,
|
||||
skippedVariants: 1,
|
||||
errors: [{ errorMessage: "Previous error" }],
|
||||
};
|
||||
|
||||
const error = new Error("Unexpected error");
|
||||
error.stack = "Error stack trace";
|
||||
|
||||
// Initialize app first
|
||||
await app.initialize();
|
||||
await app.handleUnexpectedError(error, partialResults);
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
"Unexpected error occurred in rollback mode: Unexpected error"
|
||||
);
|
||||
expect(mockLogger.logRollbackSummary).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
totalProducts: 2,
|
||||
totalVariants: 3,
|
||||
eligibleVariants: 2,
|
||||
successfulRollbacks: 1,
|
||||
failedRollbacks: 1,
|
||||
skippedVariants: 1,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Backward Compatibility", () => {
|
||||
test("should default to update mode when operation mode is not specified", async () => {
|
||||
mockConfig.operationMode = "update";
|
||||
app.config = mockConfig;
|
||||
|
||||
// Mock update workflow methods
|
||||
app.safeFetchAndValidateProducts = jest.fn().mockResolvedValue([]);
|
||||
app.safeUpdatePrices = jest.fn().mockResolvedValue({
|
||||
totalProducts: 0,
|
||||
totalVariants: 0,
|
||||
successfulUpdates: 0,
|
||||
failedUpdates: 0,
|
||||
errors: [],
|
||||
});
|
||||
app.displaySummaryAndGetExitCode = jest.fn().mockResolvedValue(0);
|
||||
|
||||
mockProductService.shopifyService.testConnection.mockResolvedValue(true);
|
||||
|
||||
const exitCode = await app.run();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockLogger.logOperationStart).toHaveBeenCalledWith(mockConfig);
|
||||
expect(mockLogger.logRollbackStart).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
797
tests/integration/rollback-workflow.test.js
Normal file
797
tests/integration/rollback-workflow.test.js
Normal file
@@ -0,0 +1,797 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -109,6 +109,47 @@ describe("ProgressService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("logRollbackStart", () => {
|
||||
test("should create progress file and log rollback operation start", async () => {
|
||||
const config = {
|
||||
targetTag: "rollback-tag",
|
||||
};
|
||||
|
||||
await progressService.logRollbackStart(config);
|
||||
|
||||
// Check that file was created
|
||||
const content = await fs.readFile(testFilePath, "utf8");
|
||||
|
||||
expect(content).toContain("# Shopify Price Update Progress Log");
|
||||
expect(content).toContain("## Price Rollback Operation -");
|
||||
expect(content).toContain("Target Tag: rollback-tag");
|
||||
expect(content).toContain("Operation Mode: rollback");
|
||||
expect(content).toContain("**Configuration:**");
|
||||
expect(content).toContain("**Progress:**");
|
||||
});
|
||||
|
||||
test("should distinguish rollback from update operations in logs", async () => {
|
||||
const updateConfig = {
|
||||
targetTag: "update-tag",
|
||||
priceAdjustmentPercentage: 10,
|
||||
};
|
||||
|
||||
const rollbackConfig = {
|
||||
targetTag: "rollback-tag",
|
||||
};
|
||||
|
||||
await progressService.logOperationStart(updateConfig);
|
||||
await progressService.logRollbackStart(rollbackConfig);
|
||||
|
||||
const content = await fs.readFile(testFilePath, "utf8");
|
||||
|
||||
expect(content).toContain("## Price Update Operation -");
|
||||
expect(content).toContain("Price Adjustment: 10%");
|
||||
expect(content).toContain("## Price Rollback Operation -");
|
||||
expect(content).toContain("Operation Mode: rollback");
|
||||
});
|
||||
});
|
||||
|
||||
describe("logProductUpdate", () => {
|
||||
test("should log successful product update", async () => {
|
||||
// First create the file
|
||||
@@ -182,6 +223,97 @@ describe("ProgressService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("logRollbackUpdate", () => {
|
||||
test("should log successful rollback operation", async () => {
|
||||
// First create the file
|
||||
await progressService.logRollbackStart({
|
||||
targetTag: "rollback-test",
|
||||
});
|
||||
|
||||
const entry = {
|
||||
productId: "gid://shopify/Product/123456789",
|
||||
productTitle: "Rollback Test Product",
|
||||
variantId: "gid://shopify/ProductVariant/987654321",
|
||||
oldPrice: 1000.0,
|
||||
compareAtPrice: 750.0,
|
||||
newPrice: 750.0,
|
||||
};
|
||||
|
||||
await progressService.logRollbackUpdate(entry);
|
||||
|
||||
const content = await fs.readFile(testFilePath, "utf8");
|
||||
|
||||
expect(content).toContain(
|
||||
"🔄 **Rollback Test Product** (gid://shopify/Product/123456789)"
|
||||
);
|
||||
expect(content).toContain(
|
||||
"Variant: gid://shopify/ProductVariant/987654321"
|
||||
);
|
||||
expect(content).toContain("Price: $1000 → $750 (from Compare At: $750)");
|
||||
expect(content).toContain("Rolled back:");
|
||||
});
|
||||
|
||||
test("should distinguish rollback from update entries", async () => {
|
||||
await progressService.logRollbackStart({
|
||||
targetTag: "test",
|
||||
});
|
||||
|
||||
const updateEntry = {
|
||||
productId: "gid://shopify/Product/123",
|
||||
productTitle: "Update Product",
|
||||
variantId: "gid://shopify/ProductVariant/456",
|
||||
oldPrice: 750.0,
|
||||
newPrice: 1000.0,
|
||||
};
|
||||
|
||||
const rollbackEntry = {
|
||||
productId: "gid://shopify/Product/789",
|
||||
productTitle: "Rollback Product",
|
||||
variantId: "gid://shopify/ProductVariant/012",
|
||||
oldPrice: 1000.0,
|
||||
compareAtPrice: 750.0,
|
||||
newPrice: 750.0,
|
||||
};
|
||||
|
||||
await progressService.logProductUpdate(updateEntry);
|
||||
await progressService.logRollbackUpdate(rollbackEntry);
|
||||
|
||||
const content = await fs.readFile(testFilePath, "utf8");
|
||||
|
||||
// Update entry should use checkmark
|
||||
expect(content).toContain("✅ **Update Product**");
|
||||
expect(content).toContain("Updated:");
|
||||
|
||||
// Rollback entry should use rollback emoji
|
||||
expect(content).toContain("🔄 **Rollback Product**");
|
||||
expect(content).toContain("from Compare At:");
|
||||
expect(content).toContain("Rolled back:");
|
||||
});
|
||||
|
||||
test("should handle products with special characters in rollback", async () => {
|
||||
await progressService.logRollbackStart({
|
||||
targetTag: "test",
|
||||
});
|
||||
|
||||
const entry = {
|
||||
productId: "gid://shopify/Product/123",
|
||||
productTitle: 'Rollback Product with "Quotes" & Special Chars!',
|
||||
variantId: "gid://shopify/ProductVariant/456",
|
||||
oldPrice: 500.0,
|
||||
compareAtPrice: 400.0,
|
||||
newPrice: 400.0,
|
||||
};
|
||||
|
||||
await progressService.logRollbackUpdate(entry);
|
||||
|
||||
const content = await fs.readFile(testFilePath, "utf8");
|
||||
|
||||
expect(content).toContain(
|
||||
'**Rollback Product with "Quotes" & Special Chars!**'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("logError", () => {
|
||||
test("should log error with all details", async () => {
|
||||
await progressService.logOperationStart({
|
||||
@@ -325,6 +457,123 @@ describe("ProgressService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("logRollbackSummary", () => {
|
||||
test("should log rollback completion summary with all statistics", async () => {
|
||||
await progressService.logRollbackStart({
|
||||
targetTag: "rollback-test",
|
||||
});
|
||||
|
||||
const startTime = new Date(Date.now() - 8000); // 8 seconds ago
|
||||
const summary = {
|
||||
totalProducts: 5,
|
||||
totalVariants: 8,
|
||||
eligibleVariants: 6,
|
||||
successfulRollbacks: 5,
|
||||
failedRollbacks: 1,
|
||||
skippedVariants: 2,
|
||||
startTime: startTime,
|
||||
};
|
||||
|
||||
await progressService.logRollbackSummary(summary);
|
||||
|
||||
const content = await fs.readFile(testFilePath, "utf8");
|
||||
|
||||
expect(content).toContain("**Rollback Summary:**");
|
||||
expect(content).toContain("Total Products Processed: 5");
|
||||
expect(content).toContain("Total Variants Processed: 8");
|
||||
expect(content).toContain("Eligible Variants: 6");
|
||||
expect(content).toContain("Successful Rollbacks: 5");
|
||||
expect(content).toContain("Failed Rollbacks: 1");
|
||||
expect(content).toContain("Skipped Variants: 2 (no compare-at price)");
|
||||
expect(content).toContain("Duration: 8 seconds");
|
||||
expect(content).toContain("Completed:");
|
||||
expect(content).toContain("---");
|
||||
});
|
||||
|
||||
test("should handle rollback summary without start time", async () => {
|
||||
await progressService.logRollbackStart({
|
||||
targetTag: "test",
|
||||
});
|
||||
|
||||
const summary = {
|
||||
totalProducts: 3,
|
||||
totalVariants: 5,
|
||||
eligibleVariants: 5,
|
||||
successfulRollbacks: 5,
|
||||
failedRollbacks: 0,
|
||||
skippedVariants: 0,
|
||||
};
|
||||
|
||||
await progressService.logRollbackSummary(summary);
|
||||
|
||||
const content = await fs.readFile(testFilePath, "utf8");
|
||||
|
||||
expect(content).toContain("Duration: Unknown seconds");
|
||||
});
|
||||
|
||||
test("should distinguish rollback summary from update summary", async () => {
|
||||
await progressService.logRollbackStart({
|
||||
targetTag: "test",
|
||||
});
|
||||
|
||||
const updateSummary = {
|
||||
totalProducts: 5,
|
||||
successfulUpdates: 4,
|
||||
failedUpdates: 1,
|
||||
startTime: new Date(Date.now() - 5000),
|
||||
};
|
||||
|
||||
const rollbackSummary = {
|
||||
totalProducts: 3,
|
||||
totalVariants: 6,
|
||||
eligibleVariants: 4,
|
||||
successfulRollbacks: 3,
|
||||
failedRollbacks: 1,
|
||||
skippedVariants: 2,
|
||||
startTime: new Date(Date.now() - 3000),
|
||||
};
|
||||
|
||||
await progressService.logCompletionSummary(updateSummary);
|
||||
await progressService.logRollbackSummary(rollbackSummary);
|
||||
|
||||
const content = await fs.readFile(testFilePath, "utf8");
|
||||
|
||||
// Should contain both summary types
|
||||
expect(content).toContain("**Summary:**");
|
||||
expect(content).toContain("Successful Updates: 4");
|
||||
expect(content).toContain("**Rollback Summary:**");
|
||||
expect(content).toContain("Successful Rollbacks: 3");
|
||||
expect(content).toContain("Skipped Variants: 2 (no compare-at price)");
|
||||
});
|
||||
|
||||
test("should handle zero rollback statistics", async () => {
|
||||
await progressService.logRollbackStart({
|
||||
targetTag: "test",
|
||||
});
|
||||
|
||||
const summary = {
|
||||
totalProducts: 0,
|
||||
totalVariants: 0,
|
||||
eligibleVariants: 0,
|
||||
successfulRollbacks: 0,
|
||||
failedRollbacks: 0,
|
||||
skippedVariants: 0,
|
||||
startTime: new Date(),
|
||||
};
|
||||
|
||||
await progressService.logRollbackSummary(summary);
|
||||
|
||||
const content = await fs.readFile(testFilePath, "utf8");
|
||||
|
||||
expect(content).toContain("Total Products Processed: 0");
|
||||
expect(content).toContain("Total Variants Processed: 0");
|
||||
expect(content).toContain("Eligible Variants: 0");
|
||||
expect(content).toContain("Successful Rollbacks: 0");
|
||||
expect(content).toContain("Failed Rollbacks: 0");
|
||||
expect(content).toContain("Skipped Variants: 0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("categorizeError", () => {
|
||||
test("should categorize rate limiting errors", () => {
|
||||
const testCases = [
|
||||
|
||||
461
tests/utils/logger.test.js
Normal file
461
tests/utils/logger.test.js
Normal file
@@ -0,0 +1,461 @@
|
||||
const Logger = require("../../src/utils/logger");
|
||||
const ProgressService = require("../../src/services/progress");
|
||||
|
||||
// Mock the ProgressService
|
||||
jest.mock("../../src/services/progress");
|
||||
|
||||
describe("Logger", () => {
|
||||
let logger;
|
||||
let mockProgressService;
|
||||
let consoleSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock progress service
|
||||
mockProgressService = {
|
||||
logOperationStart: jest.fn(),
|
||||
logRollbackStart: jest.fn(),
|
||||
logProductUpdate: jest.fn(),
|
||||
logRollbackUpdate: jest.fn(),
|
||||
logCompletionSummary: jest.fn(),
|
||||
logRollbackSummary: jest.fn(),
|
||||
logError: jest.fn(),
|
||||
logErrorAnalysis: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock the ProgressService constructor
|
||||
ProgressService.mockImplementation(() => mockProgressService);
|
||||
|
||||
logger = new Logger();
|
||||
|
||||
// Spy on console methods
|
||||
consoleSpy = {
|
||||
log: jest.spyOn(console, "log").mockImplementation(() => {}),
|
||||
warn: jest.spyOn(console, "warn").mockImplementation(() => {}),
|
||||
error: jest.spyOn(console, "error").mockImplementation(() => {}),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
consoleSpy.log.mockRestore();
|
||||
consoleSpy.warn.mockRestore();
|
||||
consoleSpy.error.mockRestore();
|
||||
});
|
||||
|
||||
describe("Rollback Logging Methods", () => {
|
||||
describe("logRollbackStart", () => {
|
||||
it("should log rollback operation start to console and progress file", async () => {
|
||||
const config = {
|
||||
targetTag: "test-tag",
|
||||
shopDomain: "test-shop.myshopify.com",
|
||||
};
|
||||
|
||||
await logger.logRollbackStart(config);
|
||||
|
||||
// Check console output
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"Starting price rollback operation with configuration:"
|
||||
)
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Target Tag: test-tag")
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Operation Mode: rollback")
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Shop Domain: test-shop.myshopify.com")
|
||||
);
|
||||
|
||||
// Check progress service was called
|
||||
expect(mockProgressService.logRollbackStart).toHaveBeenCalledWith(
|
||||
config
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("logRollbackUpdate", () => {
|
||||
it("should log successful rollback operations to console and progress file", async () => {
|
||||
const entry = {
|
||||
productTitle: "Test Product",
|
||||
productId: "gid://shopify/Product/123",
|
||||
variantId: "gid://shopify/ProductVariant/456",
|
||||
oldPrice: 1000.0,
|
||||
compareAtPrice: 750.0,
|
||||
newPrice: 750.0,
|
||||
};
|
||||
|
||||
await logger.logRollbackUpdate(entry);
|
||||
|
||||
// Check console output contains rollback-specific formatting
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("🔄")
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Rolled back "Test Product"')
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Price: 1000 → 750")
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("from Compare At: 750")
|
||||
);
|
||||
|
||||
// Check progress service was called
|
||||
expect(mockProgressService.logRollbackUpdate).toHaveBeenCalledWith(
|
||||
entry
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("logRollbackSummary", () => {
|
||||
it("should log rollback completion summary to console and progress file", async () => {
|
||||
const summary = {
|
||||
totalProducts: 5,
|
||||
totalVariants: 8,
|
||||
eligibleVariants: 6,
|
||||
successfulRollbacks: 5,
|
||||
failedRollbacks: 1,
|
||||
skippedVariants: 2,
|
||||
startTime: new Date(Date.now() - 30000), // 30 seconds ago
|
||||
};
|
||||
|
||||
await logger.logRollbackSummary(summary);
|
||||
|
||||
// Check console output contains rollback-specific formatting
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("ROLLBACK OPERATION COMPLETE")
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Total Products Processed: 5")
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Total Variants Processed: 8")
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Eligible Variants: 6")
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Successful Rollbacks: ")
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed Rollbacks: ")
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Skipped Variants: ")
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("no compare-at price")
|
||||
);
|
||||
|
||||
// Check progress service was called
|
||||
expect(mockProgressService.logRollbackSummary).toHaveBeenCalledWith(
|
||||
summary
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle zero failed rollbacks without red coloring", async () => {
|
||||
const summary = {
|
||||
totalProducts: 3,
|
||||
totalVariants: 5,
|
||||
eligibleVariants: 5,
|
||||
successfulRollbacks: 5,
|
||||
failedRollbacks: 0,
|
||||
skippedVariants: 0,
|
||||
startTime: new Date(Date.now() - 15000),
|
||||
};
|
||||
|
||||
await logger.logRollbackSummary(summary);
|
||||
|
||||
// Should show failed rollbacks without red coloring when zero
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed Rollbacks: 0")
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Skipped Variants: 0")
|
||||
);
|
||||
});
|
||||
|
||||
it("should show colored output for failed rollbacks and skipped variants when greater than zero", async () => {
|
||||
const summary = {
|
||||
totalProducts: 5,
|
||||
totalVariants: 8,
|
||||
eligibleVariants: 6,
|
||||
successfulRollbacks: 4,
|
||||
failedRollbacks: 2,
|
||||
skippedVariants: 2,
|
||||
startTime: new Date(Date.now() - 45000),
|
||||
};
|
||||
|
||||
await logger.logRollbackSummary(summary);
|
||||
|
||||
// Should show colored output for non-zero values
|
||||
const logCalls = consoleSpy.log.mock.calls.map((call) => call[0]);
|
||||
const failedRollbacksCall = logCalls.find((call) =>
|
||||
call.includes("Failed Rollbacks:")
|
||||
);
|
||||
const skippedVariantsCall = logCalls.find((call) =>
|
||||
call.includes("Skipped Variants:")
|
||||
);
|
||||
|
||||
expect(failedRollbacksCall).toContain("\x1b[31m"); // Red color code
|
||||
expect(skippedVariantsCall).toContain("\x1b[33m"); // Yellow color code
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rollback vs Update Distinction", () => {
|
||||
it("should distinguish rollback logs from update logs in console output", async () => {
|
||||
const updateEntry = {
|
||||
productTitle: "Test Product",
|
||||
oldPrice: 750.0,
|
||||
newPrice: 1000.0,
|
||||
compareAtPrice: 1000.0,
|
||||
};
|
||||
|
||||
const rollbackEntry = {
|
||||
productTitle: "Test Product",
|
||||
oldPrice: 1000.0,
|
||||
compareAtPrice: 750.0,
|
||||
newPrice: 750.0,
|
||||
};
|
||||
|
||||
await logger.logProductUpdate(updateEntry);
|
||||
await logger.logRollbackUpdate(rollbackEntry);
|
||||
|
||||
const logCalls = consoleSpy.log.mock.calls.map((call) => call[0]);
|
||||
|
||||
// Update should use checkmark emoji
|
||||
const updateCall = logCalls.find((call) => call.includes("Updated"));
|
||||
expect(updateCall).toContain("✅");
|
||||
|
||||
// Rollback should use rollback emoji
|
||||
const rollbackCall = logCalls.find((call) =>
|
||||
call.includes("Rolled back")
|
||||
);
|
||||
expect(rollbackCall).toContain("🔄");
|
||||
});
|
||||
|
||||
it("should call different progress service methods for updates vs rollbacks", async () => {
|
||||
const updateEntry = {
|
||||
productTitle: "Test",
|
||||
oldPrice: 750,
|
||||
newPrice: 1000,
|
||||
};
|
||||
const rollbackEntry = {
|
||||
productTitle: "Test",
|
||||
oldPrice: 1000,
|
||||
newPrice: 750,
|
||||
compareAtPrice: 750,
|
||||
};
|
||||
|
||||
await logger.logProductUpdate(updateEntry);
|
||||
await logger.logRollbackUpdate(rollbackEntry);
|
||||
|
||||
expect(mockProgressService.logProductUpdate).toHaveBeenCalledWith(
|
||||
updateEntry
|
||||
);
|
||||
expect(mockProgressService.logRollbackUpdate).toHaveBeenCalledWith(
|
||||
rollbackEntry
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should handle progress service errors gracefully", async () => {
|
||||
mockProgressService.logRollbackStart.mockRejectedValue(
|
||||
new Error("Progress service error")
|
||||
);
|
||||
|
||||
const config = {
|
||||
targetTag: "test-tag",
|
||||
shopDomain: "test-shop.myshopify.com",
|
||||
};
|
||||
|
||||
// Should not throw even if progress service fails
|
||||
await expect(logger.logRollbackStart(config)).resolves.not.toThrow();
|
||||
|
||||
// Console output should still work
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Starting price rollback operation")
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle rollback update logging errors gracefully", async () => {
|
||||
mockProgressService.logRollbackUpdate.mockRejectedValue(
|
||||
new Error("Progress service error")
|
||||
);
|
||||
|
||||
const entry = {
|
||||
productTitle: "Test Product",
|
||||
oldPrice: 1000,
|
||||
newPrice: 750,
|
||||
compareAtPrice: 750,
|
||||
};
|
||||
|
||||
// Should not throw even if progress service fails
|
||||
await expect(logger.logRollbackUpdate(entry)).resolves.not.toThrow();
|
||||
|
||||
// Console output should still work
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Rolled back")
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle rollback summary logging errors gracefully", async () => {
|
||||
mockProgressService.logRollbackSummary.mockRejectedValue(
|
||||
new Error("Progress service error")
|
||||
);
|
||||
|
||||
const summary = {
|
||||
totalProducts: 5,
|
||||
successfulRollbacks: 4,
|
||||
failedRollbacks: 1,
|
||||
skippedVariants: 0,
|
||||
startTime: new Date(),
|
||||
};
|
||||
|
||||
// Should not throw even if progress service fails
|
||||
await expect(logger.logRollbackSummary(summary)).resolves.not.toThrow();
|
||||
|
||||
// Console output should still work
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("ROLLBACK OPERATION COMPLETE")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Existing Logger Methods", () => {
|
||||
describe("Basic logging methods", () => {
|
||||
it("should log info messages to console", async () => {
|
||||
await logger.info("Test info message");
|
||||
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Test info message")
|
||||
);
|
||||
});
|
||||
|
||||
it("should log warning messages to console", async () => {
|
||||
await logger.warning("Test warning message");
|
||||
|
||||
expect(consoleSpy.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Test warning message")
|
||||
);
|
||||
});
|
||||
|
||||
it("should log error messages to console", async () => {
|
||||
await logger.error("Test error message");
|
||||
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Test error message")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Operation start logging", () => {
|
||||
it("should log operation start for update mode", async () => {
|
||||
const config = {
|
||||
targetTag: "test-tag",
|
||||
priceAdjustmentPercentage: 10,
|
||||
shopDomain: "test-shop.myshopify.com",
|
||||
};
|
||||
|
||||
await logger.logOperationStart(config);
|
||||
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Starting price update operation")
|
||||
);
|
||||
expect(mockProgressService.logOperationStart).toHaveBeenCalledWith(
|
||||
config
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Product update logging", () => {
|
||||
it("should log product updates", async () => {
|
||||
const entry = {
|
||||
productTitle: "Test Product",
|
||||
oldPrice: 100,
|
||||
newPrice: 110,
|
||||
compareAtPrice: 100,
|
||||
};
|
||||
|
||||
await logger.logProductUpdate(entry);
|
||||
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Updated")
|
||||
);
|
||||
expect(mockProgressService.logProductUpdate).toHaveBeenCalledWith(
|
||||
entry
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Completion summary logging", () => {
|
||||
it("should log completion summary", async () => {
|
||||
const summary = {
|
||||
totalProducts: 5,
|
||||
successfulUpdates: 4,
|
||||
failedUpdates: 1,
|
||||
startTime: new Date(),
|
||||
};
|
||||
|
||||
await logger.logCompletionSummary(summary);
|
||||
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("OPERATION COMPLETE")
|
||||
);
|
||||
expect(mockProgressService.logCompletionSummary).toHaveBeenCalledWith(
|
||||
summary
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error logging", () => {
|
||||
it("should log product errors", async () => {
|
||||
const errorEntry = {
|
||||
productTitle: "Test Product",
|
||||
errorMessage: "Test error",
|
||||
};
|
||||
|
||||
await logger.logProductError(errorEntry);
|
||||
|
||||
expect(mockProgressService.logError).toHaveBeenCalledWith(errorEntry);
|
||||
});
|
||||
|
||||
it("should log error analysis", async () => {
|
||||
const errors = [
|
||||
{ errorMessage: "Error 1" },
|
||||
{ errorMessage: "Error 2" },
|
||||
];
|
||||
const summary = { totalProducts: 2 };
|
||||
|
||||
await logger.logErrorAnalysis(errors, summary);
|
||||
|
||||
expect(mockProgressService.logErrorAnalysis).toHaveBeenCalledWith(
|
||||
errors,
|
||||
summary
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Product count logging", () => {
|
||||
it("should log product count", async () => {
|
||||
await logger.logProductCount(5);
|
||||
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Found 5 products")
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle zero products", async () => {
|
||||
await logger.logProductCount(0);
|
||||
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Found 0 products")
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,8 @@ const {
|
||||
calculatePercentageChange,
|
||||
isValidPercentage,
|
||||
preparePriceUpdate,
|
||||
validateRollbackEligibility,
|
||||
prepareRollbackUpdate,
|
||||
} = require("../../src/utils/price");
|
||||
|
||||
describe("Price Utilities", () => {
|
||||
@@ -260,4 +262,315 @@ describe("Price Utilities", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateRollbackEligibility", () => {
|
||||
test("should return eligible for valid variant with different prices", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
compareAtPrice: "75.00",
|
||||
};
|
||||
|
||||
const result = validateRollbackEligibility(variant);
|
||||
|
||||
expect(result.isEligible).toBe(true);
|
||||
expect(result.variant.id).toBe("gid://shopify/ProductVariant/123");
|
||||
expect(result.variant.currentPrice).toBe(50.0);
|
||||
expect(result.variant.compareAtPrice).toBe(75.0);
|
||||
expect(result.reason).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should return not eligible when variant is null or undefined", () => {
|
||||
expect(validateRollbackEligibility(null).isEligible).toBe(false);
|
||||
expect(validateRollbackEligibility(null).reason).toBe(
|
||||
"Invalid variant object"
|
||||
);
|
||||
|
||||
expect(validateRollbackEligibility(undefined).isEligible).toBe(false);
|
||||
expect(validateRollbackEligibility(undefined).reason).toBe(
|
||||
"Invalid variant object"
|
||||
);
|
||||
});
|
||||
|
||||
test("should return not eligible when variant is not an object", () => {
|
||||
expect(validateRollbackEligibility("invalid").isEligible).toBe(false);
|
||||
expect(validateRollbackEligibility("invalid").reason).toBe(
|
||||
"Invalid variant object"
|
||||
);
|
||||
|
||||
expect(validateRollbackEligibility(123).isEligible).toBe(false);
|
||||
expect(validateRollbackEligibility(123).reason).toBe(
|
||||
"Invalid variant object"
|
||||
);
|
||||
});
|
||||
|
||||
test("should return not eligible when current price is invalid", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "invalid",
|
||||
compareAtPrice: "75.00",
|
||||
};
|
||||
|
||||
const result = validateRollbackEligibility(variant);
|
||||
|
||||
expect(result.isEligible).toBe(false);
|
||||
expect(result.reason).toBe("Invalid current price");
|
||||
expect(result.variant.currentPrice).toBeNaN();
|
||||
});
|
||||
|
||||
test("should return not eligible when current price is negative", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "-10.00",
|
||||
compareAtPrice: "75.00",
|
||||
};
|
||||
|
||||
const result = validateRollbackEligibility(variant);
|
||||
|
||||
expect(result.isEligible).toBe(false);
|
||||
expect(result.reason).toBe("Invalid current price");
|
||||
expect(result.variant.currentPrice).toBe(-10.0);
|
||||
});
|
||||
|
||||
test("should return not eligible when compare-at price is null", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
compareAtPrice: null,
|
||||
};
|
||||
|
||||
const result = validateRollbackEligibility(variant);
|
||||
|
||||
expect(result.isEligible).toBe(false);
|
||||
expect(result.reason).toBe("No compare-at price available");
|
||||
expect(result.variant.compareAtPrice).toBe(null);
|
||||
});
|
||||
|
||||
test("should return not eligible when compare-at price is undefined", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
// compareAtPrice is undefined
|
||||
};
|
||||
|
||||
const result = validateRollbackEligibility(variant);
|
||||
|
||||
expect(result.isEligible).toBe(false);
|
||||
expect(result.reason).toBe("No compare-at price available");
|
||||
expect(result.variant.compareAtPrice).toBe(null);
|
||||
});
|
||||
|
||||
test("should return not eligible when compare-at price is invalid", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
compareAtPrice: "invalid",
|
||||
};
|
||||
|
||||
const result = validateRollbackEligibility(variant);
|
||||
|
||||
expect(result.isEligible).toBe(false);
|
||||
expect(result.reason).toBe("Invalid compare-at price");
|
||||
expect(result.variant.compareAtPrice).toBeNaN();
|
||||
});
|
||||
|
||||
test("should return not eligible when compare-at price is zero", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
compareAtPrice: "0.00",
|
||||
};
|
||||
|
||||
const result = validateRollbackEligibility(variant);
|
||||
|
||||
expect(result.isEligible).toBe(false);
|
||||
expect(result.reason).toBe("Compare-at price must be greater than zero");
|
||||
expect(result.variant.compareAtPrice).toBe(0.0);
|
||||
});
|
||||
|
||||
test("should return not eligible when compare-at price is negative", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
compareAtPrice: "-10.00",
|
||||
};
|
||||
|
||||
const result = validateRollbackEligibility(variant);
|
||||
|
||||
expect(result.isEligible).toBe(false);
|
||||
expect(result.reason).toBe("Compare-at price must be greater than zero");
|
||||
expect(result.variant.compareAtPrice).toBe(-10.0);
|
||||
});
|
||||
|
||||
test("should return not eligible when prices are the same", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
compareAtPrice: "50.00",
|
||||
};
|
||||
|
||||
const result = validateRollbackEligibility(variant);
|
||||
|
||||
expect(result.isEligible).toBe(false);
|
||||
expect(result.reason).toBe(
|
||||
"Compare-at price is the same as current price"
|
||||
);
|
||||
expect(result.variant.currentPrice).toBe(50.0);
|
||||
expect(result.variant.compareAtPrice).toBe(50.0);
|
||||
});
|
||||
|
||||
test("should return not eligible when prices are nearly the same (within epsilon)", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
compareAtPrice: "50.005", // Within 0.01 epsilon
|
||||
};
|
||||
|
||||
const result = validateRollbackEligibility(variant);
|
||||
|
||||
expect(result.isEligible).toBe(false);
|
||||
expect(result.reason).toBe(
|
||||
"Compare-at price is the same as current price"
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle numeric price values", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: 50.0,
|
||||
compareAtPrice: 75.0,
|
||||
};
|
||||
|
||||
const result = validateRollbackEligibility(variant);
|
||||
|
||||
expect(result.isEligible).toBe(true);
|
||||
expect(result.variant.currentPrice).toBe(50.0);
|
||||
expect(result.variant.compareAtPrice).toBe(75.0);
|
||||
});
|
||||
|
||||
test("should handle decimal prices correctly", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "29.99",
|
||||
compareAtPrice: "39.99",
|
||||
};
|
||||
|
||||
const result = validateRollbackEligibility(variant);
|
||||
|
||||
expect(result.isEligible).toBe(true);
|
||||
expect(result.variant.currentPrice).toBe(29.99);
|
||||
expect(result.variant.compareAtPrice).toBe(39.99);
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepareRollbackUpdate", () => {
|
||||
test("should prepare rollback update for eligible variant", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
compareAtPrice: "75.00",
|
||||
};
|
||||
|
||||
const result = prepareRollbackUpdate(variant);
|
||||
|
||||
expect(result.newPrice).toBe(75.0);
|
||||
expect(result.compareAtPrice).toBe(null);
|
||||
});
|
||||
|
||||
test("should prepare rollback update with decimal prices", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "29.99",
|
||||
compareAtPrice: "39.99",
|
||||
};
|
||||
|
||||
const result = prepareRollbackUpdate(variant);
|
||||
|
||||
expect(result.newPrice).toBe(39.99);
|
||||
expect(result.compareAtPrice).toBe(null);
|
||||
});
|
||||
|
||||
test("should handle numeric price values", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: 25.5,
|
||||
compareAtPrice: 35.75,
|
||||
};
|
||||
|
||||
const result = prepareRollbackUpdate(variant);
|
||||
|
||||
expect(result.newPrice).toBe(35.75);
|
||||
expect(result.compareAtPrice).toBe(null);
|
||||
});
|
||||
|
||||
test("should throw error for variant without compare-at price", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
compareAtPrice: null,
|
||||
};
|
||||
|
||||
expect(() => prepareRollbackUpdate(variant)).toThrow(
|
||||
"Cannot prepare rollback update: No compare-at price available"
|
||||
);
|
||||
});
|
||||
|
||||
test("should throw error for variant with invalid compare-at price", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
compareAtPrice: "invalid",
|
||||
};
|
||||
|
||||
expect(() => prepareRollbackUpdate(variant)).toThrow(
|
||||
"Cannot prepare rollback update: Invalid compare-at price"
|
||||
);
|
||||
});
|
||||
|
||||
test("should throw error for variant with zero compare-at price", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
compareAtPrice: "0.00",
|
||||
};
|
||||
|
||||
expect(() => prepareRollbackUpdate(variant)).toThrow(
|
||||
"Cannot prepare rollback update: Compare-at price must be greater than zero"
|
||||
);
|
||||
});
|
||||
|
||||
test("should throw error for variant with same prices", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "50.00",
|
||||
compareAtPrice: "50.00",
|
||||
};
|
||||
|
||||
expect(() => prepareRollbackUpdate(variant)).toThrow(
|
||||
"Cannot prepare rollback update: Compare-at price is the same as current price"
|
||||
);
|
||||
});
|
||||
|
||||
test("should throw error for invalid variant object", () => {
|
||||
expect(() => prepareRollbackUpdate(null)).toThrow(
|
||||
"Cannot prepare rollback update: Invalid variant object"
|
||||
);
|
||||
|
||||
expect(() => prepareRollbackUpdate("invalid")).toThrow(
|
||||
"Cannot prepare rollback update: Invalid variant object"
|
||||
);
|
||||
});
|
||||
|
||||
test("should throw error for variant with invalid current price", () => {
|
||||
const variant = {
|
||||
id: "gid://shopify/ProductVariant/123",
|
||||
price: "invalid",
|
||||
compareAtPrice: "75.00",
|
||||
};
|
||||
|
||||
expect(() => prepareRollbackUpdate(variant)).toThrow(
|
||||
"Cannot prepare rollback update: Invalid current price"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user