Skip to content

Commit bcc7bc9

Browse files
committed
Use Effect OpenAPI fixtures in remaining tests
1 parent 24bf5ae commit bcc7bc9

4 files changed

Lines changed: 117 additions & 130 deletions

File tree

apps/cloud/src/mcp-miniflare.e2e.node.test.ts

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,14 @@ import { resolve } from "node:path";
2424
import { createServer } from "node:http";
2525
import type { AddressInfo } from "node:net";
2626

27-
import {
28-
HttpApi,
29-
HttpApiBuilder,
30-
HttpApiEndpoint,
31-
HttpApiGroup,
32-
OpenApi,
33-
} from "effect/unstable/httpapi";
34-
import { HttpRouter, HttpServer } from "effect/unstable/http";
35-
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
36-
import { Context, Data, Effect, Layer, Option, Predicate, Schema } from "effect";
27+
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
28+
import { Context, Data, Effect, Exit, Layer, Option, Schema, Scope } from "effect";
3729

3830
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3931
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4032
import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4133
import { unstable_dev, type Unstable_DevWorker } from "wrangler";
34+
import { serveOpenApiHttpApiTestServer } from "@executor-js/plugin-openapi/testing";
4235

4336
import { makeTestBearer } from "./test-bearer";
4437

@@ -64,22 +57,13 @@ const ApproveHandlers = HttpApiBuilder.group(UpstreamApi, "approve", (h) =>
6457
h.handle("approveThing", () => Effect.succeed(ApprovedResponse.make({ approved: true }))),
6558
);
6659

67-
const UpstreamApiLive = HttpApiBuilder.layer(UpstreamApi).pipe(Layer.provide(ApproveHandlers));
68-
69-
const UpstreamServeLayer = HttpRouter.serve(UpstreamApiLive).pipe(
70-
Layer.provide(UpstreamApiLive),
71-
Layer.provideMerge(HttpRouter.layer),
72-
Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port: 0, host: "127.0.0.1" })),
73-
);
74-
7560
// ---------------------------------------------------------------------------
7661
// Services
7762
// ---------------------------------------------------------------------------
7863

79-
class Upstream extends Context.Service<
80-
Upstream,
81-
{ readonly specJson: string; readonly url: string }
82-
>()("MiniflareE2E/Upstream") {}
64+
class Upstream extends Context.Service<Upstream, { readonly specJson: string }>()(
65+
"MiniflareE2E/Upstream",
66+
) {}
8367

8468
class Worker extends Context.Service<
8569
Worker,
@@ -116,23 +100,18 @@ class MiniflareE2ETestError extends Data.TaggedError("MiniflareE2ETestError")<{
116100

117101
const UpstreamLive = Layer.effect(
118102
Upstream,
119-
Effect.gen(function* () {
120-
const server = yield* HttpServer.HttpServer;
121-
const addr = server.address;
122-
if (!Predicate.isTagged("TcpAddress")(addr)) {
123-
return yield* new MiniflareE2ETestError({
124-
message: "upstream server bound to non-TCP address",
125-
cause: addr,
126-
});
127-
}
128-
const url = `http://127.0.0.1:${addr.port}`;
129-
const specJson = JSON.stringify({
130-
...OpenApi.fromApi(UpstreamApi),
131-
servers: [{ url }],
132-
});
133-
return { specJson, url };
134-
}),
135-
).pipe(Layer.provide(UpstreamServeLayer));
103+
Effect.acquireRelease(
104+
Effect.gen(function* () {
105+
const scope = yield* Scope.make();
106+
const server = yield* serveOpenApiHttpApiTestServer({
107+
api: UpstreamApi,
108+
handlersLayer: ApproveHandlers,
109+
}).pipe(Scope.provide(scope));
110+
return { server, scope };
111+
}),
112+
({ scope }) => Scope.close(scope, Exit.void),
113+
).pipe(Effect.map(({ server }) => ({ specJson: server.specJson }))),
114+
);
136115

137116
// ---------------------------------------------------------------------------
138117
// Telemetry receiver — a node HTTP server on a random port that speaks

apps/cloud/src/services/tenant-isolation.node.test.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,22 @@
33
// on the full cloud module graph.
44

55
import { describe, expect, it } from "@effect/vitest";
6-
import { Effect, Result } from "effect";
6+
import { Effect, Result, Schema } from "effect";
7+
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi";
78

89
import { ConnectionId, ScopeId, SecretId } from "@executor-js/sdk";
910

1011
import { asOrg } from "./__test-harness__/api-harness";
1112

