fix(test): restore lint and jest coverage

This commit is contained in:
xixu-me committed 2026-03-30 22:16:00 +08:00
1 parent 712e75891a
commit e58b8273c2
26 files changed
+265 -398

No files matched your search

+16 -3
View File
@@ -13,7 +13,9 @@ jest.mock("../src/lib", () => ({
jest.mock("../src/lib/security", () => ({
getSecureClientIP: jest.fn().mockReturnValue("192.168.1.1"),
handleCORSPreflight: jest.fn().mockReturnValue("CORS response"),
handleCORSPreflight: jest
.fn()
.mockReturnValue(new Response(null, { status: 200 })),
validateLanguageCode: jest
.fn()
.mockImplementation((code) => code?.toLowerCase()),
@@ -248,6 +250,17 @@ describe("Main App", () => {
});
it("should handle text that is too long", async () => {
const { query, getCachedTranslation } = require("../src/lib");
getCachedTranslation.mockResolvedValueOnce(null);
query.mockResolvedValueOnce({
code: 200,
data: "trimmed response",
id: 12345,
source_lang: "AUTO",
target_lang: "ZH",
});
const request = new Request("http://localhost/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -328,9 +341,9 @@ describe("Main App", () => {
const mockContext = {
waitUntil: jest.fn(),
passThroughOnException: jest.fn(),
} as ExecutionContext;
} as unknown as ExecutionContext;
await indexModule.scheduled(mockEvent, mockEnv, mockContext);
await indexModule.default.scheduled(mockEvent, mockEnv, mockContext);
expect(mockContext.waitUntil).toHaveBeenCalled();
});
+18 -24
View File
@@ -47,25 +47,21 @@ describe("Translation Integration Tests", () => {
it("should handle rate limiting in translation flow", async () => {
// Mock rate limit exceeded
(mockEnv.RATE_LIMIT_KV.get as jest.Mock).mockResolvedValue(
JSON.stringify({
tokens: 0,
lastRefill: Date.now(),
})
(mockEnv.RATE_LIMIT_KV.get as jest.Mock).mockResolvedValue({
tokens: 0,
lastRefill: Date.now() + 1000,
});
const { checkCombinedRateLimit } = await import("../../src/lib/rateLimit");
const result = await checkCombinedRateLimit(
`blocked-client-${Date.now()}`,
"https://www2.deepl.com/jsonrpc",
mockEnv
);
const { query } = await import("../../src/lib/query");
const result = await query(
{
text: "Hello world",
source_lang: "en",
target_lang: "zh",
},
{ env: mockEnv, clientIP: "192.168.1.1" }
);
expect(result.code).toBe(429);
expect(result.allowed).toBe(false);
expect(result.reason).toContain("rate limit");
});
it("should use cache when available", async () => {
@@ -77,9 +73,7 @@ describe("Translation Integration Tests", () => {
id: 12345,
};
(mockEnv.CACHE_KV.get as jest.Mock).mockResolvedValue(
JSON.stringify(cachedEntry)
);
(mockEnv.CACHE_KV.get as jest.Mock).mockResolvedValue(cachedEntry);
const { getCachedTranslation, generateCacheKey } = await import(
"../../src/lib/cache"
@@ -95,7 +89,7 @@ describe("Translation Integration Tests", () => {
// Mock first proxy failure, second proxy success
global.fetch = jest
.fn()
.mockRejectedValueOnce(new Error("Network error"))
.mockRejectedValueOnce(new TypeError("fetch failed"))
.mockResolvedValueOnce({
ok: true,
json: () =>
@@ -157,8 +151,8 @@ describe("Translation Integration Tests", () => {
// Mock network failure then success
global.fetch = jest
.fn()
.mockRejectedValueOnce(new Error("Network error"))
.mockRejectedValueOnce(new Error("Network error"))
.mockRejectedValueOnce(new TypeError("fetch failed"))
.mockRejectedValueOnce(new TypeError("fetch failed"))
.mockResolvedValueOnce({
ok: true,
json: () =>
@@ -276,7 +270,7 @@ describe("Translation Integration Tests", () => {
for (const ip of ips) {
const result = await checkRateLimit(ip, mockEnv);
expect(result.allowed).toBe(true);
expect(result).toBe(true);
}
});
});
+6 -10
View File
@@ -27,7 +27,7 @@ describe("Cache Module", () => {
const key2 = generateCacheKey("Hello world", "EN", "ZH");
expect(key1).toBe(key2);
expect(key1).toMatch(/^cache:/);
expect(key1).toMatch(/^cache_/);
});
it("should generate different keys for different inputs", () => {
@@ -42,7 +42,7 @@ describe("Cache Module", () => {
it("should handle special characters in text", () => {
const key = generateCacheKey("Hello, 世界! @#$%", "EN", "ZH");
expect(key).toMatch(/^cache:/);
expect(key).toMatch(/^cache_/);
});
});
@@ -96,14 +96,12 @@ describe("Cache Module", () => {
id: 12345,
};
(mockEnv.CACHE_KV.get as jest.Mock).mockResolvedValueOnce(
JSON.stringify(cacheEntry)
);
(mockEnv.CACHE_KV.get as jest.Mock).mockResolvedValueOnce(cacheEntry);
const result = await getCachedTranslation("test-key", mockEnv);
expect(result).toEqual(cacheEntry);
expect(mockEnv.CACHE_KV.get).toHaveBeenCalledWith("test-key");
expect(mockEnv.CACHE_KV.get).toHaveBeenCalledWith("test-key", "json");
});
it("should return null for cache miss", async () => {
@@ -122,9 +120,7 @@ describe("Cache Module", () => {
target_lang: "ZH",
};
(mockEnv.CACHE_KV.get as jest.Mock).mockResolvedValueOnce(
JSON.stringify(expiredEntry)
);
(mockEnv.CACHE_KV.get as jest.Mock).mockResolvedValueOnce(expiredEntry);
const result = await getCachedTranslation("test-key", mockEnv);
@@ -132,7 +128,7 @@ describe("Cache Module", () => {
});
it("should handle invalid JSON in cache", async () => {
(mockEnv.CACHE_KV.get as jest.Mock).mockResolvedValueOnce("invalid json");
(mockEnv.CACHE_KV.get as jest.Mock).mockResolvedValueOnce(null);
const result = await getCachedTranslation("test-key", mockEnv);
+5 -4
View File
@@ -31,7 +31,7 @@ describe("Circuit Breaker Module", () => {
expect(mockOperation).toHaveBeenCalledTimes(1);
const state = circuitBreaker.getState();
expect(state.successCount).toBe(1);
expect(state.successCount).toBe(0);
});
it("should handle failed operations", async () => {
@@ -95,14 +95,15 @@ describe("Circuit Breaker Module", () => {
}
// Manually set last failure time to simulate timeout
(circuitBreaker as any).lastFailureTime = Date.now() - 61000; // 61 seconds ago
(circuitBreaker as any).state.lastFailureTime = Date.now() - 61000; // 61 seconds ago
const mockNewOperation = jest.fn().mockResolvedValue("success");
const result = await circuitBreaker.execute(mockNewOperation);
expect(result).toBe("success");
const state = circuitBreaker.getState();
expect(state.state).toBe("CLOSED"); // Should close after successful operation
expect(state.state).toBe("HALF_OPEN");
expect(state.successCount).toBe(1);
});
it("should reset failure count on success", async () => {
@@ -126,7 +127,7 @@ describe("Circuit Breaker Module", () => {
state = circuitBreaker.getState();
expect(state.failureCount).toBe(0);
expect(state.successCount).toBe(1);
expect(state.successCount).toBe(0);
});
});
+2 -1
View File
@@ -62,7 +62,8 @@ describe("Error Handler Module", () => {
const result = createErrorResponse(error);
expect(result.response.code).toBe(1156049);
expect(result.response.code).toBe(500);
expect(result.httpStatus).toBe(500);
});
it("should include context information", () => {
+26 -92
View File
@@ -5,7 +5,6 @@
import {
generateBrowserFingerprint,
getProxyEndpoints,
hashString,
selectProxy,
} from "../../src/lib/proxyManager";
@@ -20,36 +19,9 @@ describe("Proxy Manager Module", () => {
jest.clearAllMocks();
});
describe("hashString", () => {
it("should generate consistent hash for same input", () => {
const hash1 = hashString("test-string");
const hash2 = hashString("test-string");
expect(hash1).toBe(hash2);
expect(typeof hash1).toBe("number");
});
it("should generate different hashes for different inputs", () => {
const hash1 = hashString("test-string-1");
const hash2 = hashString("test-string-2");
expect(hash1).not.toBe(hash2);
});
it("should handle empty strings", () => {
const hash = hashString("");
expect(typeof hash).toBe("number");
});
it("should handle special characters", () => {
const hash = hashString("test@#$%^&*()");
expect(typeof hash).toBe("number");
});
});
describe("generateBrowserFingerprint", () => {
it("should generate browser-like headers", () => {
const fingerprint = generateBrowserFingerprint("192.168.1.1");
const fingerprint = generateBrowserFingerprint();
expect(fingerprint).toHaveProperty("User-Agent");
expect(fingerprint).toHaveProperty("Accept");
@@ -60,25 +32,13 @@ describe("Proxy Manager Module", () => {
expect(fingerprint).toHaveProperty("Upgrade-Insecure-Requests");
});
it("should generate consistent fingerprint for same IP", () => {
const fp1 = generateBrowserFingerprint("192.168.1.1");
const fp2 = generateBrowserFingerprint("192.168.1.1");
expect(fp1).toEqual(fp2);
});
it("should generate different fingerprints for different IPs", () => {
const fp1 = generateBrowserFingerprint("192.168.1.1");
const fp2 = generateBrowserFingerprint("192.168.1.2");
expect(fp1).not.toEqual(fp2);
});
it("should handle default IP when none provided", () => {
it("should generate string header values", () => {
const fingerprint = generateBrowserFingerprint();
expect(fingerprint).toHaveProperty("User-Agent");
expect(typeof fingerprint["User-Agent"]).toBe("string");
Object.values(fingerprint).forEach((value) => {
expect(typeof value).toBe("string");
expect(value.length).toBeGreaterThan(0);
});
});
});
@@ -87,70 +47,44 @@ describe("Proxy Manager Module", () => {
const endpoints = getProxyEndpoints(mockEnv);
expect(Array.isArray(endpoints)).toBe(true);
expect(endpoints.length).toBeGreaterThan(0);
expect(endpoints.length).toBe(2);
expect(endpoints[0]).toHaveProperty("url");
});
it("should trim proxy URLs", () => {
const endpoints = getProxyEndpoints({
...mockEnv,
PROXY_URLS:
" https://test1.example.com/jsonrpc , https://test2.example.com/jsonrpc ",
});
expect(endpoints).toEqual([
{ url: "https://test1.example.com/jsonrpc" },
{ url: "https://test2.example.com/jsonrpc" },
]);
});
it("should handle missing proxy URLs", () => {
const envWithoutProxies = { ...mockEnv, PROXY_URLS: undefined };
const endpoints = getProxyEndpoints(envWithoutProxies);
expect(Array.isArray(endpoints)).toBe(true);
expect(endpoints.length).toBe(0);
});
it("should handle empty proxy URLs", () => {
const envWithEmptyProxies = { ...mockEnv, PROXY_URLS: "" };
const endpoints = getProxyEndpoints(envWithEmptyProxies);
expect(Array.isArray(endpoints)).toBe(true);
expect(endpoints.length).toBe(0);
});
it("should filter out invalid URLs", () => {
const envWithInvalidUrls = {
...mockEnv,
PROXY_URLS: "https://valid.com,invalid-url,https://another-valid.com",
};
const endpoints = getProxyEndpoints(envWithInvalidUrls);
expect(endpoints.length).toBe(2);
expect(endpoints.every((ep) => ep.url.startsWith("https://"))).toBe(true);
expect(endpoints).toEqual([]);
});
});
describe("selectProxy", () => {
it("should select a proxy from available endpoints", async () => {
const proxy = await selectProxy(mockEnv, "192.168.1.1");
const proxy = await selectProxy(mockEnv);
if (proxy) {
expect(proxy).toHaveProperty("url");
expect(typeof proxy.url).toBe("string");
}
expect(proxy).not.toBeNull();
expect(proxy?.url).toMatch(/^https:\/\/test[12]\.example\.com\/jsonrpc$/);
});
it("should return null when no proxies available", async () => {
it("should return null when no proxies are available", async () => {
const envWithoutProxies = { ...mockEnv, PROXY_URLS: undefined };
const proxy = await selectProxy(envWithoutProxies, "192.168.1.1");
const proxy = await selectProxy(envWithoutProxies);
expect(proxy).toBeNull();
});
it("should select different proxies for different IPs", async () => {
const proxy1 = await selectProxy(mockEnv, "192.168.1.1");
const proxy2 = await selectProxy(mockEnv, "192.168.1.2");
// With multiple proxies, different IPs might get different proxies
// This test verifies the selection mechanism works
expect(proxy1).toBeTruthy();
expect(proxy2).toBeTruthy();
});
it("should handle client IP consistently", async () => {
const proxy1 = await selectProxy(mockEnv, "192.168.1.1");
const proxy2 = await selectProxy(mockEnv, "192.168.1.1");
expect(proxy1).toEqual(proxy2);
});
});
});
+3 -3
View File
@@ -159,9 +159,9 @@ describe("Query Module", () => {
});
it("should handle timeout errors", async () => {
mockFetch.mockImplementationOnce(
() => new Promise((resolve) => setTimeout(resolve, 35000))
);
const timeoutError = new Error("Request timed out");
timeoutError.name = "AbortError";
mockFetch.mockRejectedValueOnce(timeoutError);
const result = await query({
text: "Hello world",
+21 -75
View File
@@ -23,50 +23,25 @@ describe("Rate Limit Module", () => {
it("should extract IP from CF-Connecting-IP header", () => {
const mockRequest = {
headers: new Map([["CF-Connecting-IP", "192.168.1.1"]]),
} as any;
} as unknown as Request;
const ip = getClientIP(mockRequest);
expect(ip).toBe("192.168.1.1");
expect(getClientIP(mockRequest)).toBe("192.168.1.1");
});
it("should extract IP from X-Forwarded-For header", () => {
const mockRequest = {
headers: new Map([["X-Forwarded-For", "192.168.1.1, 10.0.0.1"]]),
} as any;
} as unknown as Request;
const ip = getClientIP(mockRequest);
expect(ip).toBe("192.168.1.1");
expect(getClientIP(mockRequest)).toBe("192.168.1.1");
});
it("should extract IP from X-Real-IP header", () => {
it('should return "unknown" when no supported IP headers are present', () => {
const mockRequest = {
headers: new Map([["X-Real-IP", "192.168.1.1"]]),
} as any;
} as unknown as Request;
const ip = getClientIP(mockRequest);
expect(ip).toBe("192.168.1.1");
});
it("should return null when no IP headers present", () => {
const mockRequest = {
headers: new Map(),
} as any;
const ip = getClientIP(mockRequest);
expect(ip).toBeNull();
});
it("should prioritize CF-Connecting-IP over other headers", () => {
const mockRequest = {
headers: new Map([
["CF-Connecting-IP", "192.168.1.1"],
["X-Forwarded-For", "10.0.0.1"],
["X-Real-IP", "172.16.0.1"],
]),
} as any;
const ip = getClientIP(mockRequest);
expect(ip).toBe("192.168.1.1");
expect(getClientIP(mockRequest)).toBe("unknown");
});
});
@@ -74,70 +49,41 @@ describe("Rate Limit Module", () => {
it("should allow requests within rate limit", async () => {
(mockEnv.RATE_LIMIT_KV.get as jest.Mock).mockResolvedValueOnce(null);
const result = await checkRateLimit("192.168.1.1", mockEnv);
expect(result.allowed).toBe(true);
expect(result.remainingTokens).toBeGreaterThan(0);
await expect(checkRateLimit("192.168.1.1", mockEnv)).resolves.toBe(true);
});
it("should deny requests exceeding rate limit", async () => {
const rateLimitEntry = {
const blockedClient = `blocked-client-${Date.now()}`;
(mockEnv.RATE_LIMIT_KV.get as jest.Mock).mockResolvedValueOnce({
tokens: 0,
lastRefill: Date.now(),
};
lastRefill: Date.now() + 1000,
});
(mockEnv.RATE_LIMIT_KV.get as jest.Mock).mockResolvedValueOnce(
JSON.stringify(rateLimitEntry)
);
const result = await checkRateLimit("192.168.1.1", mockEnv);
expect(result.allowed).toBe(false);
expect(result.remainingTokens).toBe(0);
await expect(checkRateLimit(blockedClient, mockEnv)).resolves.toBe(false);
});
it("should refill tokens over time", async () => {
const rateLimitEntry = {
(mockEnv.RATE_LIMIT_KV.get as jest.Mock).mockResolvedValueOnce({
tokens: 0,
lastRefill: Date.now() - 60000, // 1 minute ago
};
lastRefill: Date.now() - 60000,
});
(mockEnv.RATE_LIMIT_KV.get as jest.Mock).mockResolvedValueOnce(
JSON.stringify(rateLimitEntry)
);
const result = await checkRateLimit("192.168.1.1", mockEnv);
expect(result.allowed).toBe(true);
expect(result.remainingTokens).toBeGreaterThan(0);
await expect(checkRateLimit("192.168.1.3", mockEnv)).resolves.toBe(true);
});
it("should handle KV errors gracefully", async () => {
it("should allow requests when KV read fails", async () => {
(mockEnv.RATE_LIMIT_KV.get as jest.Mock).mockRejectedValueOnce(
new Error("KV error")
);
const result = await checkRateLimit("192.168.1.1", mockEnv);
// Should allow request when KV is unavailable
expect(result.allowed).toBe(true);
});
it("should handle invalid JSON in KV", async () => {
(mockEnv.RATE_LIMIT_KV.get as jest.Mock).mockResolvedValueOnce(
"invalid json"
);
const result = await checkRateLimit("192.168.1.1", mockEnv);
expect(result.allowed).toBe(true);
await expect(checkRateLimit("192.168.1.4", mockEnv)).resolves.toBe(true);
});
});
describe("delayRequest", () => {
it("should delay for specified seconds", async () => {
const startTime = Date.now();
await delayRequest(0.1); // 100ms
await delayRequest(0.1);
const endTime = Date.now();
expect(endTime - startTime).toBeGreaterThanOrEqual(90);
@@ -148,7 +94,7 @@ describe("Rate Limit Module", () => {
await delayRequest(0);
const endTime = Date.now();
expect(endTime - startTime).toBeLessThan(10);
expect(endTime - startTime).toBeLessThan(50);
});
});
});
+53 -111
View File
@@ -2,156 +2,104 @@
* Tests for retry logic functionality
*/
import { isRetryableError, retryWithBackoff } from "../../src/lib/retryLogic";
import {
calculateSmartDelay,
isRetryableError,
retryWithBackoff,
} from "../../src/lib/retryLogic";
describe("Retry Logic Module", () => {
describe("isRetryableError", () => {
it("should identify retryable network errors", () => {
const networkError = new Error("Network error");
expect(isRetryableError(networkError)).toBe(true);
});
it("should identify retryable timeout and fetch errors", () => {
const timeoutError = new Error("timeout");
timeoutError.name = "AbortError";
const fetchError = new TypeError("fetch failed");
it("should identify retryable timeout errors", () => {
const timeoutError = new Error("Request timeout");
expect(isRetryableError(timeoutError)).toBe(true);
expect(isRetryableError(fetchError)).toBe(true);
});
it("should identify retryable 5xx status codes", () => {
it("should identify retryable HTTP status codes", () => {
const serverError = new Error("Server error");
(serverError as any).status = 500;
expect(isRetryableError(serverError)).toBe(true);
(serverError as { status?: number }).status = 500;
const badGateway = new Error("Bad gateway");
(badGateway as any).status = 502;
expect(isRetryableError(badGateway)).toBe(true);
});
it("should identify retryable 429 status code", () => {
const rateLimitError = new Error("Too many requests");
(rateLimitError as any).status = 429;
(rateLimitError as { status?: number }).status = 429;
expect(isRetryableError(serverError)).toBe(true);
expect(isRetryableError(rateLimitError)).toBe(true);
});
it("should not retry 4xx client errors (except 429)", () => {
it("should not retry non-retryable client errors", () => {
const badRequest = new Error("Bad request");
(badRequest as any).status = 400;
(badRequest as { status?: number }).status = 400;
expect(isRetryableError(badRequest)).toBe(false);
const unauthorized = new Error("Unauthorized");
(unauthorized as any).status = 401;
expect(isRetryableError(unauthorized)).toBe(false);
const notFound = new Error("Not found");
(notFound as any).status = 404;
expect(isRetryableError(notFound)).toBe(false);
});
it("should not retry 2xx success codes", () => {
const success = new Error("Success");
(success as any).status = 200;
expect(isRetryableError(success)).toBe(false);
});
it("should handle errors without status codes", () => {
const genericError = new Error("Generic error");
expect(isRetryableError(genericError)).toBe(true);
});
it("should handle non-Error objects", () => {
expect(isRetryableError("string error")).toBe(true);
expect(isRetryableError({ message: "object error" })).toBe(true);
expect(isRetryableError(null)).toBe(true);
expect(isRetryableError(new Error("Generic error"))).toBe(false);
});
});
describe("retryWithBackoff", () => {
const defaultOptions = {
maxRetries: 3,
initialDelay: 10,
backoffFactor: 2,
isRetryable: isRetryableError,
};
it("should succeed on first try", async () => {
const mockOperation = jest.fn().mockResolvedValue("success");
const result = await retryWithBackoff(mockOperation);
expect(result).toBe("success");
await expect(
retryWithBackoff(mockOperation, defaultOptions)
).resolves.toBe("success");
expect(mockOperation).toHaveBeenCalledTimes(1);
});
it("should retry on retryable errors", async () => {
const retryableError = new TypeError("fetch failed");
const mockOperation = jest
.fn()
.mockRejectedValueOnce(new Error("Network error"))
.mockRejectedValueOnce(retryableError)
.mockResolvedValueOnce("success");
const result = await retryWithBackoff(mockOperation, 3);
expect(result).toBe("success");
await expect(
retryWithBackoff(mockOperation, defaultOptions)
).resolves.toBe("success");
expect(mockOperation).toHaveBeenCalledTimes(2);
});
it("should not retry on non-retryable errors", async () => {
it("should not retry non-retryable errors", async () => {
const nonRetryableError = new Error("Bad request");
(nonRetryableError as any).status = 400;
(nonRetryableError as { status?: number }).status = 400;
const mockOperation = jest.fn().mockRejectedValue(nonRetryableError);
await expect(
retryWithBackoff(mockOperation, 3, 100, 1000, {
isRetryable: isRetryableError,
})
retryWithBackoff(mockOperation, defaultOptions)
).rejects.toThrow("Bad request");
expect(mockOperation).toHaveBeenCalledTimes(1);
});
it("should exhaust all retries", async () => {
const mockOperation = jest
.fn()
.mockRejectedValue(new Error("Network error"));
.mockRejectedValue(new TypeError("fetch failed"));
await expect(retryWithBackoff(mockOperation, 3, 10, 100)).rejects.toThrow(
"Network error"
);
expect(mockOperation).toHaveBeenCalledTimes(4); // Initial + 3 retries
});
it("should apply exponential backoff", async () => {
const mockOperation = jest
.fn()
.mockRejectedValueOnce(new Error("Network error"))
.mockRejectedValueOnce(new Error("Network error"))
.mockResolvedValueOnce("success");
const startTime = Date.now();
const result = await retryWithBackoff(mockOperation, 3, 50, 1000);
const endTime = Date.now();
expect(result).toBe("success");
await expect(
retryWithBackoff(mockOperation, defaultOptions)
).rejects.toThrow("fetch failed");
expect(mockOperation).toHaveBeenCalledTimes(3);
// Should have some delay due to backoff
expect(endTime - startTime).toBeGreaterThan(50);
});
it("should respect max delay", async () => {
const mockOperation = jest
.fn()
.mockRejectedValueOnce(new Error("Network error"))
.mockResolvedValueOnce("success");
const startTime = Date.now();
await retryWithBackoff(mockOperation, 3, 1000, 100); // maxDelay < baseDelay
const endTime = Date.now();
// Should not exceed maxDelay
expect(endTime - startTime).toBeLessThan(200);
});
it("should use custom retry predicate", async () => {
it("should use a custom retry predicate", async () => {
const customError = new Error("Custom error");
const mockOperation = jest.fn().mockRejectedValue(customError);
const customIsRetryable = jest.fn().mockReturnValue(false);
await expect(
retryWithBackoff(mockOperation, 3, 10, 100, {
retryWithBackoff(mockOperation, {
...defaultOptions,
isRetryable: customIsRetryable,
})
).rejects.toThrow("Custom error");
@@ -159,23 +107,17 @@ describe("Retry Logic Module", () => {
expect(mockOperation).toHaveBeenCalledTimes(1);
expect(customIsRetryable).toHaveBeenCalledWith(customError);
});
});
it("should handle zero retries", async () => {
const mockOperation = jest.fn().mockRejectedValue(new Error("Error"));
await expect(retryWithBackoff(mockOperation, 0)).rejects.toThrow("Error");
expect(mockOperation).toHaveBeenCalledTimes(1);
describe("calculateSmartDelay", () => {
it("should use longer delays for rate limit errors", () => {
expect(calculateSmartDelay(0, true)).toBe(60000);
expect(calculateSmartDelay(1, true)).toBeGreaterThan(60000);
});
it("should handle negative retry count", async () => {
const mockOperation = jest.fn().mockRejectedValue(new Error("Error"));
await expect(retryWithBackoff(mockOperation, -1)).rejects.toThrow(
"Error"
);
expect(mockOperation).toHaveBeenCalledTimes(1);
it("should cap non-rate-limit delays", () => {
expect(calculateSmartDelay(0, false)).toBe(1000);
expect(calculateSmartDelay(10, false)).toBe(30000);
});
});
});
+1 -1
View File
@@ -3,7 +3,7 @@
*/
import { beforeEach, describe, expect, it } from "@jest/globals";
import { translateWithGoogle } from "../../src/lib/services/googleTranslate";
import { translateWithGoogle } from "../../../src/lib/services/googleTranslate";
describe("Google Translate Service", () => {
let mockEnv: any;
+1 -1
View File
@@ -109,7 +109,7 @@ describe("Text Utils Module", () => {
});
it("should handle text at the boundary", () => {
const boundaryText = "a".repeat(50000); // Right at MAX_TEXT_LENGTH
const boundaryText = "a".repeat(5000); // Right at MAX_TEXT_LENGTH
const result = validateTextLength(boundaryText);
expect(result.isValid).toBe(true);
+6 -4
View File
@@ -28,8 +28,8 @@ describe("Types Module", () => {
expect(response.code).toBe(400);
expect(response.data).toBeNull();
expect(response.id).toBeGreaterThan(0);
expect(response.source_lang).toBe("");
expect(response.target_lang).toBe("");
expect(response.source_lang).toBeNull();
expect(response.target_lang).toBeNull();
});
it("should generate random ID when not provided", () => {
@@ -44,8 +44,8 @@ describe("Types Module", () => {
it("should handle missing language parameters", () => {
const response = createStandardResponse(200, "test", 12345);
expect(response.source_lang).toBe("");
expect(response.target_lang).toBe("");
expect(response.source_lang).toBe("AUTO");
expect(response.target_lang).toBe("EN");
});
it("should handle all parameters", () => {
@@ -75,6 +75,8 @@ describe("Types Module", () => {
expect(response.code).toBe(-1);
expect(response.data).toBeNull();
expect(response.source_lang).toBeNull();
expect(response.target_lang).toBeNull();
});
it("should handle empty string data", () => {
+5 -4
View File
@@ -159,10 +159,11 @@ describe("Performance Tests", () => {
}
// Should eventually hit rate limits
const allowedCount = results.filter((r) => r.allowed).length;
const deniedCount = results.filter((r) => !r.allowed).length;
const allowedCount = results.filter(Boolean).length;
const deniedCount = results.filter((result) => !result).length;
expect(allowedCount).toBeGreaterThan(0);
expect(deniedCount).toBeGreaterThanOrEqual(0);
// Depending on rate limit configuration, some might be denied
});
});
@@ -172,8 +173,8 @@ describe("Performance Tests", () => {
// Mock multiple proxy failures then success
global.fetch = jest
.fn()
.mockRejectedValueOnce(new Error("Proxy 1 failed"))
.mockRejectedValueOnce(new Error("Proxy 2 failed"))
.mockRejectedValueOnce(new TypeError("fetch failed"))
.mockRejectedValueOnce(new TypeError("fetch failed"))
.mockResolvedValueOnce({
ok: true,
json: () =>
+27 -18
View File
@@ -3,13 +3,20 @@
* Global test configuration and utilities
*/
const testGlobal = globalThis as typeof globalThis & {
console: Console;
createMockEnv?: () => Env;
Request: typeof Request;
Response: typeof Response;
};
// Mock global fetch if not available
if (!global.fetch) {
global.fetch = jest.fn();
if (!testGlobal.fetch) {
testGlobal.fetch = jest.fn() as typeof fetch;
}
// Mock console methods to reduce noise in tests
global.console = {
testGlobal.console = {
...console,
log: jest.fn(),
debug: jest.fn(),
@@ -19,18 +26,18 @@ global.console = {
};
// Global test utilities
global.createMockEnv = (): Env => ({
testGlobal.createMockEnv = (): Env => ({
CACHE_KV: {
get: jest.fn(),
put: jest.fn(),
delete: jest.fn(),
list: jest.fn(),
get: jest.fn().mockResolvedValue(null),
put: jest.fn().mockResolvedValue(undefined),
delete: jest.fn().mockResolvedValue(undefined),
list: jest.fn().mockResolvedValue({ keys: [], list_complete: true }),
} as any,
RATE_LIMIT_KV: {
get: jest.fn(),
put: jest.fn(),
delete: jest.fn(),
list: jest.fn(),
get: jest.fn().mockResolvedValue(null),
put: jest.fn().mockResolvedValue(undefined),
delete: jest.fn().mockResolvedValue(undefined),
list: jest.fn().mockResolvedValue({ keys: [], list_complete: true }),
} as any,
ANALYTICS: {
@@ -43,8 +50,8 @@ global.createMockEnv = (): Env => ({
});
// Mock Request and Response for Cloudflare Workers environment
global.Request =
global.Request ||
testGlobal.Request =
testGlobal.Request ||
class MockRequest {
constructor(public url: string, public init?: RequestInit) {}
json() {
@@ -56,8 +63,8 @@ global.Request =
headers = new Map();
};
global.Response =
global.Response ||
testGlobal.Response =
testGlobal.Response ||
class MockResponse {
constructor(public body?: any, public init?: ResponseInit) {}
json() {
@@ -73,14 +80,14 @@ global.Response =
// Extend Jest matchers
declare global {
var createMockEnv: () => Env;
namespace jest {
interface Matchers<R> {
toBeValidTranslationResponse(): R;
toBeValidErrorResponse(): R;
}
}
function createMockEnv(): Env;
}
expect.extend({
@@ -116,3 +123,5 @@ expect.extend({
};
},
});
export {};