1import {
2 closePrismaDevDatabase,
3 ensurePrismaDevDatabase,
4} from "@/Prisma/PrismaDevDatabase";
5import * as NodeServices from "@effect/platform-node/NodeServices";
6import { describe, expect, it } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Redacted from "effect/Redacted";
9import * as Result from "effect/Result";
10import * as TestClock from "effect/testing/TestClock";
11import { Client } from "pg";
12
13const toError = (message: string) => (cause: unknown) =>
14 cause instanceof Error ? cause : new Error(`${message}: ${String(cause)}`);
15
16const queryAnswer = Effect.fn(function* (connectionString: string) {
17 const client = yield* Effect.sync(() => new Client({ connectionString }));
18 yield* Effect.tryPromise({
19 try: () => client.connect(),
20 catch: toError("Failed to connect to local Prisma database"),
21 });
22 return yield* Effect.tryPromise({
23 try: () => client.query("select 42::int as answer"),
24 catch: toError("Failed to query local Prisma database"),
25 }).pipe(
26 Effect.map((result) => Number(result.rows[0]?.answer)),
27 Effect.ensuring(
28 Effect.tryPromise({
29 try: () => client.end(),
30 catch: toError("Failed to close local Prisma database client"),
31 }).pipe(Effect.catch(() => Effect.void)),
32 ),
33 );
34});
35
36describe(
37 "Prisma dev database",
38 { tags: ["provider:prisma", "provider:prisma:database", "local"] },
39 () => {
40 it.effect(
41 "rejects non-positive migration timeouts",
42 () =>
43 Effect.gen(function* () {
44 const result = yield* ensurePrismaDevDatabase("dev:invalid-timeout", {
45 migrateTimeoutSeconds: 0,
46 }).pipe(Effect.result);
47
48 expect(Result.isFailure(result)).toBe(true);
49 if (Result.isFailure(result)) {
50 expect(String(result.failure)).toContain(
51 "migrateTimeoutSeconds must be a positive finite number",
52 );
53 }
54 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
55 { tags: ["unit"] },
56 );
57
58 it.effect(
59 "starts @prisma/dev and returns usable direct and pooled URLs",
60 () => {
61 const basePort = 58000 + (process.pid % 1000);
62 const databaseId = `dev:database:test-${process.pid}`;
63
64 return Effect.gen(function* () {
65 const attrs = yield* ensurePrismaDevDatabase(databaseId, {
66 provider: "@prisma/dev",
67 persistenceMode: "stateless",
68 port: basePort,
69 databasePort: basePort + 1,
70 shadowDatabasePort: basePort + 2,
71 });
72
73 expect(attrs).toBeDefined();
74 const direct = Redacted.value(attrs!.directConnectionString);
75 const pooled = Redacted.value(attrs!.pooledConnectionString);
76
77 expect(direct).toContain("127.0.0.1");
78 expect(pooled).toMatch(/^prisma\+postgres:\/\//);
79 expect(attrs!.host).toBe("127.0.0.1");
80
81 const answer = yield* queryAnswer(direct);
82 expect(answer).toBe(42);
83
84 expect(
85 yield* ensurePrismaDevDatabase(databaseId, false),
86 ).toBeUndefined();
87 const afterDisable = yield* queryAnswer(direct).pipe(Effect.result);
88 expect(Result.isFailure(afterDisable)).toBe(true);
89 }).pipe(
90 Effect.ensuring(
91 closePrismaDevDatabase(databaseId).pipe(Effect.orDie),
92 ),
93 Effect.scoped,
94 Effect.provide(NodeServices.layer),
95 );
96 },
97 );
98
99 it.effect(
100 "redacts migration failures and terminates commands at the timeout",
101 () => {
102 const basePort = 59000 + (process.pid % 500);
103 const databaseId = `dev:migration-test-${process.pid}`;
104 const node = JSON.stringify(process.execPath);
105 const options = {
106 provider: "@prisma/dev" as const,
107 persistenceMode: "stateless" as const,
108 port: basePort,
109 databasePort: basePort + 1,
110 shadowDatabasePort: basePort + 2,
111 };
112
113 return Effect.gen(function* () {
114 const attrs = yield* ensurePrismaDevDatabase(databaseId, options);
115 expect(attrs).toBeDefined();
116 const direct = Redacted.value(attrs!.directConnectionString);
117 const pooled = Redacted.value(attrs!.pooledConnectionString);
118 const apiKey = new URL(pooled).searchParams.get("api_key");
119 expect(apiKey).toBeTruthy();
120
121 const failed = yield* ensurePrismaDevDatabase(databaseId, {
122 ...options,
123 migrate: `exec ${node} -e 'const url = new URL(process.env.DATABASE_URL); console.error(process.env.DATABASE_URL); console.error(url.searchParams.get("api_key")); process.exit(7)'`,
124 migrateTimeoutSeconds: 5,
125 }).pipe(Effect.result);
126 expect(Result.isFailure(failed)).toBe(true);
127 if (Result.isFailure(failed)) {
128 const message = String(failed.failure);
129 expect(message).toContain("exit code 7");
130 expect(message).toContain("[REDACTED]");
131 expect(message).not.toContain(direct);
132 expect(message).not.toContain(pooled);
133 expect(message).not.toContain(apiKey!);
134 expect(JSON.stringify(failed.failure)).not.toContain(apiKey!);
135 }
136
137 const overflow = yield* ensurePrismaDevDatabase(databaseId, {
138 ...options,
139 migrate: `exec ${node} -e 'process.stdout.write("x".repeat(1048577))'`,
140 migrateTimeoutSeconds: 5,
141 }).pipe(Effect.result);
142 expect(Result.isFailure(overflow)).toBe(true);
143 if (Result.isFailure(overflow)) {
144 expect(String(overflow.failure)).toContain(
145 "migration stdout exceeded the 1048576 byte safety limit",
146 );
147 }
148
149 const timedOut = yield* TestClock.withLive(
150 ensurePrismaDevDatabase(databaseId, {
151 ...options,
152 migrate: `exec ${node} -e 'setInterval(() => {}, 1000)'`,
153 migrateTimeoutSeconds: 0.1,
154 }),
155 ).pipe(Effect.result);
156 expect(Result.isFailure(timedOut)).toBe(true);
157 if (Result.isFailure(timedOut)) {
158 expect(String(timedOut.failure)).toContain(
159 "timed out after 0.1 seconds and was terminated",
160 );
161 }
162 }).pipe(
163 Effect.ensuring(
164 closePrismaDevDatabase(databaseId).pipe(Effect.orDie),
165 ),
166 Effect.scoped,
167 Effect.provide(NodeServices.layer),
168 );
169 },
170 );
171 },
172);