12-
const MINIMAL_OPENAPI_SPEC = JSON.stringify({
13-
openapi: "3.0.0",
14-
info: { title: "Tenant Test API", version: "1.0.0" },
15-
paths: {
16-
"/ping": {
17-
get: {
18-
operationId: "ping",
19-
responses: { "200": { description: "ok" } },
20-
},
21-
},
22-
},
23-
});
13+
const PingGroup = HttpApiGroup.make("default", { topLevel: true }).add(
14+
HttpApiEndpoint.get("ping", "/ping", { success: Schema.Unknown }),
15+
);
16+
17+
const TenantIsolationApi = HttpApi.make("tenantIsolationTest")
18+
.add(PingGroup)
19+
.annotateMerge(OpenApi.annotations({ title: "Tenant Test API", version: "1.0.0" }));
20+
21+
const MINIMAL_OPENAPI_SPEC = JSON.stringify(OpenApi.fromApi(TenantIsolationApi));
2422

2523
describe("tenant isolation (HTTP)", () => {
2624
it.effect("write requests cannot target another org scope", () =>

packages/plugins/openapi/src/sdk/index.test.ts

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,36 @@ const PetstoreApi = HttpApi.make("petstore").add(PetstoreGroup);
4141
// Generate OpenAPI spec from the Effect API definition
4242
const spec = OpenApi.fromApi(PetstoreApi);
4343

44+
type TestOpenApiServer = {
45+
readonly url: string;
46+
readonly description?: string;
47+
readonly variables?: Record<
48+
string,
49+
{
50+
readonly default: string;
51+
readonly enum?: [string, ...string[]];
52+
readonly description?: string;
53+
}
54+
>;
55+
};
56+
57+
const pingSpecWithServers = (title: string, servers: readonly TestOpenApiServer[]) =>
58+
OpenApi.fromApi(
59+
HttpApi.make("serverVariablesTest")
60+
.add(
61+
HttpApiGroup.make("default", { topLevel: true }).add(
62+
HttpApiEndpoint.get("ping", "/ping", { success: Schema.Unknown }),
63+
),
64+
)
65+
.annotateMerge(
66+
OpenApi.annotations({
67+
title,
68+
version: "1.0.0",
69+
servers,
70+
}),
71+
),
72+
);
73+
4474
// ---------------------------------------------------------------------------
4575
// Tests
4676
// ---------------------------------------------------------------------------
@@ -165,26 +195,19 @@ describe("OpenAPI plugin", () => {
165195

166196
it.effect("extracts server variables with enum and description", () =>
167197
Effect.gen(function* () {
168-
const specWithServerVars = {
169-
openapi: "3.0.0",
170-
info: { title: "Sentry", version: "1.0.0" },
171-
servers: [
172-
{
173-
url: "https://{region}.sentry.io",
174-
description: "Regional endpoint",
175-
variables: {
176-
region: {
177-
default: "us",
178-
description: "The data-storage-location for an organization",
179-
enum: ["us", "de"],
180-
},
198+
const specWithServerVars = pingSpecWithServers("Sentry", [
199+
{
200+
url: "https://{region}.sentry.io",
201+
description: "Regional endpoint",
202+
variables: {
203+
region: {
204+
default: "us",
205+
description: "The data-storage-location for an organization",
206+
enum: ["us", "de"],
181207
},
182208
},
183-
],
184-
paths: {
185-
"/ping": { get: { responses: { "200": { description: "ok" } } } },
186209
},
187-
};
210+
]);
188211
// @effect-diagnostics-next-line preferSchemaOverJson:off
189212
const doc = yield* parse(JSON.stringify(specWithServerVars));
190213
const result = yield* extract(doc);
@@ -212,27 +235,20 @@ describe("OpenAPI plugin", () => {
212235
// ---------------------------------------------------------------------------
213236

214237
describe("extract — server variables", () => {
215-
const specWithServerVars = {
216-
openapi: "3.0.0",
217-
info: { title: "Test", version: "1.0.0" },
218-
servers: [
219-
{
220-
url: "https://{region}.example.com/{basePath}",
221-
description: "Regional endpoint",
222-
variables: {
223-
region: {
224-
default: "us",
225-
enum: ["us", "eu", "ap"],
226-
description: "Data region",
227-
},
228-
basePath: { default: "v1" },
238+
const specWithServerVars = pingSpecWithServers("Test", [
239+
{
240+
url: "https://{region}.example.com/{basePath}",
241+
description: "Regional endpoint",
242+
variables: {
243+
region: {
244+
default: "us",
245+
enum: ["us", "eu", "ap"],
246+
description: "Data region",
229247
},
248+
basePath: { default: "v1" },
230249
},
231-
],
232-
paths: {
233-
"/ping": { get: { responses: { "200": { description: "ok" } } } },
234250
},
235-
};
251+
]);
236252

237253
it.effect("preserves enum, default, and description for server variables", () =>
238254
Effect.gen(function* () {

packages/plugins/openapi/src/sdk/preview-oauth2.test.ts

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,43 @@
88
// ---------------------------------------------------------------------------
99

1010
import { describe, expect, it } from "@effect/vitest";
11-
import { Effect, Option } from "effect";
11+
import { Effect, Option, Schema } from "effect";
1212
import { FetchHttpClient } from "effect/unstable/http";
13+
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi";
1314

1415
import { previewSpec as previewSpecRaw } from "./preview";
1516

1617
const previewSpec = (input: string) =>
1718
previewSpecRaw(input).pipe(Effect.provide(FetchHttpClient.layer));
1819

20+
const PreviewGroup = HttpApiGroup.make("default", { topLevel: true }).add(
21+
HttpApiEndpoint.get("ping", "/ping", { success: Schema.Unknown }),
22+
);
23+
24+
const PreviewApi = HttpApi.make("previewOauth2Test")
25+
.add(PreviewGroup)
26+
.annotateMerge(
27+
OpenApi.annotations({
28+
title: "Test API",
29+
version: "1.0.0",
30+
servers: [{ url: "https://api.example.com" }],
31+
}),
32+
);
33+
1934
const minimalSpec = (
2035
securitySchemes: Record<string, unknown>,
2136
components: Record<string, unknown> = {},
22-
) => ({
23-
openapi: "3.0.0",
24-
info: { title: "Test API", version: "1.0.0" },
25-
servers: [{ url: "https://api.example.com" }],
26-
paths: {
27-
"/ping": {
28-
get: { responses: { "200": { description: "ok" } } },
29-
},
30-
},
31-
components: { ...components, securitySchemes },
32-
});
37+
) =>
38+
OpenApi.fromApi(
39+
PreviewApi.annotateMerge(
40+
OpenApi.annotations({
41+
transform: (spec) => ({
42+
...spec,
43+
components: { ...components, securitySchemes },
44+
}),
45+
}),
46+
),
47+
);
3348

3449
describe("previewSpec OAuth2 extraction", () => {
3550
it.effect("extracts authorizationCode flow with URLs, scopes, refreshUrl", () =>
@@ -139,37 +154,17 @@ describe("previewSpec OAuth2 extraction", () => {
139154

140155
it.effect("resolves security schemes defined via $ref", () =>
141156
Effect.gen(function* () {
142-
const spec = minimalSpec(
143-
{
144-
api_token: { $ref: "#/components/securitySchemes/_api_token_impl" },
145-
},
146-
{
147-
securitySchemes: {
148-
_api_token_impl: {
149-
type: "http",
150-
scheme: "bearer",
151-
bearerFormat: "JWT",
152-
description: "Internal token scheme",
153-
},
154-
},
155-
},
156-
);
157-
// Note: the outer securitySchemes at `components.securitySchemes` is
158-
// what previewSpec reads; the `_api_token_impl` shim inside
159-
// components.securitySchemes allows $ref resolution via the resolver.
160-
// The test spec above is slightly awkward because we have to nest both
161-
// under the same key — adjust by merging.
162-
spec.components = {
163-
securitySchemes: {
164-
api_token: { $ref: "#/components/securitySchemes/_api_token_impl" },
165-
_api_token_impl: {
166-
type: "http",
167-
scheme: "bearer",
168-
bearerFormat: "JWT",
169-
description: "Internal token scheme",
170-
},
157+
const spec = minimalSpec({
158+
api_token: { $ref: "#/components/securitySchemes/_api_token_impl" },
159+
_api_token_impl: {
160+
type: "http",
161+
scheme: "bearer",
162+
bearerFormat: "JWT",
163+
description: "Internal token scheme",
171164
},
172-
};
165+
});
166+
// Note: `api_token` should resolve through the sibling
167+
// `_api_token_impl` scheme in `components.securitySchemes`.
173168

174169
const preview = yield* previewSpec(JSON.stringify(spec));
175170
// Both keys are present, but the `api_token` entry should resolve to
@@ -181,7 +176,6 @@ describe("previewSpec OAuth2 extraction", () => {
181176
expect(Option.getOrElse(apiToken!.bearerFormat, () => "")).toBe("JWT");
182177
}),
183178
);
184-
185179
it.effect("captures openIdConnectUrl for openIdConnect schemes", () =>
186180
Effect.gen(function* () {
187181
const spec = minimalSpec({

0 commit comments

Comments
 (0)