Implemented Rollback Functionality

This commit is contained in:
2025-08-06 15:18:44 -05:00
parent d741dd5466
commit 78818793f2
20 changed files with 6365 additions and 74 deletions

461
tests/utils/logger.test.js Normal file
View 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")
);
});
});
});
});

View File

@@ -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"
);
});
});
});