diff --git a/src/__tests__/errors.test.ts b/src/__tests__/errors.test.ts new file mode 100644 index 0000000..7d42e79 --- /dev/null +++ b/src/__tests__/errors.test.ts @@ -0,0 +1,199 @@ +import { describe, it, expect } from "vitest"; +import { isTransientError } from "../utils/errors.js"; + +describe("isTransientError", () => { + describe("null/undefined/empty input", () => { + it("returns false for null", () => { + expect(isTransientError(null)).toBe(false); + }); + + it("returns false for undefined", () => { + expect(isTransientError(undefined)).toBe(false); + }); + + it("returns false for empty object", () => { + expect(isTransientError({})).toBe(false); + }); + }); + + describe("error codes from TRANSIENT_ERRORS set", () => { + it("returns true for ECONNRESET", () => { + expect(isTransientError({ code: "ECONNRESET" })).toBe(true); + }); + + it("returns true for ECONNREFUSED", () => { + expect(isTransientError({ code: "ECONNREFUSED" })).toBe(true); + }); + + it("returns true for EPIPE", () => { + expect(isTransientError({ code: "EPIPE" })).toBe(true); + }); + + it("returns true for ETIMEDOUT", () => { + expect(isTransientError({ code: "ETIMEDOUT" })).toBe(true); + }); + + it("returns true for CONNECTION_LOST", () => { + expect(isTransientError({ code: "CONNECTION_LOST" })).toBe(true); + }); + + it("returns true for 57P01 (admin_shutdown)", () => { + expect(isTransientError({ code: "57P01" })).toBe(true); + }); + + it("returns true for 57P02 (crash_shutdown)", () => { + expect(isTransientError({ code: "57P02" })).toBe(true); + }); + + it("returns true for 57P03 (cannot_connect_now)", () => { + expect(isTransientError({ code: "57P03" })).toBe(true); + }); + + it("returns true for 08006 (connection_failure)", () => { + expect(isTransientError({ code: "08006" })).toBe(true); + }); + + it("returns true for 08003 (connection_does_not_exist)", () => { + expect(isTransientError({ code: "08003" })).toBe(true); + }); + + it("returns true for 08001 (sqlclient_unable_to_establish_sqlconnection)", () => { + expect(isTransientError({ code: "08001" })).toBe(true); + }); + }); + + describe("message substring matching", () => { + it("returns true for 'no available server'", () => { + expect(isTransientError({ message: "no available server" })).toBe(true); + }); + + it("returns true for 'connection terminated'", () => { + expect(isTransientError({ message: "connection terminated unexpectedly" })).toBe(true); + }); + + it("returns true for 'connection refused'", () => { + expect(isTransientError({ message: "connection refused by server" })).toBe(true); + }); + + it("returns true for 'server closed the connection'", () => { + expect(isTransientError({ message: "server closed the connection unexpectedly" })).toBe(true); + }); + + it("returns true for 'timeout expired'", () => { + expect(isTransientError({ message: "timeout expired waiting for connection" })).toBe(true); + }); + }); + + describe("case-insensitive message matching", () => { + it("returns true for 'No Available Server' (mixed case)", () => { + expect(isTransientError({ message: "No Available Server" })).toBe(true); + }); + + it("returns true for 'CONNECTION TERMINATED' (uppercase)", () => { + expect(isTransientError({ message: "CONNECTION TERMINATED" })).toBe(true); + }); + + it("returns true for 'Connection Refused' (title case)", () => { + expect(isTransientError({ message: "Connection Refused" })).toBe(true); + }); + + it("returns true for 'SERVER CLOSED THE CONNECTION' (uppercase)", () => { + expect(isTransientError({ message: "SERVER CLOSED THE CONNECTION" })).toBe(true); + }); + + it("returns true for 'Timeout Expired' (title case)", () => { + expect(isTransientError({ message: "Timeout Expired" })).toBe(true); + }); + }); + + describe("non-transient errors", () => { + it("returns false for syntax error", () => { + expect(isTransientError({ + code: "42601", + message: "syntax error at or near SELECT" + })).toBe(false); + }); + + it("returns false for unique constraint violation", () => { + expect(isTransientError({ + code: "23505", + message: "duplicate key value violates unique constraint" + })).toBe(false); + }); + + it("returns false for foreign key violation", () => { + expect(isTransientError({ + code: "23503", + message: "foreign key constraint violation" + })).toBe(false); + }); + + it("returns false for not null violation", () => { + expect(isTransientError({ + code: "23502", + message: "null value in column violates not-null constraint" + })).toBe(false); + }); + + it("returns false for permission denied", () => { + expect(isTransientError({ + code: "42501", + message: "permission denied for table users" + })).toBe(false); + }); + }); + + describe("unrelated codes and messages", () => { + it("returns false for unrelated error code", () => { + expect(isTransientError({ code: "UNKNOWN_ERROR" })).toBe(false); + }); + + it("returns false for unrelated error message", () => { + expect(isTransientError({ message: "Something went wrong" })).toBe(false); + }); + + it("returns false for generic database error", () => { + expect(isTransientError({ + code: "P0001", + message: "Database operation failed" + })).toBe(false); + }); + + it("returns false for application error", () => { + expect(isTransientError({ + message: "Invalid user input" + })).toBe(false); + }); + }); + + describe("edge cases", () => { + it("returns true when both code and message match", () => { + expect(isTransientError({ + code: "ECONNRESET", + message: "connection terminated" + })).toBe(true); + }); + + it("returns true when only code matches", () => { + expect(isTransientError({ + code: "ETIMEDOUT", + message: "some other message" + })).toBe(true); + }); + + it("returns true when only message matches", () => { + expect(isTransientError({ + code: "SOME_CODE", + message: "no available server to connect" + })).toBe(true); + }); + + it("returns false for error with only unrelated code", () => { + expect(isTransientError({ code: "NOTFOUND" })).toBe(false); + }); + + it("returns false for error with empty message", () => { + expect(isTransientError({ message: "" })).toBe(false); + }); + }); +});