refactor: eliminate all catch(err: any) with proper unknown typing + type email transport
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 19m10s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 19m10s
- Replace all catch(err: any) with catch(err: unknown) across 8 source files - Add errorMessage() and errorCode() helpers for safe error property access - Type nodemailer transport config as SMTPTransport.Options (was any) - Type health endpoint databaseStatus (was any) - Type convert route margin param (was any) - Change queryWithRetry params from any[] to unknown[] - Update isTransientError to require Error instances (was accepting plain objects) - 19 new TDD tests (error-type-safety.test.ts) - Updated existing tests to use proper Error instances - 598 tests total, all passing, zero type errors
This commit is contained in:
parent
da049b77e3
commit
5a7ee79316
12 changed files with 221 additions and 98 deletions
|
|
@ -1,11 +1,18 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { isTransientError } from "../utils/errors.js";
|
||||
|
||||
/** Create an Error with a `.code` property (like Node/pg errors) */
|
||||
function makeError(opts: { code?: string; message?: string }): Error {
|
||||
const err = new Error(opts.message || "");
|
||||
if (opts.code) (err as Error & { code: string }).code = opts.code;
|
||||
return err;
|
||||
}
|
||||
|
||||
describe("isTransientError", () => {
|
||||
describe("transient error codes", () => {
|
||||
for (const code of ["ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "CONNECTION_LOST", "57P01", "57P02", "57P03", "08006", "08003", "08001"]) {
|
||||
it(`detects code ${code}`, () => {
|
||||
expect(isTransientError({ code })).toBe(true);
|
||||
expect(isTransientError(makeError({ code }))).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -20,7 +27,7 @@ describe("isTransientError", () => {
|
|||
|
||||
describe("non-transient errors", () => {
|
||||
it("rejects generic error", () => expect(isTransientError(new Error("something broke"))).toBe(false));
|
||||
it("rejects SQL syntax error", () => expect(isTransientError({ code: "42601", message: "syntax error" })).toBe(false));
|
||||
it("rejects SQL syntax error", () => expect(isTransientError(makeError({ code: "42601", message: "syntax error" }))).toBe(false));
|
||||
});
|
||||
|
||||
describe("null/undefined input", () => {
|
||||
|
|
@ -29,8 +36,9 @@ describe("isTransientError", () => {
|
|||
});
|
||||
|
||||
describe("partial error objects", () => {
|
||||
it("handles error with code but no message", () => expect(isTransientError({ code: "ECONNRESET" })).toBe(true));
|
||||
it("handles error with message but no code", () => expect(isTransientError({ message: "connection terminated" })).toBe(true));
|
||||
it("rejects error with unrelated code and no message", () => expect(isTransientError({ code: "UNKNOWN" })).toBe(false));
|
||||
it("handles Error with code but no message", () => expect(isTransientError(makeError({ code: "ECONNRESET" }))).toBe(true));
|
||||
it("handles Error with message but no code", () => expect(isTransientError(new Error("connection terminated"))).toBe(true));
|
||||
it("rejects Error with unrelated code and no message", () => expect(isTransientError(makeError({ code: "UNKNOWN" }))).toBe(false));
|
||||
it("rejects plain object (not an Error instance)", () => expect(isTransientError({ code: "ECONNRESET" })).toBe(false));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
84
src/__tests__/error-type-safety.test.ts
Normal file
84
src/__tests__/error-type-safety.test.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { isTransientError, errorMessage, errorCode } from "../utils/errors.js";
|
||||
|
||||
describe("error type safety — unknown error types", () => {
|
||||
describe("isTransientError with non-Error objects", () => {
|
||||
it("handles string thrown as error", () => {
|
||||
expect(isTransientError("connection refused")).toBe(false);
|
||||
});
|
||||
|
||||
it("handles null thrown as error", () => {
|
||||
expect(isTransientError(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("handles undefined thrown as error", () => {
|
||||
expect(isTransientError(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("handles number thrown as error", () => {
|
||||
expect(isTransientError(42)).toBe(false);
|
||||
});
|
||||
|
||||
it("handles plain object with code property", () => {
|
||||
expect(isTransientError({ code: "ECONNRESET" })).toBe(false);
|
||||
});
|
||||
|
||||
it("handles Error with transient code", () => {
|
||||
const err = new Error("connection reset");
|
||||
(err as any).code = "ECONNRESET";
|
||||
expect(isTransientError(err)).toBe(true);
|
||||
});
|
||||
|
||||
it("handles Error with transient message", () => {
|
||||
const err = new Error("Connection terminated unexpectedly");
|
||||
expect(isTransientError(err)).toBe(true);
|
||||
});
|
||||
|
||||
it("handles Error with non-transient message", () => {
|
||||
const err = new Error("syntax error at position 42");
|
||||
expect(isTransientError(err)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("errorMessage", () => {
|
||||
it("extracts message from Error", () => {
|
||||
expect(errorMessage(new Error("test"))).toBe("test");
|
||||
});
|
||||
|
||||
it("returns string directly", () => {
|
||||
expect(errorMessage("raw string error")).toBe("raw string error");
|
||||
});
|
||||
|
||||
it("stringifies null", () => {
|
||||
expect(errorMessage(null)).toBe("null");
|
||||
});
|
||||
|
||||
it("stringifies number", () => {
|
||||
expect(errorMessage(42)).toBe("42");
|
||||
});
|
||||
|
||||
it("stringifies undefined", () => {
|
||||
expect(errorMessage(undefined)).toBe("undefined");
|
||||
});
|
||||
});
|
||||
|
||||
describe("errorCode", () => {
|
||||
it("extracts code from Error with code", () => {
|
||||
const err = new Error("fail");
|
||||
(err as any).code = "ECONNRESET";
|
||||
expect(errorCode(err)).toBe("ECONNRESET");
|
||||
});
|
||||
|
||||
it("returns undefined for Error without code", () => {
|
||||
expect(errorCode(new Error("fail"))).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for non-Error", () => {
|
||||
expect(errorCode("string")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for null", () => {
|
||||
expect(errorCode(null)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { isTransientError } from "../utils/errors.js";
|
||||
|
||||
/** Create an Error with a `.code` property (like Node/pg errors) */
|
||||
function makeError(opts: { code?: string; message?: string }): Error {
|
||||
const err = new Error(opts.message || "");
|
||||
if (opts.code) (err as Error & { code: string }).code = opts.code;
|
||||
return err;
|
||||
}
|
||||
|
||||
describe("isTransientError", () => {
|
||||
describe("null/undefined/empty input", () => {
|
||||
it("returns false for null", () => {
|
||||
|
|
@ -11,189 +18,191 @@ describe("isTransientError", () => {
|
|||
expect(isTransientError(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for empty object", () => {
|
||||
it("returns false for empty object (not an Error)", () => {
|
||||
expect(isTransientError({})).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for plain string", () => {
|
||||
expect(isTransientError("ECONNRESET")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error codes from TRANSIENT_ERRORS set", () => {
|
||||
it("returns true for ECONNRESET", () => {
|
||||
expect(isTransientError({ code: "ECONNRESET" })).toBe(true);
|
||||
expect(isTransientError(makeError({ code: "ECONNRESET" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for ECONNREFUSED", () => {
|
||||
expect(isTransientError({ code: "ECONNREFUSED" })).toBe(true);
|
||||
expect(isTransientError(makeError({ code: "ECONNREFUSED" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for EPIPE", () => {
|
||||
expect(isTransientError({ code: "EPIPE" })).toBe(true);
|
||||
expect(isTransientError(makeError({ code: "EPIPE" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for ETIMEDOUT", () => {
|
||||
expect(isTransientError({ code: "ETIMEDOUT" })).toBe(true);
|
||||
expect(isTransientError(makeError({ code: "ETIMEDOUT" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for CONNECTION_LOST", () => {
|
||||
expect(isTransientError({ code: "CONNECTION_LOST" })).toBe(true);
|
||||
expect(isTransientError(makeError({ code: "CONNECTION_LOST" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 57P01 (admin_shutdown)", () => {
|
||||
expect(isTransientError({ code: "57P01" })).toBe(true);
|
||||
expect(isTransientError(makeError({ code: "57P01" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 57P02 (crash_shutdown)", () => {
|
||||
expect(isTransientError({ code: "57P02" })).toBe(true);
|
||||
expect(isTransientError(makeError({ code: "57P02" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 57P03 (cannot_connect_now)", () => {
|
||||
expect(isTransientError({ code: "57P03" })).toBe(true);
|
||||
expect(isTransientError(makeError({ code: "57P03" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 08006 (connection_failure)", () => {
|
||||
expect(isTransientError({ code: "08006" })).toBe(true);
|
||||
expect(isTransientError(makeError({ code: "08006" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 08003 (connection_does_not_exist)", () => {
|
||||
expect(isTransientError({ code: "08003" })).toBe(true);
|
||||
expect(isTransientError(makeError({ code: "08003" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 08001 (sqlclient_unable_to_establish_sqlconnection)", () => {
|
||||
expect(isTransientError({ code: "08001" })).toBe(true);
|
||||
expect(isTransientError(makeError({ code: "08001" }))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("message substring matching", () => {
|
||||
it("returns true for 'no available server'", () => {
|
||||
expect(isTransientError({ message: "no available server" })).toBe(true);
|
||||
expect(isTransientError(new Error("no available server"))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 'connection terminated'", () => {
|
||||
expect(isTransientError({ message: "connection terminated unexpectedly" })).toBe(true);
|
||||
expect(isTransientError(new Error("connection terminated unexpectedly"))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 'connection refused'", () => {
|
||||
expect(isTransientError({ message: "connection refused by server" })).toBe(true);
|
||||
expect(isTransientError(new Error("connection refused by server"))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 'server closed the connection'", () => {
|
||||
expect(isTransientError({ message: "server closed the connection unexpectedly" })).toBe(true);
|
||||
expect(isTransientError(new Error("server closed the connection unexpectedly"))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 'timeout expired'", () => {
|
||||
expect(isTransientError({ message: "timeout expired waiting for connection" })).toBe(true);
|
||||
expect(isTransientError(new Error("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);
|
||||
expect(isTransientError(new Error("No Available Server"))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 'CONNECTION TERMINATED' (uppercase)", () => {
|
||||
expect(isTransientError({ message: "CONNECTION TERMINATED" })).toBe(true);
|
||||
expect(isTransientError(new Error("CONNECTION TERMINATED"))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 'Connection Refused' (title case)", () => {
|
||||
expect(isTransientError({ message: "Connection Refused" })).toBe(true);
|
||||
expect(isTransientError(new Error("Connection Refused"))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 'SERVER CLOSED THE CONNECTION' (uppercase)", () => {
|
||||
expect(isTransientError({ message: "SERVER CLOSED THE CONNECTION" })).toBe(true);
|
||||
expect(isTransientError(new Error("SERVER CLOSED THE CONNECTION"))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for 'Timeout Expired' (title case)", () => {
|
||||
expect(isTransientError({ message: "Timeout Expired" })).toBe(true);
|
||||
expect(isTransientError(new Error("Timeout Expired"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("non-transient errors", () => {
|
||||
it("returns false for syntax error", () => {
|
||||
expect(isTransientError({
|
||||
expect(isTransientError(makeError({
|
||||
code: "42601",
|
||||
message: "syntax error at or near SELECT"
|
||||
})).toBe(false);
|
||||
}))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for unique constraint violation", () => {
|
||||
expect(isTransientError({
|
||||
expect(isTransientError(makeError({
|
||||
code: "23505",
|
||||
message: "duplicate key value violates unique constraint"
|
||||
})).toBe(false);
|
||||
}))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for foreign key violation", () => {
|
||||
expect(isTransientError({
|
||||
expect(isTransientError(makeError({
|
||||
code: "23503",
|
||||
message: "foreign key constraint violation"
|
||||
})).toBe(false);
|
||||
}))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for not null violation", () => {
|
||||
expect(isTransientError({
|
||||
expect(isTransientError(makeError({
|
||||
code: "23502",
|
||||
message: "null value in column violates not-null constraint"
|
||||
})).toBe(false);
|
||||
}))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for permission denied", () => {
|
||||
expect(isTransientError({
|
||||
expect(isTransientError(makeError({
|
||||
code: "42501",
|
||||
message: "permission denied for table users"
|
||||
})).toBe(false);
|
||||
}))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unrelated codes and messages", () => {
|
||||
it("returns false for unrelated error code", () => {
|
||||
expect(isTransientError({ code: "UNKNOWN_ERROR" })).toBe(false);
|
||||
expect(isTransientError(makeError({ code: "UNKNOWN_ERROR" }))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for unrelated error message", () => {
|
||||
expect(isTransientError({ message: "Something went wrong" })).toBe(false);
|
||||
expect(isTransientError(new Error("Something went wrong"))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for generic database error", () => {
|
||||
expect(isTransientError({
|
||||
expect(isTransientError(makeError({
|
||||
code: "P0001",
|
||||
message: "Database operation failed"
|
||||
})).toBe(false);
|
||||
}))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for application error", () => {
|
||||
expect(isTransientError({
|
||||
message: "Invalid user input"
|
||||
})).toBe(false);
|
||||
expect(isTransientError(new Error("Invalid user input"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("returns true when both code and message match", () => {
|
||||
expect(isTransientError({
|
||||
expect(isTransientError(makeError({
|
||||
code: "ECONNRESET",
|
||||
message: "connection terminated"
|
||||
})).toBe(true);
|
||||
}))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when only code matches", () => {
|
||||
expect(isTransientError({
|
||||
expect(isTransientError(makeError({
|
||||
code: "ETIMEDOUT",
|
||||
message: "some other message"
|
||||
})).toBe(true);
|
||||
}))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when only message matches", () => {
|
||||
expect(isTransientError({
|
||||
expect(isTransientError(makeError({
|
||||
code: "SOME_CODE",
|
||||
message: "no available server to connect"
|
||||
})).toBe(true);
|
||||
}))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for error with only unrelated code", () => {
|
||||
expect(isTransientError({ code: "NOTFOUND" })).toBe(false);
|
||||
expect(isTransientError(makeError({ code: "NOTFOUND" }))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for error with empty message", () => {
|
||||
expect(isTransientError({ message: "" })).toBe(false);
|
||||
it("returns false for Error with empty message", () => {
|
||||
expect(isTransientError(new Error(""))).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue