215 lines
7.6 KiB
TypeScript
215 lines
7.6 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { postgresTest } from "@internal/testcontainers";
|
|
import { Prisma, PrismaClient } from "@trigger.dev/database";
|
|
import {
|
|
captureInfrastructureErrors,
|
|
clientSafeErrorMessage,
|
|
infraErrorAlreadyLogged,
|
|
logTransactionInfrastructureError,
|
|
} from "~/utils/prismaErrors";
|
|
|
|
vi.setConfig({ testTimeout: 60_000 });
|
|
|
|
function capturingLogger() {
|
|
const captured: Array<{ message: string; fields?: Record<string, unknown> }> = [];
|
|
return {
|
|
captured,
|
|
error: (message: string, fields?: Record<string, unknown>) => {
|
|
captured.push({ message, fields });
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("captureInfrastructureErrors", () => {
|
|
postgresTest(
|
|
"P2025 (not found) passes through with code intact and unlogged",
|
|
async ({ prisma }) => {
|
|
const log = capturingLogger();
|
|
const client = captureInfrastructureErrors(prisma, log);
|
|
|
|
const error = await client.secretStore
|
|
.update({ where: { key: "does-not-exist" }, data: { version: "2" } })
|
|
.then(() => undefined)
|
|
.catch((e) => e);
|
|
|
|
expect(error).toBeInstanceOf(Prisma.PrismaClientKnownRequestError);
|
|
expect((error as Prisma.PrismaClientKnownRequestError).code).toBe("P2025");
|
|
expect(log.captured).toHaveLength(0);
|
|
}
|
|
);
|
|
|
|
postgresTest(
|
|
"P2002 (unique violation) passes through with code intact and unlogged",
|
|
async ({ prisma }) => {
|
|
const log = capturingLogger();
|
|
const client = captureInfrastructureErrors(prisma, log);
|
|
|
|
await client.secretStore.create({ data: { key: "dup-key", value: { a: 1 } } });
|
|
|
|
const error = await client.secretStore
|
|
.create({ data: { key: "dup-key", value: { a: 2 } } })
|
|
.then(() => undefined)
|
|
.catch((e) => e);
|
|
|
|
expect(error).toBeInstanceOf(Prisma.PrismaClientKnownRequestError);
|
|
expect((error as Prisma.PrismaClientKnownRequestError).code).toBe("P2002");
|
|
expect(log.captured).toHaveLength(0);
|
|
}
|
|
);
|
|
|
|
postgresTest(
|
|
"errors raised inside an interactive $transaction keep their code",
|
|
async ({ prisma }) => {
|
|
const log = capturingLogger();
|
|
const client = captureInfrastructureErrors(prisma, log);
|
|
|
|
// Proves $allOperations fires per-statement inside a transaction — the
|
|
// basis for transaction retry logic (which branches on error.code) staying
|
|
// intact.
|
|
const error = await client
|
|
.$transaction(async (tx) => {
|
|
await tx.secretStore.update({ where: { key: "missing-in-tx" }, data: { version: "2" } });
|
|
})
|
|
.then(() => undefined)
|
|
.catch((e) => e);
|
|
|
|
expect(error).toBeInstanceOf(Prisma.PrismaClientKnownRequestError);
|
|
expect((error as Prisma.PrismaClientKnownRequestError).code).toBe("P2025");
|
|
expect(log.captured).toHaveLength(0);
|
|
}
|
|
);
|
|
|
|
postgresTest("raw queries (model undefined) are wrapped without crashing", async ({ prisma }) => {
|
|
const log = capturingLogger();
|
|
const client = captureInfrastructureErrors(prisma, log);
|
|
|
|
const rows = await client.$queryRaw<Array<{ one: number }>>(Prisma.sql`SELECT 1 as one`);
|
|
expect(rows[0].one).toBe(1);
|
|
|
|
// A failing raw query (non-infra) must still rethrow rather than throw on
|
|
// the undefined `model`.
|
|
const error = await client
|
|
.$queryRaw(Prisma.sql`SELECT 1 / 0`)
|
|
.then(() => undefined)
|
|
.catch((e) => e);
|
|
|
|
expect(error).toBeInstanceOf(Error);
|
|
expect(log.captured).toHaveLength(0);
|
|
});
|
|
|
|
postgresTest("a genuine connectivity failure is logged with model/operation/code", async () => {
|
|
const log = capturingLogger();
|
|
// Point at a closed port to provoke a real P1001 / initialization error —
|
|
// no mocking.
|
|
const unreachable = new PrismaClient({
|
|
datasources: {
|
|
db: { url: "postgresql://postgres:postgres@127.0.0.1:1/postgres?connect_timeout=2" },
|
|
},
|
|
});
|
|
const client = captureInfrastructureErrors(unreachable, log);
|
|
|
|
try {
|
|
const error = await client.secretStore
|
|
.findFirst({ where: { key: "anything" } })
|
|
.then(() => undefined)
|
|
.catch((e) => e);
|
|
|
|
expect(error).toBeInstanceOf(Error);
|
|
expect(log.captured).toHaveLength(1);
|
|
expect(log.captured[0].message).toBe("prisma infrastructure error");
|
|
expect(log.captured[0].fields?.operation).toBe("findFirst");
|
|
expect(log.captured[0].fields?.model).toBe("SecretStore");
|
|
|
|
// Dedupe: the extension tagged it, so a $transaction-boundary logger
|
|
// seeing the same error must NOT log it a second time.
|
|
expect(infraErrorAlreadyLogged(error)).toBe(true);
|
|
const boundaryLog = capturingLogger();
|
|
expect(logTransactionInfrastructureError(error, boundaryLog)).toBe(false);
|
|
expect(boundaryLog.captured).toHaveLength(0);
|
|
} finally {
|
|
await unreachable.$disconnect();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("logTransactionInfrastructureError", () => {
|
|
// Covers the transaction boundary, which $allOperations cannot reach.
|
|
it("logs an uncoded infra error (PrismaClientInitializationError)", () => {
|
|
const log = capturingLogger();
|
|
const error = new Prisma.PrismaClientInitializationError(
|
|
"Can't reach database server",
|
|
"6.14.0",
|
|
"P1001"
|
|
);
|
|
|
|
expect(logTransactionInfrastructureError(error, log)).toBe(true);
|
|
expect(log.captured).toHaveLength(1);
|
|
expect(log.captured[0].message).toBe("prisma.$transaction infrastructure error");
|
|
expect(log.captured[0].fields?.name).toBe("PrismaClientInitializationError");
|
|
});
|
|
|
|
it("skips a coded infra error (transac's callback already logs those)", () => {
|
|
const log = capturingLogger();
|
|
const error = new Prisma.PrismaClientKnownRequestError("Can't reach database server", {
|
|
code: "P1001",
|
|
clientVersion: "6.14.0",
|
|
});
|
|
|
|
expect(logTransactionInfrastructureError(error, log)).toBe(false);
|
|
expect(log.captured).toHaveLength(0);
|
|
});
|
|
|
|
it("skips a non-infra coded error (P2002)", () => {
|
|
const log = capturingLogger();
|
|
const error = new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
|
|
code: "P2002",
|
|
clientVersion: "6.14.0",
|
|
});
|
|
|
|
expect(logTransactionInfrastructureError(error, log)).toBe(false);
|
|
expect(log.captured).toHaveLength(0);
|
|
});
|
|
|
|
it("skips a plain non-Prisma error", () => {
|
|
const log = capturingLogger();
|
|
|
|
expect(logTransactionInfrastructureError(new Error("boom"), log)).toBe(false);
|
|
expect(log.captured).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("clientSafeErrorMessage", () => {
|
|
// Guards the API-route leak: an infra error's message carries the DB hostname.
|
|
it("obfuscates a coded infra error (P1001) message", () => {
|
|
const error = new Prisma.PrismaClientKnownRequestError(
|
|
"Can't reach database server at `db-internal.example.com:5432`",
|
|
{ code: "P1001", clientVersion: "6.14.0" }
|
|
);
|
|
|
|
expect(clientSafeErrorMessage(error)).toBe("Internal Server Error");
|
|
});
|
|
|
|
it("obfuscates an uncoded infra error (PrismaClientInitializationError) message", () => {
|
|
const error = new Prisma.PrismaClientInitializationError(
|
|
"Can't reach database server at `db-internal.example.com:5432`",
|
|
"6.14.0",
|
|
"P1001"
|
|
);
|
|
|
|
expect(clientSafeErrorMessage(error)).toBe("Internal Server Error");
|
|
});
|
|
|
|
it("leaves non-infra Prisma error (P2002) messages unchanged", () => {
|
|
const error = new Prisma.PrismaClientKnownRequestError("Unique constraint failed on email", {
|
|
code: "P2002",
|
|
clientVersion: "6.14.0",
|
|
});
|
|
|
|
expect(clientSafeErrorMessage(error)).toBe("Unique constraint failed on email");
|
|
});
|
|
|
|
it("leaves plain domain/validation error messages unchanged", () => {
|
|
expect(clientSafeErrorMessage(new Error("Invalid delay value"))).toBe("Invalid delay value");
|
|
});
|
|
});
|