Skip to content

Commit 60ef3f6

Browse files
update tests
1 parent 55a5877 commit 60ef3f6

5 files changed

Lines changed: 311 additions & 0 deletions

File tree

.DS_Store

6 KB
Binary file not shown.

src/date.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { describe, it, expect } from "vitest";
2+
import { getDate } from "./date.js";
3+
4+
describe("getDate", () => {
5+
it("should return a Date object", () => {
6+
const result = getDate();
7+
expect(result).toBeInstanceOf(Date);
8+
});
9+
10+
it("should return current time", () => {
11+
const before = Date.now();
12+
const result = getDate();
13+
const after = Date.now();
14+
15+
expect(result.getTime()).toBeGreaterThanOrEqual(before);
16+
expect(result.getTime()).toBeLessThanOrEqual(after);
17+
});
18+
});

src/pg/client.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,83 @@ describe("transaction", () => {
159159
);
160160
});
161161
});
162+
163+
describe("createEvent", () => {
164+
it("should execute the correct query", async () => {
165+
const pgClient = {
166+
query: vi.fn<any>(() => Promise.resolve()),
167+
} as any;
168+
const event = {
169+
id: "1",
170+
timestamp: new Date(),
171+
type: "test_event",
172+
data: {
173+
thing1: "something",
174+
},
175+
correlation_id: "abc123",
176+
handler_results: {},
177+
errors: 0,
178+
};
179+
const client = createProcessorClient(pgClient);
180+
await client.transaction(async (txClient) => {
181+
await txClient.createEvent(event);
182+
});
183+
184+
expect(pgClient.query).toHaveBeenCalledTimes(3);
185+
expect(pgClient.query).toHaveBeenCalledWith(
186+
'INSERT INTO "events" (id, timestamp, type, data, correlation_id, handler_results, errors) VALUES ($1, $2, $3, $4, $5, $6, $7)',
187+
[
188+
event.id,
189+
event.timestamp,
190+
event.type,
191+
event.data,
192+
event.correlation_id,
193+
event.handler_results,
194+
event.errors,
195+
],
196+
);
197+
});
198+
});
199+
200+
describe("rollback failure", () => {
201+
it("should throw combined error when both transaction and rollback fail", async () => {
202+
const transactionError = new Error("transaction failed");
203+
const rollbackError = new Error("rollback failed");
204+
let queryCount = 0;
205+
206+
const pgClient = {
207+
query: vi.fn<any>((sql: string) => {
208+
queryCount++;
209+
if (sql === "BEGIN") {
210+
// BEGIN succeeds
211+
return Promise.resolve();
212+
} else if (sql === "ROLLBACK") {
213+
// ROLLBACK fails
214+
return Promise.reject(rollbackError);
215+
} else {
216+
// COMMIT or other operations
217+
return Promise.resolve();
218+
}
219+
}),
220+
} as any;
221+
222+
const client = createProcessorClient(pgClient);
223+
224+
try {
225+
await client.transaction(async () => {
226+
throw transactionError;
227+
});
228+
expect.fail("should have thrown an error");
229+
} catch (error: any) {
230+
expect(error.message).toBe(
231+
"Transaction failed: transaction failed (rollback also failed: rollback failed)",
232+
);
233+
expect(error.cause).toBe(transactionError);
234+
}
235+
236+
expect(pgClient.query).toHaveBeenCalledTimes(2);
237+
expect(pgClient.query).toHaveBeenNthCalledWith(1, "BEGIN");
238+
expect(pgClient.query).toHaveBeenNthCalledWith(2, "ROLLBACK");
239+
});
240+
});
162241
});

src/processor.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,67 @@ describe("processEvents", () => {
633633
type: "evtType1",
634634
});
635635
});
636+
637+
it("should throw and log when onEventMaxErrorsReached hook throws an error", async () => {
638+
const hookError = new Error("hook error");
639+
const logger = {
640+
debug: vi.fn(),
641+
info: vi.fn(),
642+
warn: vi.fn(),
643+
error: vi.fn(),
644+
};
645+
const opts = {
646+
maxErrors: 5,
647+
backoff: vi.fn(),
648+
onEventMaxErrorsReached: vi.fn(() => {
649+
throw hookError;
650+
}),
651+
logger,
652+
};
653+
const errUnprocessable = new ErrorUnprocessableEventHandler(
654+
new Error("err1"),
655+
);
656+
const handlerMap = {
657+
evtType1: {
658+
handler1: vi.fn(() => Promise.reject(errUnprocessable)),
659+
},
660+
};
661+
const evt1: TxOBEvent<keyof typeof handlerMap> = {
662+
type: "evtType1",
663+
id: "1",
664+
timestamp: now,
665+
data: {},
666+
correlation_id: "abc123",
667+
handler_results: {},
668+
errors: 0,
669+
};
670+
const events = [evt1];
671+
mockClient.getEventsToProcess.mockImplementation(() => events);
672+
mockTxClient.getEventByIdForUpdateSkipLocked.mockImplementation((id) => {
673+
return events.find((e) => e.id === id);
674+
});
675+
mockTxClient.updateEvent.mockImplementation(() => {
676+
return Promise.resolve();
677+
});
678+
679+
await processEvents(mockClient, handlerMap, opts);
680+
681+
expect(opts.onEventMaxErrorsReached).toHaveBeenCalledOnce();
682+
expect(logger.error).toHaveBeenCalledWith(
683+
{
684+
eventId: "1",
685+
error: hookError,
686+
},
687+
"error in onEventMaxErrorsReached hook",
688+
);
689+
expect(logger.error).toHaveBeenCalledWith(
690+
{
691+
eventId: "1",
692+
error: hookError,
693+
},
694+
"error processing event",
695+
);
696+
});
636697
});
637698

