| 1 | import { |
| 2 | EnvironmentId, |
| 3 | ORCHESTRATION_PROTOCOL_VERSION, |
| 4 | type DesktopSshEnvironmentTarget, |
| 5 | } from "@t3tools/contracts"; |
| 6 | import { describe, expect, it } from "@effect/vitest"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as Layer from "effect/Layer"; |
| 9 | import * as Option from "effect/Option"; |
| 10 | import * as Ref from "effect/Ref"; |
| 11 | import * as Tracer from "effect/Tracer"; |
| 12 | |
| 13 | import * as ConnectionResolver from "./resolver.ts"; |
| 14 | import * as ClientCapabilities from "../platform/capabilities.ts"; |
| 15 | import * as RemoteEnvironmentAuthorization from "../authorization/service.ts"; |
| 16 | import { |
| 17 | BearerConnectionCredential, |
| 18 | BearerConnectionProfile, |
| 19 | HubConnectionCredential, |
| 20 | type ConnectionCatalogEntry, |
| 21 | SshConnectionProfile, |
| 22 | type ConnectionCredential, |
| 23 | type ConnectionProfile, |
| 24 | } from "./catalog.ts"; |
| 25 | import * as ConnectionCredentialStore from "./credentialStore.ts"; |
| 26 | import { |
| 27 | BearerConnectionTarget, |
| 28 | ConnectionTransientError, |
| 29 | PrimaryConnectionTarget, |
| 30 | RelayConnectionTarget, |
| 31 | SshConnectionTarget, |
| 32 | type ConnectionTarget, |
| 33 | } from "./model.ts"; |
| 34 | import * as ConnectionProfileStore from "./profileStore.ts"; |
| 35 | import * as RpcHttp from "../rpc/http.ts"; |
| 36 | import { |
| 37 | GitHubRoutingPermissions, |
| 38 | gitHubRoutingConnectionKey, |
| 39 | makeGitHubRoutingPermissions, |
| 40 | } from "./githubRoutingPermissions.ts"; |
| 41 | |
| 42 | const ENVIRONMENT_ID = EnvironmentId.make("environment-1"); |
| 43 | const ENDPOINT = { |
| 44 | httpBaseUrl: "https://environment.example.test", |
| 45 | wsBaseUrl: "wss://environment.example.test", |
| 46 | }; |
| 47 | const SSH_TARGET: DesktopSshEnvironmentTarget = { |
| 48 | alias: "development", |
| 49 | hostname: "development.example.test", |
| 50 | username: "developer", |
| 51 | port: 22, |
| 52 | }; |
| 53 | |
| 54 | function catalogEntry( |
| 55 | target: ConnectionTarget, |
| 56 | profile: Option.Option<ConnectionProfile> = Option.none(), |
| 57 | ): ConnectionCatalogEntry { |
| 58 | return { target, profile, enabled: true }; |
| 59 | } |
| 60 | |
| 61 | function collectingTracer(spans: Array<string>): Tracer.Tracer { |
| 62 | return Tracer.make({ |
| 63 | span: (options) => { |
| 64 | const span = new Tracer.NativeSpan(options); |
| 65 | const end = span.end.bind(span); |
| 66 | span.end = (endTime, exit) => { |
| 67 | end(endTime, exit); |
| 68 | spans.push(span.name); |
| 69 | }; |
| 70 | return span; |
| 71 | }, |
| 72 | }); |
| 73 | } |
| 74 | |
| 75 | const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((options?: { |
| 76 | readonly profiles?: ReadonlyArray<ConnectionProfile>; |
| 77 | readonly profileStore?: ConnectionProfileStore.ConnectionProfileStore["Service"]; |
| 78 | readonly credentials?: ReadonlyArray<readonly [string, ConnectionCredential]>; |
| 79 | readonly authorizeBearer?: RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"]["authorizeBearer"]; |
| 80 | readonly authorizeDpop?: RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"]["authorizeDpop"]; |
| 81 | readonly primaryBearerToken?: string; |
| 82 | readonly prepareSsh?: ClientCapabilities.SshEnvironmentGateway["Service"]["prepare"]; |
| 83 | readonly descriptorProtocolVersion?: number | null | undefined; |
| 84 | }) => { |
| 85 | const profiles = new Map( |
| 86 | (options?.profiles ?? []).map((profile) => [profile.connectionId, profile]), |
| 87 | ); |
| 88 | const credentials = new Map(options?.credentials ?? []); |
| 89 | |
| 90 | const profileStore = ConnectionProfileStore.ConnectionProfileStore.of({ |
| 91 | get: (connectionId) => Effect.succeed(Option.fromNullishOr(profiles.get(connectionId))), |
| 92 | put: (profile) => Effect.sync(() => void profiles.set(profile.connectionId, profile)), |
| 93 | remove: (connectionId) => Effect.sync(() => void profiles.delete(connectionId)), |
| 94 | }); |
| 95 | const credentialStore = ConnectionCredentialStore.ConnectionCredentialStore.of({ |
| 96 | get: (connectionId) => Effect.succeed(Option.fromNullishOr(credentials.get(connectionId))), |
| 97 | put: (connectionId, credential) => |
| 98 | Effect.sync(() => void credentials.set(connectionId, credential)), |
| 99 | remove: (connectionId) => Effect.sync(() => void credentials.delete(connectionId)), |
| 100 | }); |
| 101 | const remote = RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization.of({ |
| 102 | authorizeBearer: |
| 103 | options?.authorizeBearer ?? |
| 104 | ((input) => |
| 105 | Effect.succeed({ |
| 106 | environmentId: input.expectedEnvironmentId, |
| 107 | label: "Authorized bearer environment", |
| 108 | httpBaseUrl: input.httpBaseUrl, |
| 109 | socketUrl: "wss://authorized.example.test/ws?wsTicket=bearer", |
| 110 | httpAuthorization: { |
| 111 | _tag: "Bearer" as const, |
| 112 | token: input.bearerToken, |
| 113 | }, |
| 114 | })), |
| 115 | authorizeDpop: |
| 116 | options?.authorizeDpop ?? |
| 117 | ((input) => |
| 118 | Effect.succeed({ |
| 119 | environmentId: input.expectedEnvironmentId, |
| 120 | label: "Authorized relay environment", |
| 121 | httpBaseUrl: ENDPOINT.httpBaseUrl, |
| 122 | socketUrl: "wss://authorized.example.test/ws?wsTicket=dpop", |
| 123 | httpAuthorization: { |
| 124 | _tag: "Dpop" as const, |
| 125 | accessToken: "dpop-access-token", |
| 126 | expiresAtEpochMs: Number.MAX_SAFE_INTEGER, |
| 127 | }, |
| 128 | })), |
| 129 | authorizeDpopHttp: () => Effect.die("unused"), |
| 130 | }); |
| 131 | const ssh = ClientCapabilities.SshEnvironmentGateway.of({ |
| 132 | provision: () => Effect.die("unused"), |
| 133 | prepare: |
| 134 | options?.prepareSsh ?? |
| 135 | (() => |
| 136 | Effect.succeed({ |
| 137 | bootstrap: { |
| 138 | target: SSH_TARGET, |
| 139 | httpBaseUrl: "http://127.0.0.1:4010", |
| 140 | wsBaseUrl: "ws://127.0.0.1:4010", |
| 141 | pairingToken: null, |
| 142 | }, |
| 143 | bearerToken: "ssh-bearer", |
| 144 | })), |
| 145 | disconnect: () => Effect.void, |
| 146 | }); |
| 147 | |
| 148 | const layerDependencies = Layer.mergeAll( |
| 149 | RpcHttp.layerRemoteHttpClient((() => |
| 150 | Promise.resolve( |
| 151 | Response.json({ |
| 152 | environmentId: ENVIRONMENT_ID, |
| 153 | label: "Compatible environment", |
| 154 | platform: { os: "linux", arch: "x64" }, |
| 155 | serverVersion: "0.0.0-test", |
| 156 | ...(options?.descriptorProtocolVersion === undefined |
| 157 | ? { orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION } |
| 158 | : options.descriptorProtocolVersion === null |
| 159 | ? {} |
| 160 | : { orchestrationProtocolVersion: options.descriptorProtocolVersion }), |
| 161 | capabilities: { repositoryIdentity: true }, |
| 162 | }), |
| 163 | )) satisfies typeof fetch), |
| 164 | Layer.succeed( |
| 165 | ConnectionProfileStore.ConnectionProfileStore, |
| 166 | options?.profileStore ?? profileStore, |
| 167 | ), |
| 168 | Layer.succeed(ConnectionCredentialStore.ConnectionCredentialStore, credentialStore), |
| 169 | Layer.succeed( |
| 170 | ClientCapabilities.PrimaryEnvironmentAuth, |
| 171 | ClientCapabilities.PrimaryEnvironmentAuth.of({ |
| 172 | bearerToken: Effect.succeed(Option.fromNullishOr(options?.primaryBearerToken)), |
| 173 | }), |
| 174 | ), |
| 175 | Layer.succeed( |
| 176 | ClientCapabilities.ClientPresentation, |
| 177 | ClientCapabilities.ClientPresentation.of({ |
| 178 | metadata: { label: "Test Client", deviceType: "desktop", surface: "web" }, |
| 179 | }), |
| 180 | ), |
| 181 | Layer.succeed(RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization, remote), |
| 182 | Layer.succeed(ClientCapabilities.SshEnvironmentGateway, ssh), |
| 183 | ); |
| 184 | |
| 185 | return Effect.succeed(ConnectionResolver.layer.pipe(Layer.provide(layerDependencies))); |
| 186 | }); |
| 187 | |
| 188 | describe("ConnectionResolver", () => { |
| 189 | it.effect("resolves hub routes through one privately stored credential", () => |
| 190 | Effect.gen(function* () { |
| 191 | const connectionId = "hub:hub:environment-1"; |
| 192 | const profile = new BearerConnectionProfile({ |
| 193 | connectionId, |
| 194 | environmentId: ENVIRONMENT_ID, |
| 195 | label: "Machine", |
| 196 | httpBaseUrl: "https://hub.example/environments/environment-1/", |
| 197 | wsBaseUrl: "wss://hub.example/environments/environment-1/", |
| 198 | }); |
| 199 | const dependencies = yield* makeDependencies({ |
| 200 | credentials: [ |
| 201 | [connectionId, new HubConnectionCredential({ connectionId: "hub-session:hub" })], |
| 202 | ["hub-session:hub", new BearerConnectionCredential({ token: "single-hub-secret" })], |
| 203 | ], |
| 204 | }); |
| 205 | const resolver = yield* ConnectionResolver.ConnectionResolver.pipe( |
| 206 | Effect.provide(dependencies), |
| 207 | ); |
| 208 | const target = new BearerConnectionTarget({ |
| 209 | connectionId, |
| 210 | environmentId: ENVIRONMENT_ID, |
| 211 | label: "Machine", |
| 212 | }); |
| 213 | const prepared = yield* resolver.prepare(catalogEntry(target, Option.some(profile))); |
| 214 | expect(prepared.httpAuthorization).toEqual({ _tag: "Bearer", token: "single-hub-secret" }); |
| 215 | expect(prepared.httpBaseUrl).toBe(profile.httpBaseUrl); |
| 216 | }), |
| 217 | ); |
| 218 | it.effect("blocks an old host during discovery before opening orchestration RPC", () => |
| 219 | Effect.gen(function* () { |
| 220 | const layerBroker = yield* makeDependencies({ descriptorProtocolVersion: null }); |
| 221 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker)); |
| 222 | const target = new PrimaryConnectionTarget({ |
| 223 | environmentId: ENVIRONMENT_ID, |
| 224 | label: "Primary", |
| 225 | httpBaseUrl: "http://127.0.0.1:3777", |
| 226 | wsBaseUrl: "ws://127.0.0.1:3777", |
| 227 | }); |
| 228 | |
| 229 | const error = yield* Effect.flip(broker.prepare(catalogEntry(target))); |
| 230 | |
| 231 | expect(error).toMatchObject({ reason: "unsupported" }); |
| 232 | expect(error.message).toContain("Update the server on Compatible environment"); |
| 233 | }), |
| 234 | ); |
| 235 | |
| 236 | it.effect("blocks an incompatible host during discovery before opening orchestration RPC", () => |
| 237 | Effect.gen(function* () { |
| 238 | const layerBroker = yield* makeDependencies({ |
| 239 | descriptorProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION + 1, |
| 240 | }); |
| 241 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker)); |
| 242 | const target = new PrimaryConnectionTarget({ |
| 243 | environmentId: ENVIRONMENT_ID, |
| 244 | label: "Primary", |
| 245 | httpBaseUrl: "http://127.0.0.1:3777", |
| 246 | wsBaseUrl: "ws://127.0.0.1:3777", |
| 247 | }); |
| 248 | |
| 249 | const error = yield* Effect.flip(broker.prepare(catalogEntry(target))); |
| 250 | |
| 251 | expect(error).toMatchObject({ reason: "unsupported" }); |
| 252 | expect(error.message).toContain("This client is not supported"); |
| 253 | }), |
| 254 | ); |
| 255 | |
| 256 | it.effect("prepares a primary environment without remote capabilities", () => |
| 257 | Effect.gen(function* () { |
| 258 | const layerBroker = yield* makeDependencies(); |
| 259 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker)); |
| 260 | const target = new PrimaryConnectionTarget({ |
| 261 | environmentId: ENVIRONMENT_ID, |
| 262 | label: "Primary", |
| 263 | httpBaseUrl: "http://127.0.0.1:3777", |
| 264 | wsBaseUrl: "ws://127.0.0.1:3777", |
| 265 | }); |
| 266 | |
| 267 | expect(yield* broker.prepare(catalogEntry(target))).toEqual({ |
| 268 | environmentId: ENVIRONMENT_ID, |
| 269 | label: "Primary", |
| 270 | httpBaseUrl: "http://127.0.0.1:3777", |
| 271 | socketUrl: "ws://127.0.0.1:3777/ws?clientSurface=web&orchestrationProtocol=2", |
| 272 | httpAuthorization: null, |
| 273 | target, |
| 274 | }); |
| 275 | }), |
| 276 | ); |
| 277 | |
| 278 | it.effect("authorizes a desktop primary environment with its platform bearer token", () => |
| 279 | Effect.gen(function* () { |
| 280 | const bearerInputs = yield* Ref.make<ReadonlyArray<{ token: string; method: string }>>([]); |
| 281 | const layerBroker = yield* makeDependencies({ |
| 282 | primaryBearerToken: "desktop-bearer", |
| 283 | authorizeBearer: (input) => |
| 284 | Ref.update(bearerInputs, (values) => [ |
| 285 | ...values, |
| 286 | { token: input.bearerToken, method: input.connectionMethod }, |
| 287 | ]).pipe( |
| 288 | Effect.as({ |
| 289 | environmentId: input.expectedEnvironmentId, |
| 290 | label: "Primary", |
| 291 | httpBaseUrl: input.httpBaseUrl, |
| 292 | socketUrl: "ws://127.0.0.1:3777/ws?wsTicket=desktop", |
| 293 | httpAuthorization: { |
| 294 | _tag: "Bearer" as const, |
| 295 | token: input.bearerToken, |
| 296 | }, |
| 297 | }), |
| 298 | ), |
| 299 | }); |
| 300 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker)); |
| 301 | const target = new PrimaryConnectionTarget({ |
| 302 | environmentId: ENVIRONMENT_ID, |
| 303 | label: "Primary", |
| 304 | httpBaseUrl: "http://127.0.0.1:3777", |
| 305 | wsBaseUrl: "ws://127.0.0.1:3777", |
| 306 | }); |
| 307 | |
| 308 | expect(yield* broker.prepare(catalogEntry(target))).toMatchObject({ |
| 309 | socketUrl: "ws://127.0.0.1:3777/ws?wsTicket=desktop&orchestrationProtocol=2", |
| 310 | httpAuthorization: { _tag: "Bearer", token: "desktop-bearer" }, |
| 311 | target, |
| 312 | }); |
| 313 | expect(yield* Ref.get(bearerInputs)).toEqual([{ token: "desktop-bearer", method: "direct" }]); |
| 314 | }), |
| 315 | ); |
| 316 | |
| 317 | it.effect("uses the registered bearer profile without re-reading the profile store", () => |
| 318 | Effect.gen(function* () { |
| 319 | const bearerInputs = yield* Ref.make<ReadonlyArray<{ token: string; method: string }>>([]); |
| 320 | const target = new BearerConnectionTarget({ |
| 321 | environmentId: ENVIRONMENT_ID, |
| 322 | label: "Saved", |
| 323 | connectionId: "saved-1", |
| 324 | }); |
| 325 | const profile = new BearerConnectionProfile({ |
| 326 | connectionId: "saved-1", |
| 327 | environmentId: ENVIRONMENT_ID, |
| 328 | label: "Saved", |
| 329 | httpBaseUrl: ENDPOINT.httpBaseUrl, |
| 330 | wsBaseUrl: ENDPOINT.wsBaseUrl, |
| 331 | }); |
| 332 | const layerBroker = yield* makeDependencies({ |
| 333 | credentials: [["saved-1", new BearerConnectionCredential({ token: "secret-bearer" })]], |
| 334 | authorizeBearer: (input) => |
| 335 | Ref.update(bearerInputs, (values) => [ |
| 336 | ...values, |
| 337 | { token: input.bearerToken, method: input.connectionMethod }, |
| 338 | ]).pipe( |
| 339 | Effect.as({ |
| 340 | environmentId: input.expectedEnvironmentId, |
| 341 | label: "Saved", |
| 342 | httpBaseUrl: input.httpBaseUrl, |
| 343 | socketUrl: "wss://environment.example.test/ws?wsTicket=ticket", |
| 344 | httpAuthorization: { |
| 345 | _tag: "Bearer" as const, |
| 346 | token: input.bearerToken, |
| 347 | }, |
| 348 | }), |
| 349 | ), |
| 350 | }); |
| 351 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker)); |
| 352 | |
| 353 | expect( |
| 354 | (yield* broker.prepare(catalogEntry(target, Option.some(profile)))).socketUrl, |
| 355 | ).toContain("wsTicket=ticket"); |
| 356 | expect(yield* Ref.get(bearerInputs)).toEqual([{ token: "secret-bearer", method: "direct" }]); |
| 357 | }), |
| 358 | ); |
| 359 | |
| 360 | it.effect("prepares relay connections with the authorized endpoint and credentials", () => |
| 361 | Effect.gen(function* () { |
| 362 | const target = new RelayConnectionTarget({ |
| 363 | environmentId: ENVIRONMENT_ID, |
| 364 | label: "Cloud", |
| 365 | }); |
| 366 | const layerBroker = yield* makeDependencies(); |
| 367 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker)); |
| 368 | |
| 369 | expect(yield* broker.prepare(catalogEntry(target))).toEqual({ |
| 370 | environmentId: ENVIRONMENT_ID, |
| 371 | label: "Authorized relay environment", |
| 372 | httpBaseUrl: ENDPOINT.httpBaseUrl, |
| 373 | socketUrl: `wss://authorized.example.test/ws?wsTicket=dpop&orchestrationProtocol=${ORCHESTRATION_PROTOCOL_VERSION}`, |
| 374 | httpAuthorization: { |
| 375 | _tag: "Dpop", |
| 376 | accessToken: "dpop-access-token", |
| 377 | expiresAtEpochMs: Number.MAX_SAFE_INTEGER, |
| 378 | }, |
| 379 | target, |
| 380 | }); |
| 381 | }), |
| 382 | ); |
| 383 | |
| 384 | it.effect("records the complete relay authorization flow in local tracing", () => |
| 385 | Effect.gen(function* () { |
| 386 | const userSpans: Array<string> = []; |
| 387 | const target = new RelayConnectionTarget({ |
| 388 | environmentId: ENVIRONMENT_ID, |
| 389 | label: "Cloud", |
| 390 | }); |
| 391 | const layerBroker = yield* makeDependencies({ |
| 392 | authorizeDpop: (input) => |
| 393 | Effect.succeed({ |
| 394 | environmentId: input.expectedEnvironmentId, |
| 395 | label: "Cloud", |
| 396 | httpBaseUrl: ENDPOINT.httpBaseUrl, |
| 397 | socketUrl: "wss://environment.example.test/ws?wsTicket=dpop", |
| 398 | httpAuthorization: { |
| 399 | _tag: "Dpop" as const, |
| 400 | accessToken: "dpop-access-token", |
| 401 | expiresAtEpochMs: Number.MAX_SAFE_INTEGER, |
| 402 | }, |
| 403 | }).pipe(Effect.withSpan("test.remote.authorizeDpop")), |
| 404 | }); |
| 405 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker)); |
| 406 | |
| 407 | yield* broker |
| 408 | .prepare(catalogEntry(target)) |
| 409 | .pipe(Effect.withTracer(collectingTracer(userSpans))); |
| 410 | |
| 411 | expect(userSpans).toContain("clientRuntime.connection.broker.relay"); |
| 412 | expect(userSpans).toContain("test.remote.authorizeDpop"); |
| 413 | expect(userSpans).toContain("clientRuntime.connection.broker.prepare"); |
| 414 | }), |
| 415 | ); |
| 416 | |
| 417 | it.effect("delegates SSH launch to the platform gateway before remote authorization", () => |
| 418 | Effect.gen(function* () { |
| 419 | const preparedTargets = yield* Ref.make<ReadonlyArray<DesktopSshEnvironmentTarget>>([]); |
| 420 | const connectionMethods = yield* Ref.make<ReadonlyArray<string>>([]); |
| 421 | const target = new SshConnectionTarget({ |
| 422 | environmentId: ENVIRONMENT_ID, |
| 423 | label: "SSH", |
| 424 | connectionId: "ssh-1", |
| 425 | }); |
| 426 | const profile = new SshConnectionProfile({ |
| 427 | connectionId: "ssh-1", |
| 428 | environmentId: ENVIRONMENT_ID, |
| 429 | label: "SSH", |
| 430 | target: SSH_TARGET, |
| 431 | }); |
| 432 | const layerBroker = yield* makeDependencies({ |
| 433 | prepareSsh: (input) => |
| 434 | Ref.update(preparedTargets, (values) => [...values, input.target]).pipe( |
| 435 | Effect.as({ |
| 436 | bootstrap: { |
| 437 | target: input.target, |
| 438 | httpBaseUrl: "http://127.0.0.1:4010", |
| 439 | wsBaseUrl: "ws://127.0.0.1:4010", |
| 440 | pairingToken: null, |
| 441 | }, |
| 442 | bearerToken: "ssh-bearer", |
| 443 | }), |
| 444 | ), |
| 445 | authorizeBearer: (input) => |
| 446 | Ref.update(connectionMethods, (methods) => [...methods, input.connectionMethod]).pipe( |
| 447 | Effect.as({ |
| 448 | environmentId: input.expectedEnvironmentId, |
| 449 | label: "SSH", |
| 450 | httpBaseUrl: input.httpBaseUrl, |
| 451 | socketUrl: "wss://environment.example.test/ws?wsTicket=bearer", |
| 452 | httpAuthorization: { |
| 453 | _tag: "Bearer" as const, |
| 454 | token: input.bearerToken, |
| 455 | }, |
| 456 | }), |
| 457 | ), |
| 458 | }); |
| 459 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker)); |
| 460 | |
| 461 | expect( |
| 462 | (yield* broker.prepare(catalogEntry(target, Option.some(profile)))).socketUrl, |
| 463 | ).toContain("wsTicket=bearer"); |
| 464 | expect(yield* Ref.get(preparedTargets)).toEqual([SSH_TARGET]); |
| 465 | expect(yield* Ref.get(connectionMethods)).toEqual(["ssh"]); |
| 466 | }), |
| 467 | ); |
| 468 | |
| 469 | it.effect.each(["unchanged", "changed", "revocation-failed"] as const)( |
| 470 | "handles %s SSH routing consent before saving or authorizing", |
| 471 | (scenario) => |
| 472 | Effect.gen(function* () { |
| 473 | const calls: string[] = []; |
| 474 | const target = new SshConnectionTarget({ |
| 475 | environmentId: ENVIRONMENT_ID, |
| 476 | label: "SSH", |
| 477 | connectionId: "ssh-1", |
| 478 | }); |
| 479 | const profile = new SshConnectionProfile({ |
| 480 | connectionId: target.connectionId, |
| 481 | environmentId: ENVIRONMENT_ID, |
| 482 | label: "SSH", |
| 483 | target: SSH_TARGET, |
| 484 | }); |
| 485 | const entry = catalogEntry(target, Option.some(profile)); |
| 486 | const preparedTarget = |
| 487 | scenario === "unchanged" |
| 488 | ? SSH_TARGET |
| 489 | : { ...SSH_TARGET, hostname: "replacement.example.test" }; |
| 490 | const failure = new ConnectionTransientError({ |
| 491 | reason: "remote-unavailable", |
| 492 | detail: "Could not persist routing consent.", |
| 493 | }); |
| 494 | const permissions = yield* makeGitHubRoutingPermissions({ |
| 495 | read: Effect.succeed([ |
| 496 | { |
| 497 | environmentId: ENVIRONMENT_ID, |
| 498 | connectionKey: gitHubRoutingConnectionKey(entry)!, |
| 499 | permission: "read-write", |
| 500 | }, |
| 501 | ]), |
| 502 | write: () => |
| 503 | Effect.sync(() => { |
| 504 | calls.push("revoke"); |
| 505 | }).pipe( |
| 506 | Effect.andThen(scenario === "revocation-failed" ? Effect.fail(failure) : Effect.void), |
| 507 | ), |
| 508 | }); |
| 509 | let savedProfile: ConnectionProfile = profile; |
| 510 | const layerBroker = yield* makeDependencies({ |
| 511 | profileStore: { |
| 512 | get: () => Effect.sync(() => Option.some(savedProfile)), |
| 513 | put: (value) => |
| 514 | Effect.sync(() => { |
| 515 | calls.push("profile"); |
| 516 | savedProfile = value; |
| 517 | }), |
| 518 | remove: () => Effect.die("unused"), |
| 519 | }, |
| 520 | prepareSsh: () => |
| 521 | Effect.succeed({ |
| 522 | bootstrap: { |
| 523 | target: preparedTarget, |
| 524 | httpBaseUrl: "http://127.0.0.1:4010", |
| 525 | wsBaseUrl: "ws://127.0.0.1:4010", |
| 526 | pairingToken: null, |
| 527 | }, |
| 528 | bearerToken: "ssh-bearer", |
| 529 | }), |
| 530 | authorizeBearer: (input) => |
| 531 | Effect.sync(() => { |
| 532 | calls.push("authorize"); |
| 533 | return { |
| 534 | environmentId: input.expectedEnvironmentId, |
| 535 | label: "SSH", |
| 536 | httpBaseUrl: input.httpBaseUrl, |
| 537 | socketUrl: "ws://127.0.0.1:4010/ws?wsTicket=ssh", |
| 538 | httpAuthorization: { _tag: "Bearer" as const, token: input.bearerToken }, |
| 539 | }; |
| 540 | }), |
| 541 | }); |
| 542 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe( |
| 543 | Effect.provide(layerBroker), |
| 544 | ); |
| 545 | const prepare = broker |
| 546 | .prepare(entry) |
| 547 | .pipe(Effect.provideService(GitHubRoutingPermissions, permissions)); |
| 548 | if (scenario === "revocation-failed") { |
| 549 | expect(yield* Effect.flip(prepare)).toBe(failure); |
| 550 | expect(savedProfile).toBe(profile); |
| 551 | expect(calls).toEqual(["revoke"]); |
| 552 | } else { |
| 553 | expect((yield* prepare).socketUrl).toContain("wsTicket=ssh"); |
| 554 | expect(savedProfile).toMatchObject({ target: preparedTarget }); |
| 555 | expect(calls).toEqual( |
| 556 | scenario === "unchanged" |
| 557 | ? ["profile", "authorize"] |
| 558 | : ["revoke", "profile", "authorize"], |
| 559 | ); |
| 560 | } |
| 561 | expect(yield* permissions.get(entry)).toBe(scenario === "changed" ? "off" : "read-write"); |
| 562 | }), |
| 563 | ); |
| 564 | |
| 565 | it.effect("preserves relay authorization failure classification and trace details", () => |
| 566 | Effect.gen(function* () { |
| 567 | const target = new RelayConnectionTarget({ |
| 568 | environmentId: ENVIRONMENT_ID, |
| 569 | label: "Cloud", |
| 570 | }); |
| 571 | const authorizationError = new ConnectionTransientError({ |
| 572 | reason: "timeout", |
| 573 | detail: "Relay environment connection timed out.", |
| 574 | traceId: "relay-trace", |
| 575 | }); |
| 576 | const layerBroker = yield* makeDependencies({ |
| 577 | authorizeDpop: () => Effect.fail(authorizationError), |
| 578 | }); |
| 579 | const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(layerBroker)); |
| 580 | const error = yield* Effect.flip(broker.prepare(catalogEntry(target))); |
| 581 | |
| 582 | expect(error).toBe(authorizationError); |
| 583 | }), |
| 584 | ); |
| 585 | }); |