Open History to see the last commit
1// @effect-diagnostics nodeBuiltinImport:off -- the fake `op` reads the template the driver wrote.
2import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
3import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
4import { assert, describe, it } from "@effect/vitest";
5import * as Clock from "effect/Clock";
6import * as Deferred from "effect/Deferred";
7import type * as Duration from "effect/Duration";
8import * as Effect from "effect/Effect";
9import * as Fiber from "effect/Fiber";
10import * as FileSystem from "effect/FileSystem";
11import * as Layer from "effect/Layer";
12import * as Sink from "effect/Sink";
13import * as Stream from "effect/Stream";
14import * as TestClock from "effect/testing/TestClock";
15import { ChildProcessSpawner } from "effect/process";
16import * as NodeFS from "node:fs";
17
18import { OnePasswordAccount } from "../shared/definition.ts";
19import { driver } from "./driver.ts";
20
21const encoder = new TextEncoder();
22const account = OnePasswordAccount.make("my.1password.com");
23
24type OpResult = {
25 readonly stdout?: string;
26 readonly stderr?: string;
27 readonly code?: number;
28 readonly hang?: boolean;
29 readonly delay?: Duration.Input;
30};
31
32function fakeOp(answer: (args: ReadonlyArray<string>) => OpResult) {
33 const invocations: Array<ReadonlyArray<string>> = [];
34 const spawned = Deferred.makeUnsafe<void>();
35 const layer = Layer.mergeAll(
36 Layer.succeed(
37 ChildProcessSpawner.ChildProcessSpawner,
38 ChildProcessSpawner.make((command) =>
39 Effect.sync(() => {
40 const { args } = command as unknown as { readonly args: ReadonlyArray<string> };
41 invocations.push(args);
42 Deferred.doneUnsafe(spawned, Effect.void);
43 const result = answer(args);
44 return ChildProcessSpawner.makeHandle({
45 pid: ChildProcessSpawner.ProcessId(1),
46 exitCode: result.hang
47 ? Effect.never
48 : Effect.sleep(result.delay ?? 0).pipe(
49 Effect.as(ChildProcessSpawner.ExitCode(result.code ?? 0)),
50 ),
51 isRunning: Effect.succeed(false),
52 kill: () => Effect.void,
53 unref: Effect.succeed(Effect.void),
54 stdin: Sink.drain,
55 stdout: Stream.make(encoder.encode(result.stdout ?? "")),
56 stderr: Stream.make(encoder.encode(result.stderr ?? "")),
57 all: Stream.empty,
58 getInputFd: () => Sink.drain,
59 getOutputFd: () => Stream.empty,
60 });
61 }),
62 ),
63 ),
64 NodeFileSystem.layer,
65 NodeCrypto.layer,
66 );
67 return { layer, invocations, spawned };
68}
69
70// Answers `op inject` by substituting each `{{ reference }}` in the template file it was handed.
71const injectFrom =
72 (secrets: Readonly<Record<string, string>>) =>
73 (args: ReadonlyArray<string>): OpResult => {
74 const template = NodeFS.readFileSync(args[args.indexOf("-i") + 1]!, "utf8");
75 return { stdout: template.replace(/\{\{ (\S+) \}\}/g, (_, reference) => secrets[reference]!) };
76 };
77
78describe("1Password driver", () => {
79 it.effect("reads one reference from the account", () => {
80 const op = fakeOp(() => ({ stdout: "sk-live" }));
81 return Effect.gen(function* () {
82 assert.deepStrictEqual(yield* driver.read({ account }, ["op://Private/OpenAI/credential"]), [
83 "sk-live",
84 ]);
85 assert.deepStrictEqual(op.invocations, [
86 [
87 "--cache=false",
88 "read",
89 "--account=my.1password.com",
90 "--no-newline",
91 "op://Private/OpenAI/credential",
92 ],
93 ]);
94 }).pipe(Effect.provide(op.layer));
95 });
96
97 it.effect("reads a failed reference as undefined", () => {
98 const op = fakeOp(() => ({ code: 1, stderr: "[ERROR] account is locked" }));
99 return Effect.gen(function* () {
100 assert.deepStrictEqual(yield* driver.read({ account }, ["op://Private/OpenAI/credential"]), [
101 undefined,
102 ]);
103 }).pipe(Effect.provide(op.layer));
104 });
105
106 it.effect("reads every reference in a single call", () => {
107 const op = fakeOp(
108 injectFrom({
109 "op://Private/OpenAI/credential": "sk-a",
110 "op://Private/Anthropic/key": " sk-b\n",
111 }),
112 );
113 return Effect.gen(function* () {
114 const values = yield* driver.read({ account }, [
115 "op://Private/OpenAI/credential",
116 "op://Private/Anthropic/key",
117 ]);
118
119 assert.deepStrictEqual(values, ["sk-a", " sk-b\n"]);
120 assert.strictEqual(op.invocations.length, 1);
121 assert.deepStrictEqual(op.invocations[0]?.slice(0, 3), [
122 "--cache=false",
123 "inject",
124 "--account=my.1password.com",
125 ]);
126 }).pipe(Effect.provide(op.layer));
127 });
128
129 it.effect("falls back to one read at a time when the batch fails", () => {
130 const op = fakeOp((args) =>
131 args.includes("inject")
132 ? { code: 1, stderr: '[ERROR] could not resolve "op://Private/Gone/key"' }
133 : args.at(-1) === "op://Private/Gone/key"
134 ? { code: 1, stderr: "[ERROR] item not found" }
135 : { stdout: "sk-a" },
136 );
137 return Effect.gen(function* () {
138 const values = yield* driver.read({ account }, [
139 "op://Private/OpenAI/credential",
140 "op://Private/Gone/key",
141 ]);
142
143 assert.deepStrictEqual(values, ["sk-a", undefined]);
144 assert.strictEqual(op.invocations.length, 3);
145 }).pipe(Effect.provide(op.layer));
146 });
147
148 it.effect("does not read one at a time after the batch times out", () => {
149 const op = fakeOp(() => ({ hang: true }));
150 return Effect.gen(function* () {
151 const reading = yield* driver
152 .read({ account }, ["op://Private/OpenAI/credential", "op://Private/Anthropic/key"])
153 .pipe(Effect.forkChild);
154 yield* Deferred.await(op.spawned);
155 yield* TestClock.adjust("1 minute");
156
157 assert.deepStrictEqual(yield* Fiber.join(reading), [undefined, undefined]);
158 assert.strictEqual(op.invocations.length, 1);
159 }).pipe(Effect.provide(op.layer));
160 });
161
162 it.effect("gives up within the read timeout when a slow batch fails", () => {
163 const op = fakeOp((args) =>
164 args.includes("inject")
165 ? { code: 1, delay: "40 seconds" }
166 : { stdout: "sk-a", delay: "40 seconds" },
167 );
168 // Keeps the temp template off real disk I/O so the virtual clock alone drives the read.
169 const templateFile = FileSystem.layerNoop({
170 makeTempFileScoped: () => Effect.succeed("/template"),
171 writeFileString: () => Effect.void,
172 });
173 return Effect.gen(function* () {
174 const reading = yield* driver
175 .read({ account }, ["op://Private/OpenAI/credential", "op://Private/Anthropic/key"])
176 .pipe(Effect.andThen(Clock.currentTimeMillis), Effect.forkChild);
177 yield* TestClock.adjust("5 minutes");
178
179 assert.isAtMost(yield* Fiber.join(reading), 45_000);
180 }).pipe(Effect.provide(Layer.merge(op.layer, templateFile)));
181 });
182});