Files
optima/api/tests/unit/withCwRetry.test.ts
T

246 lines
7.8 KiB
TypeScript

import { describe, test, expect, mock } from "bun:test";
import { withCwRetry } from "../../src/modules/cw-utils/withCwRetry";
describe("withCwRetry", () => {
// -------------------------------------------------------------------
// Successful execution
// -------------------------------------------------------------------
test("returns result on first successful call", async () => {
const fn = mock(() => Promise.resolve("ok"));
const result = await withCwRetry(fn);
expect(result).toBe("ok");
expect(fn).toHaveBeenCalledTimes(1);
});
test("returns result when call succeeds after transient failure", async () => {
let attempt = 0;
const fn = mock(() => {
attempt++;
if (attempt === 1) {
const err: any = new Error("timeout");
err.isAxiosError = true;
err.code = "ETIMEDOUT";
return Promise.reject(err);
}
return Promise.resolve("recovered");
});
const result = await withCwRetry(fn, { baseDelayMs: 1 });
expect(result).toBe("recovered");
expect(fn).toHaveBeenCalledTimes(2);
});
// -------------------------------------------------------------------
// Retry on transient errors
// -------------------------------------------------------------------
test("retries on ECONNABORTED", async () => {
let calls = 0;
const fn = mock(() => {
calls++;
if (calls < 3) {
const err: any = new Error("aborted");
err.isAxiosError = true;
err.code = "ECONNABORTED";
return Promise.reject(err);
}
return Promise.resolve("done");
});
const result = await withCwRetry(fn, { maxAttempts: 3, baseDelayMs: 1 });
expect(result).toBe("done");
expect(fn).toHaveBeenCalledTimes(3);
});
test("retries on ECONNRESET", async () => {
let calls = 0;
const fn = mock(() => {
calls++;
if (calls === 1) {
const err: any = new Error("reset");
err.isAxiosError = true;
err.code = "ECONNRESET";
return Promise.reject(err);
}
return Promise.resolve("ok");
});
const result = await withCwRetry(fn, { baseDelayMs: 1 });
expect(result).toBe("ok");
});
test("retries on ECONNREFUSED", async () => {
let calls = 0;
const fn = mock(() => {
calls++;
if (calls === 1) {
const err: any = new Error("refused");
err.isAxiosError = true;
err.code = "ECONNREFUSED";
return Promise.reject(err);
}
return Promise.resolve("ok");
});
const result = await withCwRetry(fn, { baseDelayMs: 1 });
expect(result).toBe("ok");
});
test("retries on ERR_NETWORK", async () => {
let calls = 0;
const fn = mock(() => {
calls++;
if (calls === 1) {
const err: any = new Error("network");
err.isAxiosError = true;
err.code = "ERR_NETWORK";
return Promise.reject(err);
}
return Promise.resolve("ok");
});
const result = await withCwRetry(fn, { baseDelayMs: 1 });
expect(result).toBe("ok");
});
test("retries on ENETUNREACH", async () => {
let calls = 0;
const fn = mock(() => {
calls++;
if (calls === 1) {
const err: any = new Error("unreachable");
err.isAxiosError = true;
err.code = "ENETUNREACH";
return Promise.reject(err);
}
return Promise.resolve("ok");
});
const result = await withCwRetry(fn, { baseDelayMs: 1 });
expect(result).toBe("ok");
});
test("retries on 5xx server errors", async () => {
let calls = 0;
const fn = mock(() => {
calls++;
if (calls === 1) {
const err: any = new Error("server error");
err.isAxiosError = true;
err.response = { status: 502 };
return Promise.reject(err);
}
return Promise.resolve("ok");
});
const result = await withCwRetry(fn, { baseDelayMs: 1 });
expect(result).toBe("ok");
});
test("retries on 500 status", async () => {
let calls = 0;
const fn = mock(() => {
calls++;
if (calls === 1) {
const err: any = new Error("internal");
err.isAxiosError = true;
err.response = { status: 500 };
return Promise.reject(err);
}
return Promise.resolve("ok");
});
const result = await withCwRetry(fn, { baseDelayMs: 1 });
expect(result).toBe("ok");
});
// -------------------------------------------------------------------
// Non-retryable errors
// -------------------------------------------------------------------
test("does not retry on 4xx errors", async () => {
const err: any = new Error("not found");
err.isAxiosError = true;
err.response = { status: 404 };
const fn = mock(() => Promise.reject(err));
await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toThrow();
expect(fn).toHaveBeenCalledTimes(1);
});
test("does not retry on 400 errors", async () => {
const err: any = new Error("bad request");
err.isAxiosError = true;
err.response = { status: 400 };
const fn = mock(() => Promise.reject(err));
await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toThrow();
expect(fn).toHaveBeenCalledTimes(1);
});
test("does not retry on non-Axios errors", async () => {
const fn = mock(() => Promise.reject(new Error("generic")));
await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toThrow(
"generic",
);
expect(fn).toHaveBeenCalledTimes(1);
});
test("does not retry on non-object errors", async () => {
const fn = mock(() => Promise.reject("string error"));
await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toBe(
"string error",
);
expect(fn).toHaveBeenCalledTimes(1);
});
// -------------------------------------------------------------------
// Max attempts exhausted
// -------------------------------------------------------------------
test("throws after maxAttempts exhausted", async () => {
const err: any = new Error("timeout");
err.isAxiosError = true;
err.code = "ETIMEDOUT";
const fn = mock(() => Promise.reject(err));
await expect(
withCwRetry(fn, { maxAttempts: 2, baseDelayMs: 1 }),
).rejects.toThrow();
expect(fn).toHaveBeenCalledTimes(2);
});
test("throws the last error when retries exhausted", async () => {
const err: any = new Error("persistent timeout");
err.isAxiosError = true;
err.code = "ETIMEDOUT";
const fn = mock(() => Promise.reject(err));
try {
await withCwRetry(fn, { maxAttempts: 3, baseDelayMs: 1 });
expect(true).toBe(false); // should not reach
} catch (e: any) {
expect(e.message).toBe("persistent timeout");
}
});
// -------------------------------------------------------------------
// Options
// -------------------------------------------------------------------
test("defaults to 3 maxAttempts", async () => {
const err: any = new Error("timeout");
err.isAxiosError = true;
err.code = "ETIMEDOUT";
const fn = mock(() => Promise.reject(err));
await expect(withCwRetry(fn, { baseDelayMs: 1 })).rejects.toThrow();
expect(fn).toHaveBeenCalledTimes(3);
});
test("accepts custom maxAttempts", async () => {
const err: any = new Error("timeout");
err.isAxiosError = true;
err.code = "ETIMEDOUT";
const fn = mock(() => Promise.reject(err));
await expect(
withCwRetry(fn, { maxAttempts: 5, baseDelayMs: 1 }),
).rejects.toThrow();
expect(fn).toHaveBeenCalledTimes(5);
});
test("maxAttempts of 1 means no retries", async () => {
const err: any = new Error("timeout");
err.isAxiosError = true;
err.code = "ETIMEDOUT";
const fn = mock(() => Promise.reject(err));
await expect(
withCwRetry(fn, { maxAttempts: 1, baseDelayMs: 1 }),
).rejects.toThrow();
expect(fn).toHaveBeenCalledTimes(1);
});
});