Files
PriceUpdaterAppv2/tests/services/progress.test.js
Spencer Grimes 1e6881ba86 Initial commit: Complete Shopify Price Updater implementation
- Full Node.js application with Shopify GraphQL API integration
- Compare At price support for promotional pricing
- Comprehensive error handling and retry logic
- Progress tracking with markdown logging
- Complete test suite with unit and integration tests
- Production-ready with proper exit codes and signal handling
2025-08-05 10:05:05 -05:00

560 lines
15 KiB
JavaScript

const ProgressService = require("../../src/services/progress");
const fs = require("fs").promises;
const path = require("path");
describe("ProgressService", () => {
let progressService;
let testFilePath;
beforeEach(() => {
// Use a unique test file for each test to avoid conflicts
testFilePath = `test-progress-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}.md`;
progressService = new ProgressService(testFilePath);
});
afterEach(async () => {
// Clean up test file after each test
try {
await fs.unlink(testFilePath);
} catch (error) {
// File might not exist, that's okay
}
});
describe("formatTimestamp", () => {
test("should format timestamp correctly", () => {
const testDate = new Date("2024-01-15T14:30:45.123Z");
const formatted = progressService.formatTimestamp(testDate);
expect(formatted).toBe("2024-01-15 14:30:45 UTC");
});
test("should use current date when no date provided", () => {
const formatted = progressService.formatTimestamp();
// Should be a valid timestamp format
expect(formatted).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC$/);
});
test("should handle different dates correctly", () => {
const testCases = [
{
input: new Date("2023-12-31T23:59:59.999Z"),
expected: "2023-12-31 23:59:59 UTC",
},
{
input: new Date("2024-01-01T00:00:00.000Z"),
expected: "2024-01-01 00:00:00 UTC",
},
{
input: new Date("2024-06-15T12:00:00.500Z"),
expected: "2024-06-15 12:00:00 UTC",
},
];
testCases.forEach(({ input, expected }) => {
expect(progressService.formatTimestamp(input)).toBe(expected);
});
});
});
describe("logOperationStart", () => {
test("should create progress file and log operation start", async () => {
const config = {
targetTag: "test-tag",
priceAdjustmentPercentage: 10,
};
await progressService.logOperationStart(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 Update Operation -");
expect(content).toContain("Target Tag: test-tag");
expect(content).toContain("Price Adjustment: 10%");
expect(content).toContain("**Configuration:**");
expect(content).toContain("**Progress:**");
});
test("should handle negative percentage", async () => {
const config = {
targetTag: "clearance",
priceAdjustmentPercentage: -25,
};
await progressService.logOperationStart(config);
const content = await fs.readFile(testFilePath, "utf8");
expect(content).toContain("Target Tag: clearance");
expect(content).toContain("Price Adjustment: -25%");
});
test("should handle special characters in tag", async () => {
const config = {
targetTag: "sale-2024_special!",
priceAdjustmentPercentage: 15.5,
};
await progressService.logOperationStart(config);
const content = await fs.readFile(testFilePath, "utf8");
expect(content).toContain("Target Tag: sale-2024_special!");
expect(content).toContain("Price Adjustment: 15.5%");
});
});
describe("logProductUpdate", () => {
test("should log successful product update", async () => {
// First create the file
await progressService.logOperationStart({
targetTag: "test",
priceAdjustmentPercentage: 10,
});
const entry = {
productId: "gid://shopify/Product/123456789",
productTitle: "Test Product",
variantId: "gid://shopify/ProductVariant/987654321",
oldPrice: 29.99,
newPrice: 32.99,
};
await progressService.logProductUpdate(entry);
const content = await fs.readFile(testFilePath, "utf8");
expect(content).toContain(
"✅ **Test Product** (gid://shopify/Product/123456789)"
);
expect(content).toContain(
"Variant: gid://shopify/ProductVariant/987654321"
);
expect(content).toContain("Price: $29.99 → $32.99");
expect(content).toContain("Updated:");
});
test("should handle products with special characters in title", async () => {
await progressService.logOperationStart({
targetTag: "test",
priceAdjustmentPercentage: 10,
});
const entry = {
productId: "gid://shopify/Product/123",
productTitle: 'Product with "Quotes" & Special Chars!',
variantId: "gid://shopify/ProductVariant/456",
oldPrice: 10.0,
newPrice: 11.0,
};
await progressService.logProductUpdate(entry);
const content = await fs.readFile(testFilePath, "utf8");
expect(content).toContain('**Product with "Quotes" & Special Chars!**');
});
test("should handle decimal prices correctly", async () => {
await progressService.logOperationStart({
targetTag: "test",
priceAdjustmentPercentage: 5.5,
});
const entry = {
productId: "gid://shopify/Product/123",
productTitle: "Decimal Price Product",
variantId: "gid://shopify/ProductVariant/456",
oldPrice: 19.95,
newPrice: 21.05,
};
await progressService.logProductUpdate(entry);
const content = await fs.readFile(testFilePath, "utf8");
expect(content).toContain("Price: $19.95 → $21.05");
});
});
describe("logError", () => {
test("should log error with all details", async () => {
await progressService.logOperationStart({
targetTag: "test",
priceAdjustmentPercentage: 10,
});
const entry = {
productId: "gid://shopify/Product/123",
productTitle: "Failed Product",
variantId: "gid://shopify/ProductVariant/456",
errorMessage: "Invalid price data",
};
await progressService.logError(entry);
const content = await fs.readFile(testFilePath, "utf8");
expect(content).toContain(
"❌ **Failed Product** (gid://shopify/Product/123)"
);
expect(content).toContain("Variant: gid://shopify/ProductVariant/456");
expect(content).toContain("Error: Invalid price data");
expect(content).toContain("Failed:");
});
test("should handle error without variant ID", async () => {
await progressService.logOperationStart({
targetTag: "test",
priceAdjustmentPercentage: 10,
});
const entry = {
productId: "gid://shopify/Product/123",
productTitle: "Failed Product",
errorMessage: "Product not found",
};
await progressService.logError(entry);
const content = await fs.readFile(testFilePath, "utf8");
expect(content).toContain(
"❌ **Failed Product** (gid://shopify/Product/123)"
);
expect(content).not.toContain("Variant:");
expect(content).toContain("Error: Product not found");
});
test("should handle complex error messages", async () => {
await progressService.logOperationStart({
targetTag: "test",
priceAdjustmentPercentage: 10,
});
const entry = {
productId: "gid://shopify/Product/123",
productTitle: "Complex Error Product",
variantId: "gid://shopify/ProductVariant/456",
errorMessage:
"GraphQL error: Field 'price' of type 'Money!' must not be null",
};
await progressService.logError(entry);
const content = await fs.readFile(testFilePath, "utf8");
expect(content).toContain(
"Error: GraphQL error: Field 'price' of type 'Money!' must not be null"
);
});
});
describe("logCompletionSummary", () => {
test("should log completion summary with all statistics", async () => {
await progressService.logOperationStart({
targetTag: "test",
priceAdjustmentPercentage: 10,
});
const startTime = new Date(Date.now() - 5000); // 5 seconds ago
const summary = {
totalProducts: 10,
successfulUpdates: 8,
failedUpdates: 2,
startTime: startTime,
};
await progressService.logCompletionSummary(summary);
const content = await fs.readFile(testFilePath, "utf8");
expect(content).toContain("**Summary:**");
expect(content).toContain("Total Products Processed: 10");
expect(content).toContain("Successful Updates: 8");
expect(content).toContain("Failed Updates: 2");
expect(content).toContain("Duration: 5 seconds");
expect(content).toContain("Completed:");
expect(content).toContain("---");
});
test("should handle summary without start time", async () => {
await progressService.logOperationStart({
targetTag: "test",
priceAdjustmentPercentage: 10,
});
const summary = {
totalProducts: 5,
successfulUpdates: 5,
failedUpdates: 0,
};
await progressService.logCompletionSummary(summary);
const content = await fs.readFile(testFilePath, "utf8");
expect(content).toContain("Duration: Unknown seconds");
});
test("should handle zero statistics", async () => {
await progressService.logOperationStart({
targetTag: "test",
priceAdjustmentPercentage: 10,
});
const summary = {
totalProducts: 0,
successfulUpdates: 0,
failedUpdates: 0,
startTime: new Date(),
};
await progressService.logCompletionSummary(summary);
const content = await fs.readFile(testFilePath, "utf8");
expect(content).toContain("Total Products Processed: 0");
expect(content).toContain("Successful Updates: 0");
expect(content).toContain("Failed Updates: 0");
});
});
describe("categorizeError", () => {
test("should categorize rate limiting errors", () => {
const testCases = [
"Rate limit exceeded",
"HTTP 429 Too Many Requests",
"Request was throttled",
];
testCases.forEach((errorMessage) => {
expect(progressService.categorizeError(errorMessage)).toBe(
"Rate Limiting"
);
});
});
test("should categorize network errors", () => {
const testCases = [
"Network connection failed",
"Connection timeout",
"Network error occurred",
];
testCases.forEach((errorMessage) => {
expect(progressService.categorizeError(errorMessage)).toBe(
"Network Issues"
);
});
});
test("should categorize authentication errors", () => {
const testCases = [
"Authentication failed",
"HTTP 401 Unauthorized",
"Invalid authentication credentials",
];
testCases.forEach((errorMessage) => {
expect(progressService.categorizeError(errorMessage)).toBe(
"Authentication"
);
});
});
test("should categorize permission errors", () => {
const testCases = [
"Permission denied",
"HTTP 403 Forbidden",
"Insufficient permissions",
];
testCases.forEach((errorMessage) => {
expect(progressService.categorizeError(errorMessage)).toBe(
"Permissions"
);
});
});
test("should categorize not found errors", () => {
const testCases = [
"Product not found",
"HTTP 404 Not Found",
"Resource not found",
];
testCases.forEach((errorMessage) => {
expect(progressService.categorizeError(errorMessage)).toBe(
"Resource Not Found"
);
});
});
test("should categorize validation errors", () => {
const testCases = [
"Validation error: Invalid price",
"Invalid product data",
"Price validation failed",
];
testCases.forEach((errorMessage) => {
expect(progressService.categorizeError(errorMessage)).toBe(
"Data Validation"
);
});
});
test("should categorize server errors", () => {
const testCases = [
"Internal server error",
"HTTP 500 Server Error",
"HTTP 502 Bad Gateway",
"HTTP 503 Service Unavailable",
];
testCases.forEach((errorMessage) => {
expect(progressService.categorizeError(errorMessage)).toBe(
"Server Errors"
);
});
});
test("should categorize Shopify API errors", () => {
const testCases = [
"Shopify API error occurred",
"Shopify API request failed",
];
testCases.forEach((errorMessage) => {
expect(progressService.categorizeError(errorMessage)).toBe(
"Shopify API"
);
});
});
test("should categorize unknown errors as Other", () => {
const testCases = [
"Something went wrong",
"Unexpected error",
"Random failure message",
];
testCases.forEach((errorMessage) => {
expect(progressService.categorizeError(errorMessage)).toBe("Other");
});
});
test("should handle case insensitive categorization", () => {
expect(progressService.categorizeError("RATE LIMIT EXCEEDED")).toBe(
"Rate Limiting"
);
expect(progressService.categorizeError("Network Connection Failed")).toBe(
"Network Issues"
);
expect(progressService.categorizeError("AUTHENTICATION FAILED")).toBe(
"Authentication"
);
});
});
describe("createProgressEntry", () => {
test("should create progress entry with timestamp", () => {
const data = {
productId: "gid://shopify/Product/123",
productTitle: "Test Product",
status: "success",
};
const entry = progressService.createProgressEntry(data);
expect(entry).toHaveProperty("timestamp");
expect(entry.timestamp).toBeInstanceOf(Date);
expect(entry.productId).toBe("gid://shopify/Product/123");
expect(entry.productTitle).toBe("Test Product");
expect(entry.status).toBe("success");
});
test("should preserve all original data", () => {
const data = {
productId: "gid://shopify/Product/456",
productTitle: "Another Product",
variantId: "gid://shopify/ProductVariant/789",
oldPrice: 10.0,
newPrice: 11.0,
errorMessage: "Some error",
};
const entry = progressService.createProgressEntry(data);
expect(entry.productId).toBe(data.productId);
expect(entry.productTitle).toBe(data.productTitle);
expect(entry.variantId).toBe(data.variantId);
expect(entry.oldPrice).toBe(data.oldPrice);
expect(entry.newPrice).toBe(data.newPrice);
expect(entry.errorMessage).toBe(data.errorMessage);
});
});
describe("appendToProgressFile", () => {
test("should create file with header when file doesn't exist", async () => {
await progressService.appendToProgressFile("Test content");
const content = await fs.readFile(testFilePath, "utf8");
expect(content).toContain("# Shopify Price Update Progress Log");
expect(content).toContain(
"This file tracks the progress of price update operations."
);
expect(content).toContain("Test content");
});
test("should append to existing file without adding header", async () => {
// Create file first
await progressService.appendToProgressFile("First content");
// Append more content
await progressService.appendToProgressFile("Second content");
const content = await fs.readFile(testFilePath, "utf8");
// Should only have one header
const headerCount = (
content.match(/# Shopify Price Update Progress Log/g) || []
).length;
expect(headerCount).toBe(1);
expect(content).toContain("First content");
expect(content).toContain("Second content");
});
test("should handle file write errors gracefully", async () => {
// Mock fs.appendFile to throw an error
const originalAppendFile = fs.appendFile;
const mockAppendFile = jest
.fn()
.mockRejectedValue(new Error("Permission denied"));
fs.appendFile = mockAppendFile;
// Should not throw an error, but should log warnings
const consoleSpy = jest
.spyOn(console, "warn")
.mockImplementation(() => {});
await progressService.appendToProgressFile("Test content");
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("Warning: Failed to write to progress file")
);
consoleSpy.mockRestore();
fs.appendFile = originalAppendFile;
});
});
});