Files
PriceUpdaterAppv2/tests/tui/integration/errorHandlingRecovery.test.js

669 lines
20 KiB
JavaScript

const React = require("react");
const { render } = require("ink-testing-library");
const TuiApplication = require("../../../src/tui/TuiApplication.jsx");
// Mock all the services and providers
jest.mock("../../../src/tui/providers/AppProvider.jsx");
jest.mock("../../../src/tui/hooks/useServices.js");
jest.mock("../../../src/tui/components/common/LoadingIndicator.jsx");
jest.mock("../../../src/tui/components/common/ErrorDisplay.jsx");
describe("Error Handling and Recovery Integration Tests", () => {
let mockAppState;
let mockServices;
let mockUseInput;
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
// Mock AppProvider
mockAppState = {
currentScreen: "main",
navigateTo: jest.fn(),
navigateBack: jest.fn(),
getScreenState: jest.fn(),
saveScreenState: jest.fn(),
updateConfiguration: jest.fn(),
getConfiguration: jest.fn(() => ({
targetTag: "test-tag",
shopDomain: "test-shop.myshopify.com",
accessToken: "test-token",
})),
};
require("../../../src/tui/providers/AppProvider.jsx").useAppState = jest.fn(
() => mockAppState
);
// Mock Services
mockServices = {
getAllSchedules: jest.fn(),
addSchedule: jest.fn(),
updateSchedule: jest.fn(),
deleteSchedule: jest.fn(),
getLogFiles: jest.fn(),
readLogFile: jest.fn(),
parseLogContent: jest.fn(),
filterLogs: jest.fn(),
fetchAllTags: jest.fn(),
getTagDetails: jest.fn(),
calculateTagStatistics: jest.fn(),
searchTags: jest.fn(),
};
require("../../../src/tui/hooks/useServices.js").useServices = jest.fn(
() => mockServices
);
// Mock useInput
mockUseInput = jest.fn();
require("ink").useInput = mockUseInput;
// Mock common components
require("../../../src/tui/components/common/LoadingIndicator.jsx").LoadingIndicator =
({ children }) =>
React.createElement("div", { "data-testid": "loading" }, children);
require("../../../src/tui/components/common/ErrorDisplay.jsx").ErrorDisplay =
({ error, onRetry }) =>
React.createElement(
"div",
{
"data-testid": "error",
onClick: onRetry,
},
error?.message || "An error occurred"
);
});
describe("Network Error Handling", () => {
test("should handle network timeouts gracefully in scheduling screen", async () => {
const networkError = new Error("Network timeout");
networkError.code = "NETWORK_TIMEOUT";
mockServices.getAllSchedules.mockRejectedValue(networkError);
mockAppState.currentScreen = "scheduling";
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Network timeout");
expect(lastFrame()).toContain("Check your internet connection");
});
test("should handle connection refused errors in tag analysis screen", async () => {
const connectionError = new Error("Connection refused");
connectionError.code = "ECONNREFUSED";
mockServices.fetchAllTags.mockRejectedValue(connectionError);
mockAppState.currentScreen = "tagAnalysis";
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Connection refused");
expect(lastFrame()).toContain("Unable to connect to Shopify");
});
test("should provide retry functionality for network errors", async () => {
const networkError = new Error("Network error");
mockServices.getLogFiles
.mockRejectedValueOnce(networkError)
.mockResolvedValueOnce([]);
mockAppState.currentScreen = "viewLogs";
let inputHandler;
mockUseInput.mockImplementation((handler) => {
inputHandler = handler;
});
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Network error");
// Retry operation
inputHandler("r");
await new Promise((resolve) => setTimeout(resolve, 100));
expect(mockServices.getLogFiles).toHaveBeenCalledTimes(2);
});
test("should implement exponential backoff for repeated network failures", async () => {
const networkError = new Error("Network unstable");
mockServices.fetchAllTags
.mockRejectedValueOnce(networkError)
.mockRejectedValueOnce(networkError)
.mockResolvedValueOnce([]);
mockAppState.currentScreen = "tagAnalysis";
let inputHandler;
mockUseInput.mockImplementation((handler) => {
inputHandler = handler;
});
render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
// First retry
inputHandler("r");
await new Promise((resolve) => setTimeout(resolve, 100));
// Second retry (should have longer delay)
inputHandler("r");
await new Promise((resolve) => setTimeout(resolve, 200));
expect(mockServices.fetchAllTags).toHaveBeenCalledTimes(3);
});
});
describe("API Error Handling", () => {
test("should handle Shopify API rate limiting", async () => {
const rateLimitError = new Error("Rate limit exceeded");
rateLimitError.code = "RATE_LIMITED";
rateLimitError.retryAfter = 5;
mockServices.fetchAllTags.mockRejectedValue(rateLimitError);
mockAppState.currentScreen = "tagAnalysis";
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Rate limit exceeded");
expect(lastFrame()).toContain("Please wait 5 seconds");
});
test("should handle authentication errors", async () => {
const authError = new Error("Invalid access token");
authError.code = "UNAUTHORIZED";
mockServices.fetchAllTags.mockRejectedValue(authError);
mockAppState.currentScreen = "tagAnalysis";
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Invalid access token");
expect(lastFrame()).toContain("Check your Shopify credentials");
expect(lastFrame()).toContain("Go to Configuration");
});
test("should handle API permission errors", async () => {
const permissionError = new Error("Insufficient permissions");
permissionError.code = "FORBIDDEN";
mockServices.getTagDetails.mockRejectedValue(permissionError);
mockAppState.currentScreen = "tagAnalysis";
let inputHandler;
mockUseInput.mockImplementation((handler) => {
inputHandler = handler;
});
mockServices.fetchAllTags.mockResolvedValue([
{ tag: "test-tag", productCount: 1, variantCount: 1, totalValue: 100 },
]);
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
// Try to view tag details
inputHandler("", { return: true });
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Insufficient permissions");
expect(lastFrame()).toContain(
"Your API token may not have the required permissions"
);
});
test("should handle API version compatibility errors", async () => {
const versionError = new Error("API version not supported");
versionError.code = "API_VERSION_MISMATCH";
mockServices.fetchAllTags.mockRejectedValue(versionError);
mockAppState.currentScreen = "tagAnalysis";
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("API version not supported");
expect(lastFrame()).toContain("Please update the application");
});
});
describe("File System Error Handling", () => {
test("should handle missing schedules.json file gracefully", async () => {
const fileError = new Error("ENOENT: no such file or directory");
fileError.code = "ENOENT";
mockServices.getAllSchedules.mockRejectedValue(fileError);
mockAppState.currentScreen = "scheduling";
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("No schedules found");
expect(lastFrame()).toContain("Create your first schedule");
});
test("should handle corrupted log files", async () => {
const mockLogFiles = [
{ filename: "corrupted.md", size: 1024, operationCount: 5 },
];
mockServices.getLogFiles.mockResolvedValue(mockLogFiles);
mockServices.readLogFile.mockResolvedValue("Corrupted content");
mockServices.parseLogContent.mockImplementation(() => {
throw new Error("Failed to parse log content");
});
mockAppState.currentScreen = "viewLogs";
let inputHandler;
mockUseInput.mockImplementation((handler) => {
inputHandler = handler;
});
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
// Select corrupted log file
inputHandler("", { return: true });
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Failed to parse log content");
expect(lastFrame()).toContain("Showing raw content");
});
test("should handle permission denied errors for file operations", async () => {
const permissionError = new Error("Permission denied");
permissionError.code = "EACCES";
mockServices.addSchedule.mockRejectedValue(permissionError);
mockAppState.currentScreen = "scheduling";
mockServices.getAllSchedules.mockResolvedValue([]);
let inputHandler;
mockUseInput.mockImplementation((handler) => {
inputHandler = handler;
});
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
// Try to create new schedule
inputHandler("n");
inputHandler("", { return: true });
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Permission denied");
expect(lastFrame()).toContain("Check file permissions");
});
test("should handle disk space errors", async () => {
const diskSpaceError = new Error("No space left on device");
diskSpaceError.code = "ENOSPC";
mockServices.addSchedule.mockRejectedValue(diskSpaceError);
mockAppState.currentScreen = "scheduling";
mockServices.getAllSchedules.mockResolvedValue([]);
let inputHandler;
mockUseInput.mockImplementation((handler) => {
inputHandler = handler;
});
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
// Try to create new schedule
inputHandler("n");
inputHandler("", { return: true });
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("No space left on device");
expect(lastFrame()).toContain("Free up disk space");
});
});
describe("Validation Error Handling", () => {
test("should handle form validation errors in scheduling screen", async () => {
mockServices.getAllSchedules.mockResolvedValue([]);
const validationError = new Error("Invalid schedule data");
validationError.code = "VALIDATION_ERROR";
validationError.details = {
scheduledTime: "Invalid date format",
operationType: "Must be 'update' or 'rollback'",
};
mockServices.addSchedule.mockRejectedValue(validationError);
mockAppState.currentScreen = "scheduling";
let inputHandler;
mockUseInput.mockImplementation((handler) => {
inputHandler = handler;
});
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
// Try to create invalid schedule
inputHandler("n");
inputHandler("", { return: true });
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Invalid date format");
expect(lastFrame()).toContain("Must be 'update' or 'rollback'");
});
test("should handle configuration validation errors", async () => {
const configError = new Error("Invalid configuration");
configError.code = "CONFIG_INVALID";
mockAppState.updateConfiguration.mockImplementation(() => {
throw configError;
});
mockServices.fetchAllTags.mockResolvedValue([
{ tag: "test-tag", productCount: 1, variantCount: 1, totalValue: 100 },
]);
mockAppState.currentScreen = "tagAnalysis";
let inputHandler;
mockUseInput.mockImplementation((handler) => {
inputHandler = handler;
});
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
// Try to update configuration with invalid tag
inputHandler("c");
inputHandler("y");
expect(lastFrame()).toContain("Invalid configuration");
});
});
describe("Recovery Mechanisms", () => {
test("should automatically retry failed operations with exponential backoff", async () => {
const transientError = new Error("Temporary service unavailable");
transientError.code = "SERVICE_UNAVAILABLE";
mockServices.fetchAllTags
.mockRejectedValueOnce(transientError)
.mockRejectedValueOnce(transientError)
.mockResolvedValueOnce([]);
mockAppState.currentScreen = "tagAnalysis";
let inputHandler;
mockUseInput.mockImplementation((handler) => {
inputHandler = handler;
});
render(React.createElement(TuiApplication));
// Should automatically retry
await new Promise((resolve) => setTimeout(resolve, 500));
expect(mockServices.fetchAllTags).toHaveBeenCalledTimes(3);
});
test("should provide manual retry option for persistent errors", async () => {
const persistentError = new Error("Service down for maintenance");
mockServices.getAllSchedules
.mockRejectedValue(persistentError)
.mockRejectedValue(persistentError)
.mockResolvedValue([]);
mockAppState.currentScreen = "scheduling";
let inputHandler;
mockUseInput.mockImplementation((handler) => {
inputHandler = handler;
});
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Service down for maintenance");
expect(lastFrame()).toContain("Press 'r' to retry");
// Manual retry
inputHandler("r");
await new Promise((resolve) => setTimeout(resolve, 100));
expect(mockServices.getAllSchedules).toHaveBeenCalledTimes(2);
});
test("should fallback to cached data when available", async () => {
const networkError = new Error("Network unavailable");
// Mock cached data
mockAppState.getScreenState.mockReturnValue({
cachedTags: [
{
tag: "cached-tag",
productCount: 5,
variantCount: 15,
totalValue: 500,
},
],
lastFetch: Date.now() - 300000, // 5 minutes ago
});
mockServices.fetchAllTags.mockRejectedValue(networkError);
mockAppState.currentScreen = "tagAnalysis";
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("cached-tag");
expect(lastFrame()).toContain("Using cached data");
expect(lastFrame()).toContain("5 minutes ago");
});
test("should gracefully degrade functionality when services are unavailable", async () => {
const serviceError = new Error("All services unavailable");
mockServices.getAllSchedules.mockRejectedValue(serviceError);
mockServices.getLogFiles.mockRejectedValue(serviceError);
mockServices.fetchAllTags.mockRejectedValue(serviceError);
// Test each screen handles degraded mode
const screens = ["scheduling", "viewLogs", "tagAnalysis"];
for (const screen of screens) {
mockAppState.currentScreen = screen;
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Service unavailable");
expect(lastFrame()).toContain("Limited functionality");
}
});
});
describe("Error State Management", () => {
test("should clear error state when operation succeeds", async () => {
const temporaryError = new Error("Temporary error");
mockServices.getAllSchedules
.mockRejectedValueOnce(temporaryError)
.mockResolvedValueOnce([]);
mockAppState.currentScreen = "scheduling";
let inputHandler;
mockUseInput.mockImplementation((handler) => {
inputHandler = handler;
});
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Temporary error");
// Retry and succeed
inputHandler("r");
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).not.toContain("Temporary error");
expect(lastFrame()).toContain("No schedules found");
});
test("should persist error state across screen navigation", async () => {
const persistentError = new Error("Configuration error");
mockServices.fetchAllTags.mockRejectedValue(persistentError);
mockAppState.currentScreen = "tagAnalysis";
let inputHandler;
mockUseInput.mockImplementation((handler) => {
inputHandler = handler;
});
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Configuration error");
// Navigate away and back
inputHandler("", { escape: true });
mockAppState.currentScreen = "main";
// Navigate back to tag analysis
inputHandler("t");
mockAppState.currentScreen = "tagAnalysis";
// Error should be saved in screen state
expect(mockAppState.saveScreenState).toHaveBeenCalledWith(
"tagAnalysis",
expect.objectContaining({
error: expect.any(Object),
})
);
});
test("should provide error context and troubleshooting guidance", async () => {
const contextualError = new Error("Shop not found");
contextualError.code = "SHOP_NOT_FOUND";
contextualError.context = {
shopDomain: "invalid-shop.myshopify.com",
suggestion: "Verify your shop domain in configuration",
};
mockServices.fetchAllTags.mockRejectedValue(contextualError);
mockAppState.currentScreen = "tagAnalysis";
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Shop not found");
expect(lastFrame()).toContain("invalid-shop.myshopify.com");
expect(lastFrame()).toContain("Verify your shop domain");
});
});
describe("Critical Error Handling", () => {
test("should handle application crashes gracefully", async () => {
const criticalError = new Error("Critical system error");
criticalError.code = "CRITICAL";
// Mock a critical error that would crash the app
mockServices.getAllSchedules.mockImplementation(() => {
throw criticalError;
});
mockAppState.currentScreen = "scheduling";
// Should not crash the entire application
expect(() => {
render(React.createElement(TuiApplication));
}).not.toThrow();
});
test("should provide safe mode when multiple services fail", async () => {
const systemError = new Error("System failure");
// All services fail
Object.keys(mockServices).forEach((service) => {
mockServices[service].mockRejectedValue(systemError);
});
mockAppState.currentScreen = "main";
const { lastFrame } = render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(lastFrame()).toContain("Safe mode");
expect(lastFrame()).toContain("Limited functionality available");
});
test("should log critical errors for debugging", async () => {
const criticalError = new Error("Memory allocation failed");
criticalError.code = "ENOMEM";
mockServices.fetchAllTags.mockRejectedValue(criticalError);
mockAppState.currentScreen = "tagAnalysis";
// Mock console.error to capture error logging
const consoleSpy = jest
.spyOn(console, "error")
.mockImplementation(() => {});
render(React.createElement(TuiApplication));
await new Promise((resolve) => setTimeout(resolve, 100));
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("Critical error"),
expect.any(Error)
);
consoleSpy.mockRestore();
});
});
});