1import {
2 AuthOrchestrationOperateScope,
3 EnvironmentAuthorizationError,
4 EnvironmentId,
5} from "@t3tools/contracts";
6import { runAttachmentUploadCycle } from "@t3tools/client-runtime/state/attachments";
7import * as Cause from "effect/Cause";
8import { AsyncResult, AtomRegistry } from "effect/reactivity";
9import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
10
11const state = vi.hoisted(() => ({
12 grantedEnvironments: new Set<string>(),
13 create: vi.fn(),
14 remove: vi.fn(),
15}));
16vi.mock("../connection/runtime", () => ({ connectionAtomRuntime: {} }));
17vi.mock("./session", () => ({
18 readEnvironmentScope: (environmentId: string, scope: string) =>
19 scope === AuthOrchestrationOperateScope && state.grantedEnvironments.has(environmentId),
20}));
21vi.mock("@t3tools/client-runtime/state/attachments", async (importOriginal) => ({
22 ...(await importOriginal<typeof import("@t3tools/client-runtime/state/attachments")>()),
23 createAttachmentEnvironmentAtoms: () => ({
24 createUploadUrl: { label: "create", run: state.create },
25 remove: { label: "remove", run: state.remove },
26 }),
27}));
28
29import { attachmentEnvironment } from "./attachments";
30
31const environmentId = EnvironmentId.make("secondary");
32const registry = AtomRegistry.make();
33beforeEach(() => {
34 state.grantedEnvironments = new Set(["primary"]);
35 state.create.mockReset().mockResolvedValue(
36 AsyncResult.success({
37 attachmentId: "pending",
38 relativeUrl: "/upload",
39 expiresAt: 1,
40 }),
41 );
42 state.remove.mockReset().mockResolvedValue(AsyncResult.success(undefined));
43});
44afterEach(() => {
45 vi.restoreAllMocks();
46});
47
48describe("attachment mutation grants", () => {
49 it("uses the attachment environment for both mint and deletion", async () => {
50 const mintResult = await attachmentEnvironment.createUploadUrl.run(registry, {
51 environmentId,
52 input: { name: "image.png", mimeType: "image/png", sizeBytes: 1 },
53 });
54 const removeResult = await attachmentEnvironment.remove.run(registry, {
55 environmentId,
56 input: { attachmentId: "pending" },
57 });
58 for (const result of [mintResult, removeResult]) {
59 expect(result._tag).toBe("Failure");
60 if (result._tag !== "Failure") throw new Error("Expected permission denial");
61 const error = Cause.squash<unknown>(result.cause);
62 expect(error).toBeInstanceOf(EnvironmentAuthorizationError);
63 expect(error).toMatchObject({ requiredScope: AuthOrchestrationOperateScope });
64 }
65 expect(state.create).not.toHaveBeenCalled();
66 expect(state.remove).not.toHaveBeenCalled();
67 state.grantedEnvironments.add("secondary");
68 expect(
69 (
70 await attachmentEnvironment.remove.run(registry, {
71 environmentId,
72 input: { attachmentId: "pending" },
73 })
74 )._tag,
75 ).toBe("Success");
76 expect(state.remove).toHaveBeenCalledOnce();
77 });
78
79 it("blocks the upload cycle's asynchronous cleanup after grant revocation", async () => {
80 state.grantedEnvironments.add("secondary");
81 const transport = vi.fn();
82 const result = await runAttachmentUploadCycle({
83 registry,
84 ...attachmentEnvironment,
85 environmentId,
86 upload: { name: "image.png", mimeType: "image/png", sizeBytes: 1 },
87 resolveUploadUrl: () => "https://example.test/upload",
88 transport,
89 onMinted: () => {
90 state.grantedEnvironments.delete("secondary");
91 return "cancel";
92 },
93 });
94 expect(result.status).toBe("cancelled");
95 expect(state.create).toHaveBeenCalledOnce();
96 expect(state.remove).not.toHaveBeenCalled();
97 expect(transport).not.toHaveBeenCalled();
98 });
99});