| @@ -1,5 +1,6 @@ |
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import { Effect, Predicate } from "effect"; | 2 | import { Effect, Predicate } from "effect"; |
| | 3 | import type * as Tracer from "effect/Tracer"; |
| 3 | | 4 | |
| 4 | import { | 5 | import { |
| 5 | AuthTemplateSlug, | 6 | AuthTemplateSlug, |
| @@ -29,6 +30,37 @@ const INTEG = IntegrationSlug.make("acme"); · 23 unmodified lines |
| 29 | const TEMPLATE = AuthTemplateSlug.make("oauth"); | 30 | const TEMPLATE = AuthTemplateSlug.make("oauth"); |
| 30 | const FIRST_PARTY = firstPartyOAuthClientSlug("acme"); | 31 | const FIRST_PARTY = firstPartyOAuthClientSlug("acme"); |
| 31 | | 32 | |
| | 33 | const makeRecordingTracer = (spans: Map<string, Map<string, unknown>>): Tracer.Tracer => ({ |
| | 34 | span: (options) => { |
| | 35 | const attributes = new Map<string, unknown>(); |
| | 36 | spans.set(options.name, attributes); |
| | 37 | let status: Tracer.SpanStatus = { _tag: "Started", startTime: options.startTime }; |
| | 38 | return { |
| | 39 | _tag: "Span", |
| | 40 | name: options.name, |
| | 41 | spanId: "0000000000000001", |
| | 42 | traceId: "00000000000000000000000000000001", |
| | 43 | parent: options.parent, |
| | 44 | annotations: options.annotations, |
| | 45 | get status() { |
| | 46 | return status; |
| | 47 | }, |
| | 48 | attributes, |
| | 49 | links: options.links, |
| | 50 | sampled: options.sampled, |
| | 51 | kind: options.kind, |
| | 52 | end: (endTime, exit) => { |
| | 53 | status = { _tag: "Ended", startTime: options.startTime, endTime, exit }; |
| | 54 | }, |
| | 55 | attribute: (key, value) => { |
| | 56 | attributes.set(key, value); |
| | 57 | }, |
| | 58 | event: () => undefined, |
| | 59 | addLinks: () => undefined, |
| | 60 | }; |
| | 61 | }, |
| | 62 | }); |
| | 63 | |
| 32 | const oauthPlugin = definePlugin(() => ({ | 64 | const oauthPlugin = definePlugin(() => ({ |
| 33 | id: "acme" as const, | 65 | id: "acme" as const, |
| 34 | storage: () => ({}), | 66 | storage: () => ({}), |
| @@ -81,8 +113,9 @@ const firstPartyClientFor = (server: { · 46 unmodified lines |
| 81 | describe("first-party oauth clients", () => { | 113 | describe("first-party oauth clients", () => { |
| 82 | it.effect( | 114 | it.effect( |
| 83 | "start → complete through a config-declared client mints an executable connection", | 115 | "start → complete through a config-declared client mints an executable connection", |
| 84 | () => | 116 | () => { |
| 85 | Effect.scoped( | 117 | const spans = new Map<string, Map<string, unknown>>(); |
| | 118 | return Effect.scoped( |
| 86 | Effect.gen(function* () { | 119 | Effect.gen(function* () { |
| 87 | const server = yield* serveOAuthTestServer({ scopes: ["read"] }); | 120 | const server = yield* serveOAuthTestServer({ scopes: ["read"] }); |
| 88 | const { executor } = yield* makeTestWorkspaceHarness({ | 121 | const { executor } = yield* makeTestWorkspaceHarness({ |
| @@ -101,6 +134,9 @@ describe("first-party oauth clients", () => { · 12 unmodified lines |
| 101 | template: TEMPLATE, | 134 | template: TEMPLATE, |
| 102 | }); | 135 | }); |
| 103 | expect(started.status).toBe("redirect"); | 136 | expect(started.status).toBe("redirect"); |
| | 137 | expect( |
| | 138 | spans.get("test.oauth.first_party")?.get("executor.oauth.client_first_party"), |
| | 139 | ).toBe(true); |
| 104 | if (started.status !== "redirect") return; | 140 | if (started.status !== "redirect") return; |
| 105 | | 141 | |
| 106 | const callback = yield* server.completeAuthorizationCodeFlow({ | 142 | const callback = yield* server.completeAuthorizationCodeFlow({ |
| @@ -111,6 +147,9 @@ describe("first-party oauth clients", () => { · 4 unmodified lines |
| 111 | code: callback.code, | 147 | code: callback.code, |
| 112 | }); | 148 | }); |
| 113 | expect(String(connection.address)).toBe("tools.acme.org.mainAccount"); | 149 | expect(String(connection.address)).toBe("tools.acme.org.mainAccount"); |
| | 150 | expect( |
| | 151 | spans.get("executor.oauth.complete")?.get("executor.oauth.client_first_party"), |
| | 152 | ).toBe(true); |
| 114 | | 153 | |
| 115 | const out = (yield* executor.execute( | 154 | const out = (yield* executor.execute( |
| 116 | ToolAddress.make("tools.acme.org.mainAccount.whoami"), | 155 | ToolAddress.make("tools.acme.org.mainAccount.whoami"), |
| @@ -119,7 +158,11 @@ describe("first-party oauth clients", () => { · 2 unmodified lines |
| 119 | expect(out.token).toMatch(/^at_/); | 158 | expect(out.token).toMatch(/^at_/); |
| 120 | expect(yield* server.acceptsAccessToken(out.token)).toBe(true); | 159 | expect(yield* server.acceptsAccessToken(out.token)).toBe(true); |
| 121 | }), | 160 | }), |
| 122 | ), | 161 | ).pipe( |
| | 162 | Effect.withSpan("test.oauth.first_party"), |
| | 163 | Effect.withTracer(makeRecordingTracer(spans)), |
| | 164 | ); |
| | 165 | }, |
| 123 | ); | 166 | ); |
| 124 | | 167 | |
| 125 | it.effect("refresh resolves the config-declared client (no oauth_client row exists)", () => | 168 | it.effect("refresh resolves the config-declared client (no oauth_client row exists)", () => |