feat: restructure sales, add PDF quote generation and WebSocket support
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user