638699
describe("defaultBackoff", () => {
@@ -644,6 +705,16 @@ describe("defaultBackoff", () => {
644705

645706
expect(diff).lessThanOrEqual(1);
646707
});
708+
709+
it("should cap backoff at maxDelayMs for large error counts", () => {
710+
const maxDelayMs = 1000 * 60; // 60 seconds
711+
const backoff = defaultBackoff(20); // Large error count that would exceed max
712+
const actual = backoff.getTime();
713+
const expected = Date.now() + maxDelayMs;
714+
const diff = Math.abs(actual - expected);
715+
716+
expect(diff).lessThanOrEqual(1);
717+
});
647718
});
648719

649720
describe("Processor", () => {
@@ -682,6 +753,91 @@ describe("Processor", () => {
682753
const diff = Date.now() - start;
683754
expect(diff).toBeLessThan(50);
684755
});
756+
it("should warn when stopping a processor that is not started", async () => {
757+
const logger = {
758+
debug: vi.fn(),
759+
info: vi.fn(),
760+
warn: vi.fn(),
761+
error: vi.fn(),
762+
};
763+
const processor = Processor(() => sleep(1), { sleepTimeMs: 0, logger });
764+
765+
await processor.stop();
766+
767+
expect(logger.warn).toHaveBeenCalledWith(
768+
"cannot stop processor from 'stopped'",
769+
);
770+
});
771+
it("should handle shutdown when processor completes before stop is called", async () => {
772+
let resolve: (() => void) | null = null;
773+
const promise = new Promise<void>((r) => {
774+
resolve = r;
775+
});
776+
777+
const processor = Processor(
778+
() => {
779+
return promise;
780+
},
781+
{ sleepTimeMs: 0 },
782+
);
783+
processor.start();
784+
785+
// Complete the processor's work
786+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
787+
resolve!();
788+
789+
// Wait a bit for the processor to emit shutdownComplete
790+
await sleep(10);
791+
792+
// Now stop should handle the already-completed case
793+
await processor.stop();
794+
});
795+
it("should warn when starting a processor that is already started", () => {
796+
const logger = {
797+
debug: vi.fn(),
798+
info: vi.fn(),
799+
warn: vi.fn(),
800+
error: vi.fn(),
801+
};
802+
const processor = Processor(() => sleep(1000), { sleepTimeMs: 0, logger });
803+
804+
processor.start();
805+
processor.start(); // Try to start again
806+
807+
expect(logger.warn).toHaveBeenCalledWith(
808+
"cannot start processor from 'started'",
809+
);
810+
});
811+
it("should handle non-abort errors and continue processing", async () => {
812+
let calls = 0;
813+
const logger = {
814+
debug: vi.fn(),
815+
info: vi.fn(),
816+
warn: vi.fn(),
817+
error: vi.fn(),
818+
};
819+
const error = new Error("processing error");
820+
821+
const processor = Processor(
822+
() => {
823+
calls++;
824+
if (calls === 1) {
825+
throw error;
826+
}
827+
return Promise.resolve();
828+
},
829+
{ sleepTimeMs: 0, logger },
830+
);
831+
processor.start();
832+
833+
// Wait for the error to be logged and processing to continue
834+
await sleep(1100);
835+
836+
await processor.stop();
837+
838+
expect(logger.error).toHaveBeenCalledWith(error);
839+
expect(calls).toBeGreaterThan(1); // Should continue processing after error
840+
});
685841
});
686842

687843
describe("EventProcessor", () => {

src/retry.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { retryable } from "./retry.js";
3+
4+
describe("retryable", () => {
5+
it("should retry and succeed on second attempt", async () => {
6+
let attempts = 0;
7+
const action = vi.fn(async () => {
8+
attempts++;
9+
if (attempts < 2) {
10+
throw new Error("temporary error");
11+
}
12+
return "success";
13+
});
14+
15+
const result = await retryable(action, {
16+
retries: 3,
17+
minTimeout: 1,
18+
maxTimeout: 10,
19+
factor: 1,
20+
});
21+
22+
expect(result).toBe("success");
23+
expect(action).toHaveBeenCalledTimes(2);
24+
});
25+
26+
it("should exhaust retries and reject", async () => {
27+
const error = new Error("persistent error");
28+
const action = vi.fn(async () => {
29+
throw error;
30+
});
31+
32+
await expect(
33+
retryable(action, {
34+
retries: 2,
35+
minTimeout: 1,
36+
maxTimeout: 10,
37+
factor: 1,
38+
}),
39+
).rejects.toThrow("persistent error");
40+
41+
expect(action).toHaveBeenCalledTimes(3); // initial + 2 retries
42+
});
43+
44+
it("should succeed on first attempt", async () => {
45+
const action = vi.fn(async () => {
46+
return "success";
47+
});
48+
49+
const result = await retryable(action, {
50+
retries: 3,
51+
minTimeout: 1,
52+
maxTimeout: 10,
53+
});
54+
55+
expect(result).toBe("success");
56+
expect(action).toHaveBeenCalledTimes(1);
57+
});
58+
});

0 commit comments

Comments
 (0)