import { describe, expect, it, vi } from "vitest"; import { createReadOnlyProxy } from "../lib/read-only-prisma.js"; function makeFakeClient() { const user = { findUnique: vi.fn(async () => ({ id: "u1" })), findMany: vi.fn(async () => []), create: vi.fn(async () => ({ id: "u1" })), update: vi.fn(async () => ({ id: "u1" })), upsert: vi.fn(async () => ({ id: "u1" })), delete: vi.fn(async () => ({ id: "u1" })), createMany: vi.fn(async () => ({ count: 1 })), createManyAndReturn: vi.fn(async () => [{ id: "u1" }]), updateMany: vi.fn(async () => ({ count: 1 })), deleteMany: vi.fn(async () => ({ count: 1 })), }; const client = { user, $queryRaw: vi.fn(async () => [{ result: 1 }]), $queryRawUnsafe: vi.fn(async () => [{ result: 1 }]), $executeRaw: vi.fn(async () => 0), $executeRawUnsafe: vi.fn(async () => 0), $transaction: vi.fn(async () => []), $runCommandRaw: vi.fn(async () => ({ ok: 1 })), }; // eslint-disable-next-line @typescript-eslint/no-explicit-any return client as any; } describe("createReadOnlyProxy", () => { it("allows model reads", async () => { const proxy = createReadOnlyProxy(makeFakeClient()); await expect(proxy.user.findUnique({ where: { id: "u1" } })).resolves.toEqual({ id: "u1" }); await expect(proxy.user.findMany()).resolves.toEqual([]); }); it("blocks model writes with clear error", () => { const proxy = createReadOnlyProxy(makeFakeClient()); expect(() => proxy.user.create({ data: {} })).toThrow( /Write operation "create" on "user" not permitted/, ); expect(() => proxy.user.update({ where: { id: "u1" }, data: {} })).toThrow( /Write operation "update"/, ); expect(() => proxy.user.upsert({ where: { id: "u1" }, create: {}, update: {} })).toThrow( /Write operation "upsert"/, ); expect(() => proxy.user.delete({ where: { id: "u1" } })).toThrow(/Write operation "delete"/); expect(() => proxy.user.createMany({ data: [] })).toThrow(/Write operation "createMany"/); expect(() => proxy.user.createManyAndReturn({ data: [] })).toThrow( /Write operation "createManyAndReturn"/, ); expect(() => proxy.user.updateMany({ where: {}, data: {} })).toThrow( /Write operation "updateMany"/, ); expect(() => proxy.user.deleteMany({ where: {} })).toThrow(/Write operation "deleteMany"/); }); it("allows template-tagged $queryRaw (read-only by contract)", async () => { const proxy = createReadOnlyProxy(makeFakeClient()); await expect(proxy.$queryRaw`SELECT 1`).resolves.toEqual([{ result: 1 }]); }); it("blocks $queryRawUnsafe (DDL/DML smuggling)", () => { const proxy = createReadOnlyProxy(makeFakeClient()); expect(() => proxy.$queryRawUnsafe("SELECT 1")).toThrow( /Raw\/escape operation "\$queryRawUnsafe" not permitted/, ); }); it("blocks $executeRaw and $executeRawUnsafe", () => { const proxy = createReadOnlyProxy(makeFakeClient()); expect(() => proxy.$executeRaw`DELETE FROM users`).toThrow( /Raw\/escape operation "\$executeRaw" not permitted/, ); expect(() => proxy.$executeRawUnsafe("DELETE FROM users")).toThrow( /Raw\/escape operation "\$executeRawUnsafe" not permitted/, ); }); it("blocks $transaction (interactive tx could contain writes)", () => { const proxy = createReadOnlyProxy(makeFakeClient()); expect(() => proxy.$transaction([])).toThrow( /Raw\/escape operation "\$transaction" not permitted/, ); }); it("blocks $runCommandRaw (Mongo-style raw command)", () => { const proxy = createReadOnlyProxy(makeFakeClient()); expect(() => proxy.$runCommandRaw({})).toThrow( /Raw\/escape operation "\$runCommandRaw" not permitted/, ); }); });