Files
PriceUpdaterAppv2/tests/index.test.js

687 lines
19 KiB
JavaScript

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();
});
});
});