1import {
2 extractConnectionSecrets,
3 PrismaApiDecodeError,
4 PrismaApiError,
5 PrismaClient,
6 PrismaClientLive,
7 type PrismaManagementClient,
8} from "@/Prisma/Client";
9import { PrismaEnvironment } from "@/Prisma/PrismaEnvironment";
10import { describe, expect, it } from "alchemy-test";
11import * as Effect from "effect/Effect";
12import * as Fiber from "effect/Fiber";
13import * as Layer from "effect/Layer";
14import * as Redacted from "effect/Redacted";
15import { TestClock } from "effect/testing";
16import * as HttpBody from "effect/http/HttpBody";
17import * as HttpClient from "effect/http/HttpClient";
18import * as HttpClientError from "effect/http/HttpClientError";
19import * as HttpClientResponse from "effect/http/HttpClientResponse";
20import { productionManagementApiRoutes } from "./fixtures/ManagementApiContract.ts";
21
22interface Captured {
23 url: string;
24 method: string;
25 pathname: string;
26 search: string;
27 authorization: string | undefined;
28 bodyJson: unknown;
29}
30
31const page = <T>(
32 data: T[],
33 hasMore = false,
34 nextCursor: string | null = null,
35) => json({ data, pagination: { hasMore, nextCursor } });
36
37const data = <T>(value: T) => json({ data: value });
38
39const json = (value: unknown, init?: ResponseInit) =>
40 new Response(JSON.stringify(value), {
41 status: 200,
42 headers: { "content-type": "application/json" },
43 ...init,
44 });
45
46const empty = () => new Response(null, { status: 204 });
47
48const expectedManagementApiRoutes = [...productionManagementApiRoutes].sort();
49
50const routeTemplateFor = (method: string, pathname: string): string => {
51 const concreteSegments = pathname.split("/");
52 const route = productionManagementApiRoutes.find((candidate) => {
53 const separator = candidate.indexOf(" ");
54 const candidateMethod = candidate.slice(0, separator);
55 const templatePath = candidate.slice(separator + 1);
56 const templateSegments = templatePath.split("/");
57 return (
58 candidateMethod === method &&
59 templateSegments.length === concreteSegments.length &&
60 templateSegments.every(
61 (segment, index) =>
62 (/^\{[^}]+\}$/.test(segment) &&
63 concreteSegments[index]?.length !== 0) ||
64 segment === concreteSegments[index],
65 )
66 );
67 });
68 if (!route) {
69 throw new Error(
70 `Missing route inventory mapping for ${method} ${pathname}`,
71 );
72 }
73 return route;
74};
75const routeInventoryFrom = (captured: Captured[]) => {
76 const routes = new Set<string>();
77 for (const request of captured) {
78 routes.add(routeTemplateFor(request.method, request.pathname));
79 }
80 routes.add("GET /v1/deployments/{deploymentId}/logs");
81 routes.add("GET /v1/builds/{buildId}/logs");
82 return [...routes].sort();
83};
84
85const fixtureResponse = (request: Captured) => {
86 if (request.pathname === "/v1/projects" && request.method === "GET") {
87 return request.search.includes("cursor=cursor-2")
88 ? page([{ id: "project-2", type: "project", name: "Two" }])
89 : page(
90 [{ id: "project-1", type: "project", name: "One" }],
91 true,
92 "cursor-2",
93 );
94 }
95
96 if (
97 request.pathname === "/v1/projects/project-1/databases" &&
98 request.method === "POST"
99 ) {
100 return data({ id: "database-1", type: "database", name: "main" });
101 }
102
103 if (
104 request.pathname === "/v1/databases/database-1/backups" &&
105 request.method === "GET"
106 ) {
107 return json({
108 data: [
109 {
110 id: "backup-1",
111 type: "backup",
112 backupType: "full",
113 createdAt: "2026-01-01T00:00:00Z",
114 status: "completed",
115 },
116 ],
117 meta: {
118 backupRetentionDays: 7,
119 },
120 pagination: {
121 hasMore: false,
122 limit: 1,
123 },
124 });
125 }
126
127 if (
128 request.pathname === "/v1/workspaces/workspace-1/integrations" &&
129 request.method === "GET"
130 ) {
131 return page([{ id: "integration-1", url: "https://example.test" }]);
132 }
133
134 if (request.pathname === "/v1/domains/domain-1" && request.method === "GET") {
135 return data({
136 id: "domain-1",
137 type: "custom-domain",
138 url: "https://api.prisma.test/v1/domains/domain-1",
139 hostname: "api.example.com",
140 appId: "app-1",
141 status: "pending_dns",
142 foundryStatus: "pending",
143 failureReason: null,
144 failureCategory: null,
145 certExpiresAt: null,
146 createdAt: "2026-01-01T00:00:00Z",
147 updatedAt: "2026-01-01T00:00:00Z",
148 dnsRecords: [],
149 });
150 }
151
152 if (request.pathname.startsWith("/v1/regions") && request.method === "GET") {
153 return json({
154 data: [
155 {
156 id: "us-east-1",
157 type: "region",
158 name: "US East",
159 product: "postgres",
160 status: "available",
161 },
162 ],
163 });
164 }
165
166 return json(
167 {
168 error: {
169 message: `Unhandled fixture request ${request.method} ${request.pathname}${request.search}`,
170 },
171 },
172 { status: 500 },
173 );
174};
175
176const layerForHttp = (
177 client: HttpClient.HttpClient,
178 baseUrl = "https://api.prisma.test",
179) =>
180 PrismaClientLive.pipe(
181 Layer.provide(
182 Layer.mergeAll(
183 Layer.succeed(HttpClient.HttpClient, client),
184 Layer.succeed(PrismaEnvironment, {
185 type: "serviceToken" as const,
186 serviceToken: Redacted.make("test-token"),
187 source: { type: "stored" as const },
188 baseUrl,
189 }),
190 ),
191 ),
192 );
193
194const harness = (baseUrl = "https://api.prisma.test") => {
195 const captured: Captured[] = [];
196 const client = HttpClient.make((request) =>
197 Effect.sync(() => {
198 const url = new URL(request.url);
199 const body = request.body as HttpBody.HttpBody;
200 const bodyText =
201 body._tag === "Uint8Array" ? new TextDecoder().decode(body.body) : "";
202 const entry: Captured = {
203 url: request.url,
204 method: request.method,
205 pathname: url.pathname,
206 search: url.search,
207 authorization: request.headers.authorization,
208 bodyJson: bodyText ? JSON.parse(bodyText) : undefined,
209 };
210 captured.push(entry);
211 return HttpClientResponse.fromWeb(request, fixtureResponse(entry));
212 }),
213 );
214 const layer = layerForHttp(client, baseUrl);
215 return { layer, captured };
216};
217
218const withClient = <A>(
219 f: (client: PrismaManagementClient) => Effect.Effect<A, any, any>,
220) =>
221 Effect.gen(function* () {
222 const client = yield* PrismaClient;
223 return yield* f(client);
224 });
225
226const routeCoverageHarness = () => {
227 const captured: Captured[] = [];
228 const client = HttpClient.make((request) =>
229 Effect.sync(() => {
230 const url = new URL(request.url);
231 const body = request.body as HttpBody.HttpBody;
232 const bodyText =
233 body._tag === "Uint8Array" ? new TextDecoder().decode(body.body) : "";
234 const entry: Captured = {
235 url: request.url,
236 method: request.method,
237 pathname: url.pathname,
238 search: url.search,
239 authorization: request.headers.authorization,
240 bodyJson: bodyText ? JSON.parse(bodyText) : undefined,
241 };
242 captured.push(entry);
243
244 if (entry.pathname.endsWith("/usage")) {
245 return HttpClientResponse.fromWeb(
246 request,
247 json({
248 period: { start: "2026-01-01", end: "2026-01-02" },
249 metrics: {
250 operations: { used: 0, unit: "ops" },
251 storage: { used: 0, unit: "GiB" },
252 },
253 generatedAt: "2026-01-02T00:00:00Z",
254 }),
255 );
256 }
257
258 if (entry.pathname.startsWith("/v1/regions") && entry.method === "GET") {
259 return HttpClientResponse.fromWeb(request, json({ data: [] }));
260 }
261
262 if (entry.method === "GET") {
263 return HttpClientResponse.fromWeb(request, page([]));
264 }
265
266 if (
267 entry.method === "DELETE" ||
268 entry.pathname.endsWith("/stop") ||
269 entry.pathname.endsWith("/transfer")
270 ) {
271 return HttpClientResponse.fromWeb(request, empty());
272 }
273
274 return HttpClientResponse.fromWeb(
275 request,
276 data({
277 id: "resource-1",
278 type: "resource",
279 foundryVersionId: "foundry-1",
280 uploadUrl: "https://upload.example.test/artifact.tar.gz",
281 previewDomain: "version-1.example.test",
282 appEndpointDomain: "app-1.example.test",
283 }),
284 );
285 }),
286 );
287 const layer = PrismaClientLive.pipe(
288 Layer.provide(
289 Layer.mergeAll(
290 Layer.succeed(HttpClient.HttpClient, client),
291 Layer.succeed(PrismaEnvironment, {
292 type: "serviceToken" as const,
293 serviceToken: Redacted.make("test-token"),
294 source: { type: "stored" as const },
295 baseUrl: "https://api.prisma.test",
296 }),
297 ),
298 ),
299 );
300 return { layer, captured };
301};
302
303describe("PrismaClient", { tags: ["unit", "provider:prisma", "local"] }, () => {
304 it("extracts canonical endpoint secrets and parses direct credentials", () => {
305 const secrets = extractConnectionSecrets({
306 id: "connection-1",
307 type: "connection",
308 url: "https://api.prisma.test/v1/connections/connection-1",
309 name: "api",
310 createdAt: "2026-01-01T00:00:00Z",
311 kind: "postgres",
312 endpoints: {
313 direct: {
314 host: "direct.prisma.test",
315 port: 5432,
316 connectionString:
317 "postgres://api:p%40ss@direct.prisma.test:5432/postgres?sslmode=require",
318 },
319 pooled: {
320 host: "pooled.prisma.test",
321 port: 5432,
322 connectionString: "postgres://pooled",
323 },
324 },
325 database: {
326 id: "database-1",
327 url: "https://api.prisma.test/v1/databases/database-1",
328 name: "main",
329 },
330 } as unknown as Parameters<typeof extractConnectionSecrets>[0]);
331
332 expect(Redacted.value(secrets.directConnectionString!)).toContain(
333 "direct.prisma.test",
334 );
335 expect(Redacted.value(secrets.pooledConnectionString!)).toBe(
336 "postgres://pooled",
337 );
338 expect(secrets.host).toBe("direct.prisma.test");
339 expect(secrets.user).toBe("api");
340 expect(Redacted.value(secrets.password!)).toBe("p@ss");
341 });
342
343 it.effect("paginates list endpoints and sends bearer auth", () => {
344 const { layer, captured } = harness();
345
346 return withClient((client) =>
347 Effect.gen(function* () {
348 const projects = yield* client.listProjects({ limit: 1 });
349
350 expect(projects.map((project: { id: string }) => project.id)).toEqual([
351 "project-1",
352 "project-2",
353 ]);
354 expect(captured.map((request) => request.pathname)).toEqual([
355 "/v1/projects",
356 "/v1/projects",
357 ]);
358 expect(captured[0]?.search).toBe("?limit=1");
359 expect(captured[1]?.search).toBe("?limit=1&cursor=cursor-2");
360 expect(
361 captured.every(
362 (request) => request.authorization === "Bearer test-token",
363 ),
364 ).toBe(true);
365 }),
366 ).pipe(Effect.provide(layer));
367 });
368
369 it.effect("starts pagination from an explicit cursor", () => {
370 const { layer, captured } = harness();
371
372 return withClient((client) =>
373 Effect.gen(function* () {
374 const projects = yield* client.listProjects({
375 limit: 1,
376 cursor: "cursor-2",
377 });
378
379 expect(projects.map((project: { id: string }) => project.id)).toEqual([
380 "project-2",
381 ]);
382 expect(captured.map((request) => request.pathname)).toEqual([
383 "/v1/projects",
384 ]);
385 expect(captured[0]?.search).toBe("?limit=1&cursor=cursor-2");
386 }),
387 ).pipe(Effect.provide(layer));
388 });
389
390 it.effect("rejects pagination that omits a required next cursor", () => {
391 const http = HttpClient.make((request) =>
392 Effect.succeed(HttpClientResponse.fromWeb(request, page([], true, null))),
393 );
394
395 return withClient((client) => client.listProjects()).pipe(
396 Effect.provide(layerForHttp(http)),
397 Effect.flip,
398 Effect.map((error) => {
399 expect(error).toBeInstanceOf(PrismaApiDecodeError);
400 expect(error.message).toContain(
401 "hasMore was true without a non-empty nextCursor",
402 );
403 }),
404 );
405 });
406
407 it.effect("rejects pagination that repeats a cursor", () => {
408 let attempts = 0;
409 const http = HttpClient.make((request) =>
410 Effect.sync(() => {
411 attempts += 1;
412 return HttpClientResponse.fromWeb(
413 request,
414 page([], true, "repeated-cursor"),
415 );
416 }),
417 );
418
419 return withClient((client) => client.listProjects()).pipe(
420 Effect.provide(layerForHttp(http)),
421 Effect.flip,
422 Effect.map((error) => {
423 expect(error).toBeInstanceOf(PrismaApiDecodeError);
424 expect(error.message).toContain("nextCursor repeated");
425 expect(attempts).toBe(2);
426 }),
427 );
428 });
429
430 it.effect("bounds aggregate pagination response bytes", () => {
431 let attempts = 0;
432 const payload = "x".repeat(4 * 1024 * 1024 - 1_024);
433 const http = HttpClient.make((request) =>
434 Effect.sync(() => {
435 attempts += 1;
436 return HttpClientResponse.fromWeb(
437 request,
438 page(
439 [{ id: `project-${attempts}`, payload }],
440 true,
441 `cursor-${attempts}`,
442 ),
443 );
444 }),
445 );
446
447 return withClient((client) => client.listProjects()).pipe(
448 Effect.provide(layerForHttp(http)),
449 Effect.flip,
450 Effect.map((error) => {
451 expect(error).toBeInstanceOf(PrismaApiDecodeError);
452 expect(error.message).toContain(
453 "67108864 aggregate response byte safety limit",
454 );
455 if (error instanceof PrismaApiDecodeError) {
456 expect(error.bodyLength).toBeGreaterThan(64 * 1024 * 1024);
457 }
458 expect(attempts).toBe(17);
459 }),
460 );
461 });
462
463 it.effect("uses the configured Prisma API base URL", () => {
464 const { layer, captured } = harness("https://control-plane.prisma.test");
465
466 return withClient((client) =>
467 Effect.gen(function* () {
468 yield* client.listProjects({ limit: 1 });
469
470 expect(captured[0]?.url).toBe(
471 "https://control-plane.prisma.test/v1/projects?limit=1",
472 );
473 }),
474 ).pipe(Effect.provide(layer));
475 });
476
477 it.effect("preserves current custom-domain wire fields", () => {
478 const { layer } = harness();
479
480 return withClient((client) =>
481 Effect.gen(function* () {
482 const domain = yield* client.getCustomDomain("domain-1");
483 expect(domain.appId).toBe("app-1");
484 expect(domain.foundryStatus).toBe("pending");
485 }),
486 ).pipe(Effect.provide(layer));
487 });
488
489 it.effect("preserves 200 versus 201 custom-domain create outcomes", () => {
490 const statuses = [200, 201] as const;
491 let requestIndex = 0;
492 const domain = {
493 id: "domain-1",
494 type: "custom-domain" as const,
495 url: "https://api.prisma.test/v1/domains/domain-1",
496 hostname: "api.example.com",
497 appId: "app-1",
498 status: "pending_dns" as const,
499 foundryStatus: "pending",
500 failureReason: null,
501 failureCategory: null,
502 certExpiresAt: null,
503 createdAt: "2026-01-01T00:00:00Z",
504 updatedAt: "2026-01-01T00:00:00Z",
505 dnsRecords: [],
506 };
507 const http = HttpClient.make((request) =>
508 Effect.sync(() => {
509 const status = statuses[requestIndex++];
510 return HttpClientResponse.fromWeb(
511 request,
512 json({ data: domain }, { status }),
513 );
514 }),
515 );
516
517 return withClient((client) =>
518 Effect.gen(function* () {
519 const existing = yield* client.createAppDomain("app-1", {
520 hostname: domain.hostname,
521 });
522 const created = yield* client.createAppDomain("app-1", {
523 hostname: domain.hostname,
524 });
525
526 expect(existing).toEqual({ status: 200, domain });
527 expect(created).toEqual({ status: 201, domain });
528 }),
529 ).pipe(Effect.provide(layerForHttp(http)));
530 });
531
532 it.effect("accepts the full flat database-create response contract", () => {
533 const database = {
534 id: "database-1",
535 type: "database" as const,
536 url: "https://api.prisma.test/v1/databases/database-1",
537 name: "main",
538 status: "failure" as const,
539 createdAt: "2026-01-01T00:00:00Z",
540 isDefault: false,
541 defaultConnectionId: null,
542 connections: [],
543 project: {
544 id: "project-1",
545 url: "https://api.prisma.test/v1/projects/project-1",
546 name: "app",
547 },
548 region: null,
549 source: null,
550 branchId: null,
551 };
552 const http = HttpClient.make((request) =>
553 Effect.succeed(
554 HttpClientResponse.fromWeb(
555 request,
556 json({ data: database }, { status: 201 }),
557 ),
558 ),
559 );
560
561 return withClient((client) =>
562 Effect.gen(function* () {
563 const result = yield* client.createDatabase({
564 projectId: "project-1",
565 });
566 expect(result.status).toBe("failure");
567 expect(result.region).toBeNull();
568 }),
569 ).pipe(Effect.provide(layerForHttp(http)));
570 });
571
572 it.effect("does not retain secret-bearing malformed response bodies", () => {
573 const secret = "postgres://admin:super-secret@db.prisma.test/postgres";
574 const http = HttpClient.make((request) =>
575 Effect.succeed(
576 HttpClientResponse.fromWeb(
577 request,
578 new Response(`{"data":{"connectionString":"${secret}"}`, {
579 status: 201,
580 headers: { "content-type": "application/json" },
581 }),
582 ),
583 ),
584 );
585 const layer = PrismaClientLive.pipe(
586 Layer.provide(
587 Layer.mergeAll(
588 Layer.succeed(HttpClient.HttpClient, http),
589 Layer.succeed(PrismaEnvironment, {
590 type: "serviceToken" as const,
591 serviceToken: Redacted.make("test-token"),
592 source: { type: "stored" as const },
593 baseUrl: "https://api.prisma.test",
594 }),
595 ),
596 ),
597 );
598
599 return withClient((client) =>
600 Effect.gen(function* () {
601 const error = yield* client
602 .createConnection({ databaseId: "database-1", name: "api" })
603 .pipe(Effect.flip);
604 expect(error).toBeInstanceOf(PrismaApiDecodeError);
605 expect(JSON.stringify(error)).not.toContain(secret);
606 if (error instanceof PrismaApiDecodeError) {
607 expect(error.bodyLength).toBeGreaterThan(0);
608 expect("body" in error).toBe(false);
609 expect("cause" in error).toBe(false);
610 }
611 }),
612 ).pipe(Effect.provide(layer));
613 });
614
615 it.effect("rejects a missing data response envelope", () => {
616 const http = HttpClient.make((request) =>
617 Effect.succeed(
618 HttpClientResponse.fromWeb(request, json({ project: {} })),
619 ),
620 );
621
622 return withClient((client) => client.getProject("project-1")).pipe(
623 Effect.provide(layerForHttp(http)),
624 Effect.flip,
625 Effect.map((error) => {
626 expect(error).toBeInstanceOf(PrismaApiDecodeError);
627 expect(error.message).toContain("did not contain a data envelope");
628 }),
629 );
630 });
631
632 it.effect("rejects path-confusing resource IDs before sending auth", () => {
633 const { layer, captured } = harness();
634
635 return withClient((client) =>
636 Effect.gen(function* () {
637 const traversal = yield* client
638 .getProject("../workspaces")
639 .pipe(Effect.flip);
640 const embeddedRoute = yield* client
641 .getProject("project-1/databases")
642 .pipe(Effect.flip);
643 const deploymentLog = yield* client
644 .getDeploymentLogsRequest("deployment-1/../../projects")
645 .pipe(Effect.flip);
646 const buildLog = yield* client
647 .getBuildLogsRequest("build-1?token=leak")
648 .pipe(Effect.flip);
649
650 for (const error of [
651 traversal,
652 embeddedRoute,
653 deploymentLog,
654 buildLog,
655 ]) {
656 expect(error).toBeInstanceOf(PrismaApiError);
657 expect(error.message).toContain("invalid Prisma Management API");
658 }
659 expect(captured).toEqual([]);
660 }),
661 ).pipe(Effect.provide(layer));
662 });
663
664 it.effect("times out an unresponsive management API request", () => {
665 const http = HttpClient.make(() => Effect.never);
666 const layer = layerForHttp(http);
667
668 return Effect.gen(function* () {
669 const fiber = yield* withClient((client) =>
670 client.getProject("project-1"),
671 ).pipe(
672 Effect.provide(layer),
673 Effect.flip,
674 Effect.forkChild({ startImmediately: true }),
675 );
676 yield* TestClock.adjust("11 seconds");
677 const error = yield* Fiber.join(fiber);
678
679 expect(error).toBeInstanceOf(PrismaApiError);
680 if (error instanceof PrismaApiError) {
681 expect(error.status).toBe(0);
682 expect(error.message).toContain("timed out after 10 seconds");
683 }
684 }).pipe(Effect.provide(TestClock.layer()));
685 });
686
687 it.effect("times out while reading a streaming response body", () => {
688 const http = HttpClient.make((request) =>
689 Effect.succeed(
690 HttpClientResponse.fromWeb(
691 request,
692 new Response(
693 new ReadableStream<Uint8Array>({
694 start(controller) {
695 controller.enqueue(new TextEncoder().encode('{"data":'));
696 },
697 }),
698 {
699 status: 201,
700 headers: { "content-type": "application/json" },
701 },
702 ),
703 ),
704 ),
705 );
706 const layer = layerForHttp(http);
707
708 return Effect.gen(function* () {
709 const fiber = yield* withClient((client) =>
710 client.getProject("project-1"),
711 ).pipe(
712 Effect.provide(layer),
713 Effect.flip,
714 Effect.forkChild({ startImmediately: true }),
715 );
716 yield* TestClock.adjust("11 seconds");
717 const error = yield* Fiber.join(fiber);
718
719 expect(error).toBeInstanceOf(PrismaApiError);
720 if (error instanceof PrismaApiError) {
721 expect(error.status).toBe(0);
722 expect(error.message).toContain("timed out after 10 seconds");
723 }
724 }).pipe(Effect.provide(TestClock.layer()));
725 });
726
727 it.effect("rejects oversized successful response bodies", () => {
728 const http = HttpClient.make((request) =>
729 Effect.succeed(
730 HttpClientResponse.fromWeb(
731 request,
732 new Response("x".repeat(4 * 1024 * 1024 + 1), {
733 status: 201,
734 headers: { "content-type": "application/json" },
735 }),
736 ),
737 ),
738 );
739
740 return withClient((client) => client.createProject({ name: "api" })).pipe(
741 Effect.provide(layerForHttp(http)),
742 Effect.flip,
743 Effect.map((error) => {
744 expect(error).toBeInstanceOf(PrismaApiDecodeError);
745 expect(error.message).toContain("4194304 byte safety limit");
746 }),
747 );
748 });
749
750 it.effect("bounds and redacts oversized API error bodies", () => {
751 const secret = "do-not-retain-this-error-body";
752 const http = HttpClient.make((request) =>
753 Effect.succeed(
754 HttpClientResponse.fromWeb(
755 request,
756 new Response(secret.repeat(3_000), {
757 status: 400,
758 headers: { "content-type": "text/plain" },
759 }),
760 ),
761 ),
762 );
763
764 return withClient((client) => client.createProject({ name: "api" })).pipe(
765 Effect.provide(layerForHttp(http)),
766 Effect.flip,
767 Effect.map((error) => {
768 expect(error).toBeInstanceOf(PrismaApiError);
769 expect(error.message).toContain("65536 byte safety limit");
770 expect(JSON.stringify(error)).not.toContain(secret);
771 if (error instanceof PrismaApiError) {
772 expect(error.status).toBe(400);
773 expect(error.body).toBeUndefined();
774 }
775 }),
776 );
777 });
778
779 it.effect("does not expose plain-text API error bodies in messages", () => {
780 const secret = "postgres://admin:plain-text-secret@db.example.test/main";
781 const http = HttpClient.make((request) =>
782 Effect.succeed(
783 HttpClientResponse.fromWeb(
784 request,
785 new Response(secret, {
786 status: 400,
787 headers: { "content-type": "text/plain" },
788 }),
789 ),
790 ),
791 );
792
793 return withClient((client) => client.createProject({ name: "api" })).pipe(
794 Effect.provide(layerForHttp(http)),
795 Effect.flip,
796 Effect.map((error) => {
797 expect(error).toBeInstanceOf(PrismaApiError);
798 expect(error.message).toBe("HTTP 400");
799 expect(String(error)).not.toContain(secret);
800 expect(JSON.stringify(error)).not.toContain(secret);
801 if (error instanceof PrismaApiError) {
802 expect(Redacted.value(error.body!)).toBe(secret);
803 }
804 }),
805 );
806 });
807
808 it.effect("exposes only safe codes from structured API errors", () => {
809 const secret = "postgres://admin:structured-secret@db.example.test/main";
810 const responseBody = {
811 error: {
812 code: "state:not_found",
813 message: secret,
814 hint: secret,
815 },
816 };
817 const http = HttpClient.make((request) =>
818 Effect.succeed(
819 HttpClientResponse.fromWeb(
820 request,
821 json(responseBody, { status: 404 }),
822 ),
823 ),
824 );
825
826 return withClient((client) => client.createProject({ name: "api" })).pipe(
827 Effect.provide(layerForHttp(http)),
828 Effect.flip,
829 Effect.map((error) => {
830 expect(error).toBeInstanceOf(PrismaApiError);
831 expect(error.message).toBe(
832 "Prisma Management API request failed (state:not_found)",
833 );
834 expect(String(error)).not.toContain(secret);
835 expect(JSON.stringify(error)).not.toContain(secret);
836 }),
837 );
838 });
839
840 it.effect("uses canonical app and deployment management routes", () => {
841 const { layer, captured } = routeCoverageHarness();
842
843 return withClient((client) =>
844 Effect.gen(function* () {
845 yield* client.createProjectDatabase("project-1", {
846 name: "main",
847 region: "us-east-1",
848 });
849 yield* client.createApp({
850 projectId: "project-1",
851 displayName: "api",
852 regionId: "us-east-1",
853 });
854 yield* client.createAppDeployment("app-1", {
855 portMapping: { http: 3000 },
856 });
857 yield* client.getDeployment("deployment-1");
858 yield* client.startDeployment("deployment-1");
859 yield* client.stopDeployment("deployment-1");
860 yield* client.deleteDeployment("deployment-1");
861 yield* client.listWorkspaceIntegrations("workspace-1", { limit: 10 });
862
863 expect(
864 captured.map((request) => [
865 request.method,
866 `${request.pathname}${request.search}`,
867 ]),
868 ).toEqual([
869 ["POST", "/v1/projects/project-1/databases"],
870 ["POST", "/v1/apps"],
871 ["POST", "/v1/apps/app-1/deployments"],
872 ["GET", "/v1/deployments/deployment-1"],
873 ["POST", "/v1/deployments/deployment-1/start"],
874 ["POST", "/v1/deployments/deployment-1/stop"],
875 ["DELETE", "/v1/deployments/deployment-1"],
876 ["GET", "/v1/workspaces/workspace-1/integrations?limit=10"],
877 ]);
878 expect(captured[0]?.bodyJson).toEqual({
879 name: "main",
880 region: "us-east-1",
881 });
882 expect(captured[2]?.bodyJson).toEqual({
883 portMapping: { http: 3000 },
884 });
885 }),
886 ).pipe(Effect.provide(layer));
887 });
888
889 it.effect("retries transient API failures for destructive requests", () => {
890 const captured: Captured[] = [];
891 let attempts = 0;
892 const http = HttpClient.make((request) =>
893 Effect.sync(() => {
894 attempts += 1;
895 const url = new URL(request.url);
896 captured.push({
897 url: request.url,
898 method: request.method,
899 pathname: url.pathname,
900 search: url.search,
901 authorization: request.headers.authorization,
902 bodyJson: undefined,
903 });
904 return HttpClientResponse.fromWeb(
905 request,
906 attempts < 3
907 ? json(
908 {
909 error: {
910 message: "transient platform failure",
911 },
912 },
913 { status: 500 },
914 )
915 : empty(),
916 );
917 }),
918 );
919 const layer = PrismaClientLive.pipe(
920 Layer.provide(
921 Layer.mergeAll(
922 Layer.succeed(HttpClient.HttpClient, http),
923 Layer.succeed(PrismaEnvironment, {
924 type: "serviceToken" as const,
925 serviceToken: Redacted.make("test-token"),
926 source: { type: "stored" as const },
927 baseUrl: "https://api.prisma.test",
928 }),
929 ),
930 ),
931 );
932
933 return Effect.gen(function* () {
934 const fiber = yield* withClient((client) =>
935 client.deleteDeployment("deployment-1"),
936 ).pipe(
937 Effect.provide(layer),
938 Effect.forkChild({ startImmediately: true }),
939 );
940 yield* TestClock.adjust("1 second");
941 yield* Fiber.join(fiber);
942
943 expect(attempts).toBe(3);
944 expect(captured.map((request) => request.method)).toEqual([
945 "DELETE",
946 "DELETE",
947 "DELETE",
948 ]);
949 expect(captured.map((request) => request.pathname)).toEqual([
950 "/v1/deployments/deployment-1",
951 "/v1/deployments/deployment-1",
952 "/v1/deployments/deployment-1",
953 ]);
954 }).pipe(Effect.provide(TestClock.layer()));
955 });
956
957 it.effect("retries transient API failures for safe lifecycle posts", () => {
958 const captured: Captured[] = [];
959 let attempts = 0;
960 const http = HttpClient.make((request) =>
961 Effect.sync(() => {
962 attempts += 1;
963 const url = new URL(request.url);
964 captured.push({
965 url: request.url,
966 method: request.method,
967 pathname: url.pathname,
968 search: url.search,
969 authorization: request.headers.authorization,
970 bodyJson: undefined,
971 });
972 return HttpClientResponse.fromWeb(
973 request,
974 attempts < 3
975 ? json(
976 {
977 error: {
978 message: "transient platform failure",
979 },
980 },
981 { status: 500 },
982 )
983 : data({
984 id: "deployment-1",
985 type: "deployment",
986 url: "https://api.prisma.test/v1/deployments/deployment-1",
987 foundryVersionId: "foundry-1",
988 status: "running",
989 previewDomain: "version-1.prisma.test",
990 }),
991 );
992 }),
993 );
994 const layer = PrismaClientLive.pipe(
995 Layer.provide(
996 Layer.mergeAll(
997 Layer.succeed(HttpClient.HttpClient, http),
998 Layer.succeed(PrismaEnvironment, {
999 type: "serviceToken" as const,
1000 serviceToken: Redacted.make("test-token"),
1001 source: { type: "stored" as const },
1002 baseUrl: "https://api.prisma.test",
1003 }),
1004 ),
1005 ),
1006 );
1007
1008 return Effect.gen(function* () {
1009 const fiber = yield* withClient((client) =>
1010 client.startDeployment("deployment-1"),
1011 ).pipe(
1012 Effect.provide(layer),
1013 Effect.forkChild({ startImmediately: true }),
1014 );
1015 yield* TestClock.adjust("1 second");
1016 const version = yield* Fiber.join(fiber);
1017
1018 expect(attempts).toBe(3);
1019 expect(version.previewDomain).toBe("version-1.prisma.test");
1020 expect(captured.map((request) => request.method)).toEqual([
1021 "POST",
1022 "POST",
1023 "POST",
1024 ]);
1025 expect(captured.map((request) => request.pathname)).toEqual([
1026 "/v1/deployments/deployment-1/start",
1027 "/v1/deployments/deployment-1/start",
1028 "/v1/deployments/deployment-1/start",
1029 ]);
1030 }).pipe(Effect.provide(TestClock.layer()));
1031 });
1032
1033 it.effect("does not retry transient API failures for create requests", () => {
1034 const captured: Captured[] = [];
1035 let attempts = 0;
1036 const http = HttpClient.make((request) =>
1037 Effect.sync(() => {
1038 attempts += 1;
1039 const url = new URL(request.url);
1040 captured.push({
1041 url: request.url,
1042 method: request.method,
1043 pathname: url.pathname,
1044 search: url.search,
1045 authorization: request.headers.authorization,
1046 bodyJson: undefined,
1047 });
1048 return HttpClientResponse.fromWeb(
1049 request,
1050 json(
1051 {
1052 error: {
1053 message: "transient platform failure",
1054 },
1055 },
1056 { status: 500 },
1057 ),
1058 );
1059 }),
1060 );
1061 const layer = PrismaClientLive.pipe(
1062 Layer.provide(
1063 Layer.mergeAll(
1064 Layer.succeed(HttpClient.HttpClient, http),
1065 Layer.succeed(PrismaEnvironment, {
1066 type: "serviceToken" as const,
1067 serviceToken: Redacted.make("test-token"),
1068 source: { type: "stored" as const },
1069 baseUrl: "https://api.prisma.test",
1070 }),
1071 ),
1072 ),
1073 );
1074
1075 return Effect.gen(function* () {
1076 const error = yield* withClient((client) =>
1077 client.createAppDeployment("app-1", {
1078 portMapping: { http: 3000 },
1079 }),
1080 ).pipe(Effect.provide(layer), Effect.flip);
1081
1082 expect(error).toBeInstanceOf(PrismaApiError);
1083 expect(attempts).toBe(1);
1084 expect(captured.map((request) => request.method)).toEqual(["POST"]);
1085 expect(captured.map((request) => request.pathname)).toEqual([
1086 "/v1/apps/app-1/deployments",
1087 ]);
1088 });
1089 });
1090
1091 it.effect(
1092 "does not retry transient API failures for transfer requests",
1093 () => {
1094 const captured: Captured[] = [];
1095 let attempts = 0;
1096 const http = HttpClient.make((request) =>
1097 Effect.sync(() => {
1098 attempts += 1;
1099 const url = new URL(request.url);
1100 captured.push({
1101 url: request.url,
1102 method: request.method,
1103 pathname: url.pathname,
1104 search: url.search,
1105 authorization: request.headers.authorization,
1106 bodyJson: undefined,
1107 });
1108 return HttpClientResponse.fromWeb(
1109 request,
1110 json(
1111 {
1112 error: {
1113 message: "transient platform failure",
1114 },
1115 },
1116 { status: 500 },
1117 ),
1118 );
1119 }),
1120 );
1121 const layer = PrismaClientLive.pipe(
1122 Layer.provide(
1123 Layer.mergeAll(
1124 Layer.succeed(HttpClient.HttpClient, http),
1125 Layer.succeed(PrismaEnvironment, {
1126 type: "serviceToken" as const,
1127 serviceToken: Redacted.make("test-token"),
1128 source: { type: "stored" as const },
1129 baseUrl: "https://api.prisma.test",
1130 }),
1131 ),
1132 ),
1133 );
1134
1135 return Effect.gen(function* () {
1136 const error = yield* withClient((client) =>
1137 client.transferProject("project-1", {
1138 recipientAccessToken: "recipient-token",
1139 }),
1140 ).pipe(Effect.provide(layer), Effect.flip);
1141
1142 expect(error).toBeInstanceOf(PrismaApiError);
1143 expect(attempts).toBe(1);
1144 expect(captured.map((request) => request.method)).toEqual(["POST"]);
1145 expect(captured.map((request) => request.pathname)).toEqual([
1146 "/v1/projects/project-1/transfer",
1147 ]);
1148 });
1149 },
1150 );
1151
1152 it.effect("retries transient transport failures", () => {
1153 let attempts = 0;
1154 const http = HttpClient.make((request) =>
1155 Effect.sync(() => {
1156 attempts += 1;
1157 return attempts < 3
1158 ? new HttpClientError.HttpClientError({
1159 reason: new HttpClientError.TransportError({
1160 request,
1161 cause: new Error("connection reset"),
1162 description: "test transport failure",
1163 }),
1164 })
1165 : undefined;
1166 }).pipe(
1167 Effect.flatMap((error) =>
1168 error
1169 ? Effect.fail(error)
1170 : Effect.succeed(
1171 HttpClientResponse.fromWeb(
1172 request,
1173 page([{ id: "project-1" }]),
1174 ),
1175 ),
1176 ),
1177 ),
1178 );
1179 const layer = PrismaClientLive.pipe(
1180 Layer.provide(
1181 Layer.mergeAll(
1182 Layer.succeed(HttpClient.HttpClient, http),
1183 Layer.succeed(PrismaEnvironment, {
1184 type: "serviceToken" as const,
1185 serviceToken: Redacted.make("test-token"),
1186 source: { type: "stored" as const },
1187 baseUrl: "https://api.prisma.test",
1188 }),
1189 ),
1190 ),
1191 );
1192
1193 return Effect.gen(function* () {
1194 const fiber = yield* withClient((client) => client.listProjects()).pipe(
1195 Effect.provide(layer),
1196 Effect.forkChild({ startImmediately: true }),
1197 );
1198 yield* TestClock.adjust("1 second");
1199 const projects = yield* Fiber.join(fiber);
1200
1201 expect(attempts).toBe(3);
1202 expect(projects.map((project) => project.id)).toEqual(["project-1"]);
1203 }).pipe(Effect.provide(TestClock.layer()));
1204 });
1205
1206 it.effect("preserves backup list metadata", () => {
1207 const { layer, captured } = harness();
1208
1209 return withClient((client) =>
1210 Effect.gen(function* () {
1211 const backups = yield* client.listBackups("database-1", { limit: 1 });
1212
1213 expect(backups).toEqual({
1214 data: [
1215 {
1216 id: "backup-1",
1217 type: "backup",
1218 backupType: "full",
1219 createdAt: "2026-01-01T00:00:00Z",
1220 status: "completed",
1221 },
1222 ],
1223 meta: {
1224 backupRetentionDays: 7,
1225 },
1226 pagination: {
1227 hasMore: false,
1228 limit: 1,
1229 },
1230 });
1231 expect(captured.map((request) => request.pathname)).toEqual([
1232 "/v1/databases/database-1/backups",
1233 ]);
1234 expect(captured[0]?.search).toBe("?limit=1");
1235 }),
1236 ).pipe(Effect.provide(layer));
1237 });
1238
1239 it.effect("reads non-paginated region endpoints", () => {
1240 const { layer } = harness();
1241
1242 return withClient((client) =>
1243 Effect.gen(function* () {
1244 const regions = yield* client.listRegions({ product: "postgres" });
1245 const postgresRegions = yield* client.listPostgresRegions();
1246 const accelerateRegions = yield* client.listAccelerateRegions();
1247
1248 expect(regions.map((region) => region.id)).toEqual(["us-east-1"]);
1249 expect(postgresRegions.map((region) => region.id)).toEqual([
1250 "us-east-1",
1251 ]);
1252 expect(accelerateRegions.map((region) => region.id)).toEqual([
1253 "us-east-1",
1254 ]);
1255 }),
1256 ).pipe(Effect.provide(layer));
1257 });
1258
1259 it.effect("builds authenticated deployment log stream requests", () => {
1260 const { layer, captured } = harness("https://api.prisma.test");
1261
1262 return withClient((client) =>
1263 Effect.gen(function* () {
1264 const request = yield* client.getDeploymentLogsRequest("deployment-1", {
1265 tail: 100,
1266 fromStart: true,
1267 cursor: "byte-42",
1268 });
1269 expect(request.url).toBe(
1270 "wss://api.prisma.test/v1/deployments/deployment-1/logs?tail=100&cursor=byte-42&from_start=true",
1271 );
1272 expect(Redacted.value(request.headers.Authorization)).toBe(
1273 "Bearer test-token",
1274 );
1275 expect(captured).toEqual([]);
1276 }),
1277 ).pipe(Effect.provide(layer));
1278 });
1279
1280 it.effect(
1281 "maps every supported Management API operation to its route",
1282 () => {
1283 const { layer, captured } = routeCoverageHarness();
1284
1285 return withClient((client) =>
1286 Effect.gen(function* () {
1287 yield* client.listWorkspaces({ limit: 1 });
1288 yield* client.getWorkspace("workspace-1");
1289 yield* client.getCurrentPrincipal();
1290 yield* client.listRegions({ product: "postgres" });
1291 yield* client.listPostgresRegions();
1292 yield* client.listAccelerateRegions();
1293
1294 yield* client.listProjects({ limit: 1 });
1295 yield* client.getProject("project-1");
1296 yield* client.createProject({ name: "app", region: "us-east-1" });
1297 yield* client.updateProject("project-1", { name: "renamed" });
1298 yield* client.deleteProject("project-1");
1299 yield* client.transferProject("project-1", {
1300 recipientAccessToken: "recipient-token",
1301 });
1302
1303 yield* client.listDatabases({
1304 projectId: "project-1",
1305 branchGitName: "main",
1306 });
1307 yield* client.listProjectDatabases("project-1", { limit: 1 });
1308 yield* client.getDatabase("database-1");
1309 yield* client.createDatabase({
1310 projectId: "project-1",
1311 name: "main",
1312 });
1313 yield* client.createProjectDatabase("project-1", {
1314 name: "main",
1315 source: {
1316 type: "backup",
1317 databaseId: "database-source",
1318 backupId: "backup-1",
1319 },
1320 });
1321 yield* client.updateDatabase("database-1", { name: "main-2" });
1322 yield* client.deleteDatabase("database-1");
1323 yield* client.listBackups("database-1", { limit: 1 });
1324 yield* client.restoreDatabase("database-1", {
1325 source: {
1326 type: "backup",
1327 databaseId: "database-source",
1328 backupId: "backup-1",
1329 },
1330 });
1331 yield* client.getDatabaseUsage("database-1", {
1332 startDate: "2026-01-01",
1333 });
1334
1335 yield* client.listConnections({ databaseId: "database-1" });
1336 yield* client.listDatabaseConnections("database-1", { limit: 1 });
1337 yield* client.getConnection("connection-1");
1338 yield* client.createConnection({
1339 databaseId: "database-1",
1340 name: "direct",
1341 });
1342 yield* client.createDatabaseConnection("database-1", {
1343 name: "direct",
1344 });
1345 yield* client.deleteConnection("connection-1");
1346 yield* client.rotateConnection("connection-1");
1347
1348 yield* client.listBranches("project-1", { gitName: "main" });
1349 yield* client.getBranch("branch-1");
1350 yield* client.createBranch("project-1", { gitName: "main" });
1351 yield* client.updateBranch("branch-1", { isDefault: true });
1352 yield* client.deleteBranch("branch-1");
1353
1354 yield* client.listBuckets({ projectId: "project-1" });
1355 yield* client.getBucket("bucket-1");
1356 yield* client.createBucket({
1357 projectId: "project-1",
1358 name: "uploads",
1359 });
1360 yield* client.deleteBucket("bucket-1");
1361 yield* client.listBucketKeys("bucket-1", { limit: 1 });
1362 yield* client.createBucketKey("bucket-1", { role: "read_write" });
1363 yield* client.deleteBucketKey("bucket-1", "key-1");
1364
1365 yield* client.getCustomDomain("domain-1");
1366 yield* client.deleteCustomDomain("domain-1");
1367 yield* client.retryCustomDomain("domain-1");
1368
1369 yield* client.listEnvironmentVariables({
1370 projectId: "project-1",
1371 class: "production",
1372 key: "TOKEN",
1373 });
1374 yield* client.getEnvironmentVariable("env-1");
1375 yield* client.createEnvironmentVariable({
1376 projectId: "project-1",
1377 class: "production",
1378 key: "TOKEN",
1379 value: "secret",
1380 });
1381 yield* client.updateEnvironmentVariable("env-1", {
1382 value: "secret-2",
1383 });
1384 yield* client.deleteEnvironmentVariable("env-1");
1385
1386 yield* client.listIntegrations({ workspaceId: "workspace-1" });
1387 yield* client.listWorkspaceIntegrations("workspace-1", { limit: 1 });
1388 yield* client.getIntegration("integration-1");
1389 yield* client.deleteIntegration("integration-1");
1390 yield* client.revokeWorkspaceIntegration("workspace-1", "client-1");
1391
1392 yield* client.listScmInstallations({ workspaceId: "workspace-1" });
1393 yield* client.createScmInstallIntent({
1394 provider: "github",
1395 workspaceId: "workspace-1",
1396 });
1397 yield* client.listScmInstallationRepositories("scminstall-1", {
1398 limit: 10,
1399 });
1400
1401 yield* client.listSourceRepositories({ projectId: "project-1" });
1402 yield* client.getSourceRepository("repo-1");
1403 yield* client.createSourceRepository({
1404 projectId: "project-1",
1405 provider: "github",
1406 providerRepositoryId: 123,
1407 installationId: "scminstall-1",
1408 });
1409 yield* client.deleteSourceRepository("repo-1");
1410
1411 yield* client.listApps({
1412 projectId: "project-1",
1413 branchGitName: "main",
1414 });
1415 yield* client.getApp("app-1");
1416 yield* client.createApp({
1417 projectId: "project-1",
1418 displayName: "web",
1419 });
1420 yield* client.updateApp("app-1", { displayName: "web-2" });
1421 yield* client.deleteApp("app-1");
1422 yield* client.promoteApp("app-1", { deploymentId: "deployment-1" });
1423 yield* client.rollbackApp("app-1", { deploymentId: "deployment-1" });
1424 yield* client.listAppDomains("app-1");
1425 yield* client.createAppDomain("app-1", {
1426 hostname: "web.example.com",
1427 });
1428 yield* client.listAppDeployments("app-1", { limit: 1 });
1429 yield* client.createAppDeployment("app-1", {
1430 skipCodeUpload: true,
1431 });
1432 yield* client.getDeployment("deployment-1");
1433 yield* client.deleteDeployment("deployment-1");
1434 yield* client.startDeployment("deployment-1");
1435 yield* client.stopDeployment("deployment-1");
1436 const deploymentLogsRequest = yield* client.getDeploymentLogsRequest(
1437 "deployment-1",
1438 { tail: 10 },
1439 );
1440 expect(deploymentLogsRequest.url).toBe(
1441 "wss://api.prisma.test/v1/deployments/deployment-1/logs?tail=10",
1442 );
1443 expect(
1444 Redacted.value(deploymentLogsRequest.headers.Authorization),
1445 ).toBe("Bearer test-token");
1446 const buildLogsRequest = yield* client.getBuildLogsRequest(
1447 "build-1",
1448 { follow: true, cursor: "cursor-1" },
1449 );
1450 expect(buildLogsRequest.url).toBe(
1451 "https://api.prisma.test/v1/builds/build-1/logs?follow=true&cursor=cursor-1",
1452 );
1453 expect(Redacted.value(buildLogsRequest.headers.Authorization)).toBe(
1454 "Bearer test-token",
1455 );
1456 expect(buildLogsRequest.headers.Accept).toBe("application/x-ndjson");
1457
1458 expect(
1459 captured.map((request) => [
1460 request.method,
1461 `${request.pathname}${request.search}`,
1462 ]),
1463 ).toEqual([
1464 ["GET", "/v1/workspaces?limit=1"],
1465 ["GET", "/v1/workspaces/workspace-1"],
1466 ["GET", "/v1/me"],
1467 ["GET", "/v1/regions?product=postgres"],
1468 ["GET", "/v1/regions/postgres"],
1469 ["GET", "/v1/regions/accelerate"],
1470 ["GET", "/v1/projects?limit=1"],
1471 ["GET", "/v1/projects/project-1"],
1472 ["POST", "/v1/projects"],
1473 ["PATCH", "/v1/projects/project-1"],
1474 ["DELETE", "/v1/projects/project-1"],
1475 ["POST", "/v1/projects/project-1/transfer"],
1476 ["GET", "/v1/databases?projectId=project-1&branchGitName=main"],
1477 ["GET", "/v1/projects/project-1/databases?limit=1"],
1478 ["GET", "/v1/databases/database-1"],
1479 ["POST", "/v1/databases"],
1480 ["POST", "/v1/projects/project-1/databases"],
1481 ["PATCH", "/v1/databases/database-1"],
1482 ["DELETE", "/v1/databases/database-1"],
1483 ["GET", "/v1/databases/database-1/backups?limit=1"],
1484 ["POST", "/v1/databases/database-1/restore"],
1485 ["GET", "/v1/databases/database-1/usage?startDate=2026-01-01"],
1486 ["GET", "/v1/connections?databaseId=database-1"],
1487 ["GET", "/v1/databases/database-1/connections?limit=1"],
1488 ["GET", "/v1/connections/connection-1"],
1489 ["POST", "/v1/connections"],
1490 ["POST", "/v1/databases/database-1/connections"],
1491 ["DELETE", "/v1/connections/connection-1"],
1492 ["POST", "/v1/connections/connection-1/rotate"],
1493 ["GET", "/v1/projects/project-1/branches?gitName=main"],
1494 ["GET", "/v1/branches/branch-1"],
1495 ["POST", "/v1/projects/project-1/branches"],
1496 ["PATCH", "/v1/branches/branch-1"],
1497 ["DELETE", "/v1/branches/branch-1"],
1498 ["GET", "/v1/buckets?projectId=project-1"],
1499 ["GET", "/v1/buckets/bucket-1"],
1500 ["POST", "/v1/buckets"],
1501 ["DELETE", "/v1/buckets/bucket-1"],
1502 ["GET", "/v1/buckets/bucket-1/keys?limit=1"],
1503 ["POST", "/v1/buckets/bucket-1/keys"],
1504 ["DELETE", "/v1/buckets/bucket-1/keys/key-1"],
1505 ["GET", "/v1/domains/domain-1"],
1506 ["DELETE", "/v1/domains/domain-1"],
1507 ["POST", "/v1/domains/domain-1/retry"],
1508 [
1509 "GET",
1510 "/v1/environment-variables?projectId=project-1&class=production&key=TOKEN",
1511 ],
1512 ["GET", "/v1/environment-variables/env-1"],
1513 ["POST", "/v1/environment-variables"],
1514 ["PATCH", "/v1/environment-variables/env-1"],
1515 ["DELETE", "/v1/environment-variables/env-1"],
1516 ["GET", "/v1/integrations?workspaceId=workspace-1"],
1517 ["GET", "/v1/workspaces/workspace-1/integrations?limit=1"],
1518 ["GET", "/v1/integrations/integration-1"],
1519 ["DELETE", "/v1/integrations/integration-1"],
1520 ["DELETE", "/v1/workspaces/workspace-1/integrations/client-1"],
1521 ["GET", "/v1/scm-installations?workspaceId=workspace-1"],
1522 ["POST", "/v1/scm-installations/install-intents"],
1523 ["GET", "/v1/scm-installations/scminstall-1/repositories?limit=10"],
1524 ["GET", "/v1/source-repositories?projectId=project-1"],
1525 ["GET", "/v1/source-repositories/repo-1"],
1526 ["POST", "/v1/source-repositories"],
1527 ["DELETE", "/v1/source-repositories/repo-1"],
1528 ["GET", "/v1/apps?projectId=project-1&branchGitName=main"],
1529 ["GET", "/v1/apps/app-1"],
1530 ["POST", "/v1/apps"],
1531 ["PATCH", "/v1/apps/app-1"],
1532 ["DELETE", "/v1/apps/app-1"],
1533 ["POST", "/v1/apps/app-1/promote"],
1534 ["POST", "/v1/apps/app-1/rollback"],
1535 ["GET", "/v1/apps/app-1/domains"],
1536 ["POST", "/v1/apps/app-1/domains"],
1537 ["GET", "/v1/apps/app-1/deployments?limit=1"],
1538 ["POST", "/v1/apps/app-1/deployments"],
1539 ["GET", "/v1/deployments/deployment-1"],
1540 ["DELETE", "/v1/deployments/deployment-1"],
1541 ["POST", "/v1/deployments/deployment-1/start"],
1542 ["POST", "/v1/deployments/deployment-1/stop"],
1543 ]);
1544 expect(routeInventoryFrom(captured)).toEqual(
1545 expectedManagementApiRoutes,
1546 );
1547 expect(expectedManagementApiRoutes).toHaveLength(78);
1548 expect(captured[11]?.bodyJson).toEqual({
1549 recipientAccessToken: "recipient-token",
1550 });
1551 const restoreRequest = captured.find(
1552 (request) =>
1553 request.method === "POST" &&
1554 request.pathname === "/v1/databases/database-1/restore",
1555 );
1556 expect(restoreRequest?.bodyJson).toEqual({
1557 source: {
1558 type: "backup",
1559 databaseId: "database-source",
1560 backupId: "backup-1",
1561 },
1562 });
1563 const projectDatabaseRequest = captured.find(
1564 (request) =>
1565 request.method === "POST" &&
1566 request.pathname === "/v1/projects/project-1/databases",
1567 );
1568 expect(projectDatabaseRequest?.bodyJson).toEqual({
1569 name: "main",
1570 source: {
1571 type: "backup",
1572 databaseId: "database-source",
1573 backupId: "backup-1",
1574 },
1575 });
1576
1577 const createEnvRequest = captured.find(
1578 (request) =>
1579 request.method === "POST" &&
1580 request.pathname === "/v1/environment-variables",
1581 );
1582 expect(createEnvRequest?.bodyJson).toEqual({
1583 projectId: "project-1",
1584 class: "production",
1585 key: "TOKEN",
1586 value: "secret",
1587 });
1588
1589 const updateEnvRequest = captured.find(
1590 (request) =>
1591 request.method === "PATCH" &&
1592 request.pathname === "/v1/environment-variables/env-1",
1593 );
1594 expect(updateEnvRequest?.bodyJson).toEqual({
1595 value: "secret-2",
1596 });
1597
1598 const sourceRepositoryRequest = captured.find(
1599 (request) =>
1600 request.method === "POST" &&
1601 request.pathname === "/v1/source-repositories",
1602 );
1603 expect(sourceRepositoryRequest?.bodyJson).toEqual({
1604 projectId: "project-1",
1605 provider: "github",
1606 providerRepositoryId: 123,
1607 installationId: "scminstall-1",
1608 });
1609
1610 const installIntentRequest = captured.find(
1611 (request) =>
1612 request.method === "POST" &&
1613 request.pathname === "/v1/scm-installations/install-intents",
1614 );
1615 expect(installIntentRequest?.bodyJson).toEqual({
1616 provider: "github",
1617 workspaceId: "workspace-1",
1618 });
1619 }),
1620 ).pipe(Effect.provide(layer));
1621 },
1622 );
1623
1624 it("matches the pinned Management API route contract", () => {
1625 expect(expectedManagementApiRoutes).toEqual(
1626 [...productionManagementApiRoutes].sort(),
1627 );
1628 });
1629});