1import {
2 AuthProvider,
3 AuthProviders,
4 describeEnvironment,
5 getAuthProvider,
6} from "@/Auth/AuthProvider.ts";
7import { getEnvRedactedRequired } from "@/Auth/Env.ts";
8import { Interaction } from "@/Interaction.ts";
9import * as NodeServices from "@effect/platform-node/NodeServices";
10import * as Cause from "effect/Cause";
11import * as Effect from "effect/Effect";
12import * as Exit from "effect/Exit";
13import * as Schema from "effect/Schema";
14import { expect, it } from "alchemy-test";
15
16const implementation = {
17 configSchema: Schema.Struct({ method: Schema.Literal("custom") }),
18 configure: () => Effect.succeed({ method: "custom" as const }),
19 login: () => Effect.void,
20 logout: () => Effect.void,
21 details: () => Effect.succeed({ lines: [] }),
22 read: () => Effect.void,
23 readEnvironment: getEnvRedactedRequired("CUSTOM_PROVIDER_TOKEN").pipe(
24 Effect.asVoid,
25 ),
26 environment: [
27 { name: "CUSTOM_PROVIDER_TOKEN", required: true, secret: true },
28 {
29 name: "CUSTOM_PROVIDER_REGION",
30 required: false,
31 alternatives: ["CUSTOM_PROVIDER_DEFAULT_REGION"],
32 },
33 ],
34};
35
36it.effect(
37 "auth providers expose their declared environment contract",
38 () =>
39 Effect.gen(function* () {
40 yield* AuthProvider<{ method: "custom" }, void>()(
41 "CustomProvider",
42 implementation,
43 );
44 const provider = yield* getAuthProvider("CustomProvider");
45
46 expect(provider.environment).toEqual(implementation.environment);
47 expect(describeEnvironment(provider.environment)).toBe(
48 "CUSTOM_PROVIDER_TOKEN, [CUSTOM_PROVIDER_REGION | CUSTOM_PROVIDER_DEFAULT_REGION]",
49 );
50 }).pipe(
51 Effect.provideService(AuthProviders, {}),
52 Effect.provide(NodeServices.layer),
53 ),
54 { tags: ["unit", "local"] },
55);
56
57it.effect(
58 "providers without environment credentials declare nothing",
59 () =>
60 Effect.gen(function* () {
61 const {
62 readEnvironment: _,
63 environment: __,
64 ...profileOnly
65 } = implementation;
66 yield* AuthProvider<{ method: "custom" }, void>()(
67 "ProfileOnlyProvider",
68 profileOnly,
69 );
70 const provider = yield* getAuthProvider("ProfileOnlyProvider");
71
72 expect(provider.readEnvironment).toBeUndefined();
73 expect(provider.environment).toEqual([]);
74 }).pipe(
75 Effect.provideService(AuthProviders, {}),
76 Effect.provide(NodeServices.layer),
77 ),
78 { tags: ["unit", "local"] },
79);
80
81it.effect(
82 "registration dies when readEnvironment lacks an environment declaration",
83 () =>
84 Effect.gen(function* () {
85 const { environment: _, ...undeclared } = implementation;
86 const exit = yield* AuthProvider<{ method: "custom" }, void>()(
87 "UndeclaredProvider",
88 undeclared,
89 ).pipe(Effect.exit);
90
91 expect(Exit.isFailure(exit)).toBe(true);
92 if (Exit.isFailure(exit)) {
93 expect(String(Cause.squash(exit.cause))).toContain(
94 "declare its `environment` variables",
95 );
96 }
97 }).pipe(
98 Effect.provideService(AuthProviders, {}),
99 Effect.provide(NodeServices.layer),
100 ),
101 { tags: ["unit", "local"] },
102);
103
104it.effect(
105 "call-time Interaction wins over the one ambient at registration",
106 () =>
107 Effect.gen(function* () {
108 const answered: string[] = [];
109 const scripted = (name: string): Interaction["Service"] => ({
110 output: {
111 info: () => Effect.void,
112 success: () => Effect.void,
113 warning: () => Effect.void,
114 error: () => Effect.void,
115 },
116 prompt: {
117 text: () =>
118 Effect.sync(() => {
119 answered.push(name);
120 return name;
121 }),
122 password: () => Effect.succeed(name),
123 confirm: () => Effect.succeed(true),
124 select: () => Effect.die("unused"),
125 multiSelect: () => Effect.die("unused"),
126 awaitExternal: () => Effect.succeed(name),
127 },
128 task: (_options, effect) => effect,
129 });
130
131 // Registered while "registration" is the ambient Interaction: the
132 // factory's context snapshot must NOT capture it — configure's declared
133 // requirement is resolved by whoever calls it.
134 yield* AuthProvider<{ method: "custom" }, void>()("OverrideProvider", {
135 ...implementation,
136 configure: () =>
137 Effect.gen(function* () {
138 const interaction = yield* Interaction;
139 yield* interaction.prompt
140 .text({ message: "token" })
141 .pipe(Effect.orDie);
142 return { method: "custom" as const };
143 }),
144 }).pipe(Effect.provideService(Interaction, scripted("registration")));
145
146 const provider = yield* getAuthProvider("OverrideProvider");
147 yield* provider
148 .configure("default")
149 .pipe(Effect.provideService(Interaction, scripted("call-time")));
150
151 expect(answered).toEqual(["call-time"]);
152 }).pipe(
153 Effect.provideService(AuthProviders, {}),
154 Effect.provide(NodeServices.layer),
155 ),
156 { tags: ["unit", "local"] },
157);