| 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import * as Duration from "effect/Duration"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Fiber from "effect/Fiber"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Ref from "effect/Ref"; |
| 7 | import * as HttpClient from "effect/http/HttpClient"; |
| 8 | import * as HttpClientRequest from "effect/http/HttpClientRequest"; |
| 9 | import * as HttpClientResponse from "effect/http/HttpClientResponse"; |
| 10 | import { PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts"; |
| 11 | import { |
| 12 | DESKTOP_BOOTSTRAP_TOKEN_WINDOW_MS, |
| 13 | currentDesktopBootstrapToken, |
| 14 | } from "@t3tools/shared/desktopBootstrapToken"; |
| 15 | import * as TestClock from "effect/testing/TestClock"; |
| 16 | |
| 17 | import * as DesktopBackendPool from "./DesktopBackendPool.ts"; |
| 18 | import * as DesktopLocalEnvironmentAuth from "./DesktopLocalEnvironmentAuth.ts"; |
| 19 | |
| 20 | const config = { |
| 21 | executablePath: "/electron", |
| 22 | entryPath: "/server/bin.mjs", |
| 23 | cwd: "/server", |
| 24 | env: {}, |
| 25 | bootstrap: { |
| 26 | mode: "desktop", |
| 27 | noBrowser: true, |
| 28 | port: 3773, |
| 29 | t3Home: "/tmp/t3", |
| 30 | host: "127.0.0.1", |
| 31 | desktopBootstrapToken: "desktop-bootstrap-token", |
| 32 | tailscaleServeEnabled: false, |
| 33 | tailscaleServePort: 443, |
| 34 | }, |
| 35 | httpBaseUrl: new URL("http://127.0.0.1:3773"), |
| 36 | captureOutput: true, |
| 37 | }; |
| 38 | |
| 39 | describe("DesktopLocalEnvironmentAuth", () => { |
| 40 | it.effect("exchanges the desktop bootstrap credential only once", () => |
| 41 | Effect.gen(function* () { |
| 42 | const requestCount = yield* Ref.make(0); |
| 43 | const layerHttpClient = Layer.succeed( |
| 44 | HttpClient.HttpClient, |
| 45 | HttpClient.make((request) => |
| 46 | Ref.update(requestCount, (count) => count + 1).pipe( |
| 47 | Effect.as( |
| 48 | HttpClientResponse.fromWeb( |
| 49 | request, |
| 50 | new Response( |
| 51 | JSON.stringify({ |
| 52 | access_token: "desktop-bearer-token", |
| 53 | issued_token_type: "urn:ietf:params:oauth:token-type:access_token", |
| 54 | token_type: "Bearer", |
| 55 | expires_in: 3600, |
| 56 | scope: "orchestration:read", |
| 57 | }), |
| 58 | { status: 200, headers: { "content-type": "application/json" } }, |
| 59 | ), |
| 60 | ), |
| 61 | ), |
| 62 | ), |
| 63 | ), |
| 64 | ); |
| 65 | const layerPool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, { |
| 66 | list: Effect.succeed([ |
| 67 | { |
| 68 | id: PRIMARY_LOCAL_ENVIRONMENT_ID, |
| 69 | label: Effect.succeed("Windows"), |
| 70 | currentConfig: Effect.succeedSome(config), |
| 71 | }, |
| 72 | ]), |
| 73 | } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]); |
| 74 | const layerTest = DesktopLocalEnvironmentAuth.layer.pipe( |
| 75 | Layer.provide(Layer.mergeAll(layerPool, layerHttpClient)), |
| 76 | ); |
| 77 | |
| 78 | const [first, second] = yield* Effect.gen(function* () { |
| 79 | const auth = yield* DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth; |
| 80 | return yield* Effect.all([auth.getBearerToken, auth.getBearerToken]); |
| 81 | }).pipe(Effect.provide(layerTest)); |
| 82 | |
| 83 | assert.strictEqual(first, "desktop-bearer-token"); |
| 84 | assert.strictEqual(second, "desktop-bearer-token"); |
| 85 | assert.strictEqual(yield* Ref.get(requestCount), 1); |
| 86 | }), |
| 87 | ); |
| 88 | |
| 89 | it.effect( |
| 90 | "exchanges the current window's token when the backend was launched with a secret", |
| 91 | () => |
| 92 | Effect.gen(function* () { |
| 93 | const presented = yield* Ref.make<string | null>(null); |
| 94 | const httpClientLayer = Layer.succeed( |
| 95 | HttpClient.HttpClient, |
| 96 | HttpClient.make((request) => { |
| 97 | const body = |
| 98 | request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : ""; |
| 99 | return Ref.set(presented, new URLSearchParams(body).get("subject_token")).pipe( |
| 100 | Effect.as( |
| 101 | HttpClientResponse.fromWeb( |
| 102 | request, |
| 103 | new Response( |
| 104 | JSON.stringify({ |
| 105 | access_token: "desktop-bearer-token", |
| 106 | issued_token_type: "urn:ietf:params:oauth:token-type:access_token", |
| 107 | token_type: "Bearer", |
| 108 | expires_in: 3600, |
| 109 | scope: "orchestration:read", |
| 110 | }), |
| 111 | { status: 200, headers: { "content-type": "application/json" } }, |
| 112 | ), |
| 113 | ), |
| 114 | ), |
| 115 | ); |
| 116 | }), |
| 117 | ); |
| 118 | const poolLayer = Layer.succeed(DesktopBackendPool.DesktopBackendPool, { |
| 119 | list: Effect.succeed([ |
| 120 | { |
| 121 | id: PRIMARY_LOCAL_ENVIRONMENT_ID, |
| 122 | label: Effect.succeed("Windows"), |
| 123 | currentConfig: Effect.succeedSome({ |
| 124 | ...config, |
| 125 | bootstrap: { ...config.bootstrap, desktopBootstrapSecret: "desktop-secret" }, |
| 126 | }), |
| 127 | }, |
| 128 | ]), |
| 129 | } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]); |
| 130 | |
| 131 | // The first exchange happens a day after launch, past the launch token's windows. |
| 132 | yield* TestClock.setTime(DESKTOP_BOOTSTRAP_TOKEN_WINDOW_MS * 2 + 1); |
| 133 | yield* Effect.gen(function* () { |
| 134 | const auth = yield* DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth; |
| 135 | return yield* auth.getBearerToken; |
| 136 | }).pipe( |
| 137 | Effect.provide( |
| 138 | DesktopLocalEnvironmentAuth.layer.pipe( |
| 139 | Layer.provide(Layer.mergeAll(poolLayer, httpClientLayer)), |
| 140 | ), |
| 141 | ), |
| 142 | ); |
| 143 | |
| 144 | assert.strictEqual( |
| 145 | yield* Ref.get(presented), |
| 146 | currentDesktopBootstrapToken("desktop-secret", DESKTOP_BOOTSTRAP_TOKEN_WINDOW_MS * 2 + 1), |
| 147 | ); |
| 148 | }).pipe(Effect.provide(TestClock.layer())), |
| 149 | ); |
| 150 | |
| 151 | const tokenResponse = (request: HttpClientRequest.HttpClientRequest) => |
| 152 | HttpClientResponse.fromWeb( |
| 153 | request, |
| 154 | new Response( |
| 155 | JSON.stringify({ |
| 156 | access_token: "desktop-bearer-token", |
| 157 | issued_token_type: "urn:ietf:params:oauth:token-type:access_token", |
| 158 | token_type: "Bearer", |
| 159 | expires_in: 3600, |
| 160 | scope: "orchestration:read", |
| 161 | }), |
| 162 | { status: 200, headers: { "content-type": "application/json" } }, |
| 163 | ), |
| 164 | ); |
| 165 | const layerPool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, { |
| 166 | list: Effect.succeed([ |
| 167 | { |
| 168 | id: PRIMARY_LOCAL_ENVIRONMENT_ID, |
| 169 | label: Effect.succeed("Windows"), |
| 170 | currentConfig: Effect.succeedSome(config), |
| 171 | }, |
| 172 | ]), |
| 173 | } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]); |
| 174 | // Answers the first `failures` exchanges with `failure`, then with a token. |
| 175 | const makeExchange = (failures: number, failure: () => Response) => |
| 176 | Effect.gen(function* () { |
| 177 | const requestCount = yield* Ref.make(0); |
| 178 | const layerHttpClient = Layer.succeed( |
| 179 | HttpClient.HttpClient, |
| 180 | HttpClient.make((request) => |
| 181 | Ref.updateAndGet(requestCount, (count) => count + 1).pipe( |
| 182 | Effect.map((count) => |
| 183 | count <= failures |
| 184 | ? HttpClientResponse.fromWeb(request, failure()) |
| 185 | : tokenResponse(request), |
| 186 | ), |
| 187 | ), |
| 188 | ), |
| 189 | ); |
| 190 | const auth = yield* DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth.pipe( |
| 191 | Effect.provide( |
| 192 | DesktopLocalEnvironmentAuth.layer.pipe( |
| 193 | Layer.provide(Layer.mergeAll(layerPool, layerHttpClient)), |
| 194 | ), |
| 195 | ), |
| 196 | ); |
| 197 | return { auth, requestCount }; |
| 198 | }); |
| 199 | |
| 200 | it.effect("retries a backend that is still starting", () => |
| 201 | Effect.gen(function* () { |
| 202 | const { auth, requestCount } = yield* makeExchange( |
| 203 | 2, |
| 204 | () => new Response("", { status: 503 }), |
| 205 | ); |
| 206 | |
| 207 | const fiber = yield* auth.getBearerToken.pipe(Effect.forkChild); |
| 208 | yield* TestClock.adjust(Duration.seconds(1)); |
| 209 | |
| 210 | assert.strictEqual(yield* Fiber.join(fiber), "desktop-bearer-token"); |
| 211 | assert.strictEqual(yield* Ref.get(requestCount), 3); |
| 212 | }).pipe(Effect.provide(TestClock.layer())), |
| 213 | ); |
| 214 | |
| 215 | it.effect("does not retry a rejected bootstrap credential", () => |
| 216 | Effect.gen(function* () { |
| 217 | const { auth, requestCount } = yield* makeExchange(1, () => |
| 218 | Response.json( |
| 219 | { |
| 220 | _tag: "EnvironmentAuthInvalidError", |
| 221 | code: "auth_invalid", |
| 222 | reason: "invalid_credential", |
| 223 | traceId: "trace-1", |
| 224 | }, |
| 225 | { status: 401 }, |
| 226 | ), |
| 227 | ); |
| 228 | |
| 229 | const fiber = yield* auth.getBearerToken.pipe(Effect.flip, Effect.forkChild); |
| 230 | yield* TestClock.adjust(Duration.seconds(1)); |
| 231 | const error = yield* Fiber.join(fiber); |
| 232 | |
| 233 | assert.strictEqual(error._tag, "DesktopLocalEnvironmentAuthSessionBootstrapError"); |
| 234 | assert.strictEqual(yield* Ref.get(requestCount), 1); |
| 235 | }).pipe(Effect.provide(TestClock.layer())), |
| 236 | ); |
| 237 | }); |