@schmock/openapi 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/crud-detector.d.ts +35 -0
  2. package/dist/crud-detector.d.ts.map +1 -0
  3. package/dist/crud-detector.js +153 -0
  4. package/dist/generators.d.ts +14 -0
  5. package/dist/generators.d.ts.map +1 -0
  6. package/dist/generators.js +158 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +221 -0
  10. package/dist/normalizer.d.ts +14 -0
  11. package/dist/normalizer.d.ts.map +1 -0
  12. package/dist/normalizer.js +194 -0
  13. package/dist/parser.d.ts +32 -0
  14. package/dist/parser.d.ts.map +1 -0
  15. package/dist/parser.js +282 -0
  16. package/dist/plugin.d.ts +32 -0
  17. package/dist/plugin.d.ts.map +1 -0
  18. package/dist/plugin.js +129 -0
  19. package/dist/seed.d.ts +15 -0
  20. package/dist/seed.d.ts.map +1 -0
  21. package/dist/seed.js +41 -0
  22. package/package.json +45 -0
  23. package/src/__fixtures__/faker-stress-test.openapi.yaml +1030 -0
  24. package/src/__fixtures__/openapi31.json +34 -0
  25. package/src/__fixtures__/petstore-openapi3.json +168 -0
  26. package/src/__fixtures__/petstore-swagger2.json +141 -0
  27. package/src/__fixtures__/scalar-galaxy.yaml +1314 -0
  28. package/src/__fixtures__/stripe-fixtures3.json +6542 -0
  29. package/src/__fixtures__/stripe-spec3.yaml +161621 -0
  30. package/src/__fixtures__/train-travel.yaml +1264 -0
  31. package/src/crud-detector.test.ts +150 -0
  32. package/src/crud-detector.ts +194 -0
  33. package/src/generators.test.ts +214 -0
  34. package/src/generators.ts +212 -0
  35. package/src/index.ts +4 -0
  36. package/src/normalizer.test.ts +253 -0
  37. package/src/normalizer.ts +233 -0
  38. package/src/parser.test.ts +181 -0
  39. package/src/parser.ts +389 -0
  40. package/src/plugin.test.ts +205 -0
  41. package/src/plugin.ts +185 -0
  42. package/src/seed.ts +62 -0
  43. package/src/steps/openapi-crud.steps.ts +132 -0
  44. package/src/steps/openapi-parsing.steps.ts +111 -0
  45. package/src/steps/openapi-seed.steps.ts +94 -0
  46. package/src/stress.test.ts +2814 -0
@@ -0,0 +1,205 @@
1
+ import { resolve } from "node:path";
2
+ import { schmock } from "@schmock/core";
3
+ import { describe, expect, it } from "vitest";
4
+ import { openapi } from "./plugin";
5
+
6
+ const fixturesDir = resolve(import.meta.dirname, "__fixtures__");
7
+
8
+ describe("openapi plugin", () => {
9
+ describe("Swagger 2.0 integration", () => {
10
+ it("auto-registers all routes from Petstore spec", async () => {
11
+ const mock = schmock({ state: {} });
12
+ mock.pipe(
13
+ await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }),
14
+ );
15
+
16
+ // List (should be empty initially)
17
+ const listResponse = await mock.handle("GET", "/pets");
18
+ expect(listResponse.status).toBe(200);
19
+ expect(listResponse.body).toEqual([]);
20
+ });
21
+
22
+ it("CRUD lifecycle with Swagger 2.0", async () => {
23
+ const mock = schmock({ state: {} });
24
+ mock.pipe(
25
+ await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }),
26
+ );
27
+
28
+ // Create
29
+ const created = await mock.handle("POST", "/pets", {
30
+ body: { name: "Buddy", tag: "dog" },
31
+ });
32
+ expect(created.status).toBe(201);
33
+ expect(created.body).toMatchObject({ name: "Buddy", petId: 1 });
34
+
35
+ // Read
36
+ const read = await mock.handle("GET", "/pets/1");
37
+ expect(read.status).toBe(200);
38
+ expect(read.body).toMatchObject({ name: "Buddy", petId: 1 });
39
+
40
+ // Update
41
+ const updated = await mock.handle("PUT", "/pets/1", {
42
+ body: { name: "Max" },
43
+ });
44
+ expect(updated.status).toBe(200);
45
+ expect(updated.body).toMatchObject({ name: "Max", petId: 1 });
46
+
47
+ // List
48
+ const list = await mock.handle("GET", "/pets");
49
+ expect(list.status).toBe(200);
50
+ expect(list.body).toHaveLength(1);
51
+
52
+ // Delete
53
+ const deleted = await mock.handle("DELETE", "/pets/1");
54
+ expect(deleted.status).toBe(204);
55
+
56
+ // Verify deletion
57
+ const afterDelete = await mock.handle("GET", "/pets/1");
58
+ expect(afterDelete.status).toBe(404);
59
+ expect(afterDelete.body).toMatchObject({
60
+ error: "Not found",
61
+ code: "NOT_FOUND",
62
+ });
63
+ });
64
+
65
+ it("handles non-CRUD endpoints", async () => {
66
+ const mock = schmock({ state: {} });
67
+ mock.pipe(
68
+ await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }),
69
+ );
70
+
71
+ // Health endpoint — should return a generated response
72
+ const health = await mock.handle("GET", "/health");
73
+ expect(health.status).toBe(200);
74
+ expect(health.body).toBeDefined();
75
+ });
76
+ });
77
+
78
+ describe("OpenAPI 3.0 integration", () => {
79
+ it("auto-registers routes from OpenAPI 3.0 spec", async () => {
80
+ const mock = schmock({ state: {} });
81
+ mock.pipe(
82
+ await openapi({ spec: `${fixturesDir}/petstore-openapi3.json` }),
83
+ );
84
+
85
+ const list = await mock.handle("GET", "/pets");
86
+ expect(list.status).toBe(200);
87
+ expect(Array.isArray(list.body)).toBe(true);
88
+ });
89
+ });
90
+
91
+ describe("seed data", () => {
92
+ it("seeds inline data", async () => {
93
+ const mock = schmock({ state: {} });
94
+ mock.pipe(
95
+ await openapi({
96
+ spec: `${fixturesDir}/petstore-swagger2.json`,
97
+ seed: {
98
+ pets: [
99
+ { petId: 1, name: "Buddy" },
100
+ { petId: 2, name: "Max" },
101
+ ],
102
+ },
103
+ }),
104
+ );
105
+
106
+ const list = await mock.handle("GET", "/pets");
107
+ expect(list.status).toBe(200);
108
+ expect(list.body).toHaveLength(2);
109
+ });
110
+
111
+ it("seeds auto-generated data from schema", async () => {
112
+ const mock = schmock({ state: {} });
113
+ mock.pipe(
114
+ await openapi({
115
+ spec: `${fixturesDir}/petstore-swagger2.json`,
116
+ seed: {
117
+ pets: { count: 5 },
118
+ },
119
+ }),
120
+ );
121
+
122
+ const list = await mock.handle("GET", "/pets");
123
+ expect(list.status).toBe(200);
124
+ expect(list.body).toHaveLength(5);
125
+ });
126
+
127
+ it("seeded data works with read endpoints", async () => {
128
+ const mock = schmock({ state: {} });
129
+ mock.pipe(
130
+ await openapi({
131
+ spec: `${fixturesDir}/petstore-swagger2.json`,
132
+ seed: {
133
+ pets: [{ petId: 42, name: "Luna" }],
134
+ },
135
+ }),
136
+ );
137
+
138
+ const read = await mock.handle("GET", "/pets/42");
139
+ expect(read.status).toBe(200);
140
+ expect(read.body).toMatchObject({ petId: 42, name: "Luna" });
141
+ });
142
+
143
+ it("continues auto-incrementing after seeded data", async () => {
144
+ const mock = schmock({ state: {} });
145
+ mock.pipe(
146
+ await openapi({
147
+ spec: `${fixturesDir}/petstore-swagger2.json`,
148
+ seed: {
149
+ pets: [
150
+ { petId: 1, name: "Buddy" },
151
+ { petId: 2, name: "Max" },
152
+ ],
153
+ },
154
+ }),
155
+ );
156
+
157
+ const created = await mock.handle("POST", "/pets", {
158
+ body: { name: "New" },
159
+ });
160
+ expect(created.status).toBe(201);
161
+ // Should get ID 3 since max existing ID is 2
162
+ expect(created.body).toMatchObject({ petId: 3 });
163
+ });
164
+ });
165
+
166
+ describe("404 handling", () => {
167
+ it("returns 404 for non-existent items", async () => {
168
+ const mock = schmock({ state: {} });
169
+ mock.pipe(
170
+ await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }),
171
+ );
172
+
173
+ const read = await mock.handle("GET", "/pets/999");
174
+ expect(read.status).toBe(404);
175
+ expect(read.body).toMatchObject({
176
+ error: "Not found",
177
+ code: "NOT_FOUND",
178
+ });
179
+ });
180
+ });
181
+
182
+ describe("state isolation", () => {
183
+ it("isolates state between separate mock instances", async () => {
184
+ const mock1 = schmock({ state: {} });
185
+ mock1.pipe(
186
+ await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }),
187
+ );
188
+
189
+ const mock2 = schmock({ state: {} });
190
+ mock2.pipe(
191
+ await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }),
192
+ );
193
+
194
+ await mock1.handle("POST", "/pets", { body: { name: "A" } });
195
+
196
+ const list1 = await mock1.handle("GET", "/pets");
197
+ const list2 = await mock2.handle("GET", "/pets");
198
+
199
+ const body1 = list1.body;
200
+ const body2 = list2.body;
201
+ expect(Array.isArray(body1) && body1.length).toBe(1);
202
+ expect(Array.isArray(body2) && body2.length).toBe(0);
203
+ });
204
+ });
205
+ });
package/src/plugin.ts ADDED
@@ -0,0 +1,185 @@
1
+ /// <reference path="../../../types/schmock.d.ts" />
2
+
3
+ import type { CrudResource } from "./crud-detector.js";
4
+ import { detectCrudResources } from "./crud-detector.js";
5
+ import {
6
+ createCreateGenerator,
7
+ createDeleteGenerator,
8
+ createListGenerator,
9
+ createReadGenerator,
10
+ createStaticGenerator,
11
+ createUpdateGenerator,
12
+ } from "./generators.js";
13
+ import { parseSpec } from "./parser.js";
14
+ import type { SeedConfig, SeedSource } from "./seed.js";
15
+ import { loadSeed } from "./seed.js";
16
+
17
+ export type { SeedConfig, SeedSource };
18
+
19
+ function isRecord(value: unknown): value is Record<string, unknown> {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21
+ }
22
+
23
+ export interface OpenApiOptions {
24
+ /** File path or inline spec object */
25
+ spec: string | object;
26
+ /** Optional seed data per resource */
27
+ seed?: SeedConfig;
28
+ /** Validate request bodies (default: true) */
29
+ validateRequests?: boolean;
30
+ /** Validate response bodies (default: false) */
31
+ validateResponses?: boolean;
32
+ /** Query features for list endpoints */
33
+ queryFeatures?: {
34
+ pagination?: boolean;
35
+ sorting?: boolean;
36
+ filtering?: boolean;
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Create an OpenAPI plugin that auto-registers CRUD routes from a spec.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const mock = schmock();
46
+ * mock.pipe(await openapi({
47
+ * spec: "./petstore.yaml",
48
+ * seed: { pets: { count: 10 } },
49
+ * }));
50
+ * ```
51
+ */
52
+ export async function openapi(
53
+ options: OpenApiOptions,
54
+ ): Promise<Schmock.Plugin> {
55
+ const spec = await parseSpec(options.spec);
56
+ const { resources, nonCrudPaths } = detectCrudResources(spec.paths);
57
+ const seedData = options.seed
58
+ ? loadSeed(options.seed, resources)
59
+ : new Map<string, unknown[]>();
60
+
61
+ return {
62
+ name: "@schmock/openapi",
63
+ version: "1.0.0",
64
+
65
+ install(instance: Schmock.CallableMockInstance) {
66
+ // Seed initial state — we need state to be initialized before registering routes
67
+ // The state is populated via generator context.state on first request,
68
+ // but we need to pre-populate it. We'll use a global config state approach.
69
+ // Since generators use ctx.state (the global config state), we set up seed
70
+ // data through a setup route that runs on first access, or we just seed
71
+ // in the generators themselves.
72
+
73
+ // Register CRUD routes
74
+ for (const resource of resources) {
75
+ registerCrudRoutes(instance, resource, seedData.get(resource.name));
76
+ }
77
+
78
+ // Register non-CRUD routes with static generators
79
+ for (const parsedPath of nonCrudPaths) {
80
+ const routeKey =
81
+ `${parsedPath.method} ${parsedPath.path}` as Schmock.RouteKey;
82
+ instance(routeKey, createStaticGenerator(parsedPath));
83
+ }
84
+ },
85
+
86
+ process(
87
+ context: Schmock.PluginContext,
88
+ response?: unknown,
89
+ ): Schmock.PluginResult {
90
+ // Pass through — generators handle everything
91
+ return { context, response };
92
+ },
93
+ };
94
+ }
95
+
96
+ function registerCrudRoutes(
97
+ instance: Schmock.CallableMockInstance,
98
+ resource: CrudResource,
99
+ seedItems?: unknown[],
100
+ ): void {
101
+ // Create a seeded generator wrapper that initializes state on first call
102
+ const ensureSeeded = createSeeder(resource, seedItems);
103
+
104
+ for (const op of resource.operations) {
105
+ switch (op) {
106
+ case "list": {
107
+ const gen = createListGenerator(resource);
108
+ const routeKey = `GET ${resource.basePath}` as Schmock.RouteKey;
109
+ instance(routeKey, wrapWithSeeder(ensureSeeded, gen));
110
+ break;
111
+ }
112
+ case "create": {
113
+ const gen = createCreateGenerator(resource);
114
+ const routeKey = `POST ${resource.basePath}` as Schmock.RouteKey;
115
+ instance(routeKey, wrapWithSeeder(ensureSeeded, gen));
116
+ break;
117
+ }
118
+ case "read": {
119
+ const gen = createReadGenerator(resource);
120
+ const routeKey = `GET ${resource.itemPath}` as Schmock.RouteKey;
121
+ instance(routeKey, wrapWithSeeder(ensureSeeded, gen));
122
+ break;
123
+ }
124
+ case "update": {
125
+ const gen = createUpdateGenerator(resource);
126
+ const putKey = `PUT ${resource.itemPath}` as Schmock.RouteKey;
127
+ const patchKey = `PATCH ${resource.itemPath}` as Schmock.RouteKey;
128
+ instance(putKey, wrapWithSeeder(ensureSeeded, gen));
129
+ instance(patchKey, wrapWithSeeder(ensureSeeded, gen));
130
+ break;
131
+ }
132
+ case "delete": {
133
+ const gen = createDeleteGenerator(resource);
134
+ const routeKey = `DELETE ${resource.itemPath}` as Schmock.RouteKey;
135
+ instance(routeKey, wrapWithSeeder(ensureSeeded, gen));
136
+ break;
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Create a seeder function that initializes collection state once.
144
+ */
145
+ function createSeeder(
146
+ resource: CrudResource,
147
+ seedItems?: unknown[],
148
+ ): (state: Record<string, unknown>) => void {
149
+ const stateKey = `openapi:collections:${resource.name}`;
150
+ const counterKey = `openapi:counter:${resource.name}`;
151
+ const seededKey = `openapi:seeded:${resource.name}`;
152
+
153
+ return (state: Record<string, unknown>) => {
154
+ if (state[seededKey]) return;
155
+ state[seededKey] = true;
156
+
157
+ if (seedItems && seedItems.length > 0) {
158
+ state[stateKey] = [...seedItems];
159
+ // Set counter to highest existing ID
160
+ let maxId = 0;
161
+ for (const item of seedItems) {
162
+ if (isRecord(item) && resource.idParam in item) {
163
+ const id = item[resource.idParam];
164
+ if (typeof id === "number" && id > maxId) {
165
+ maxId = id;
166
+ }
167
+ }
168
+ }
169
+ state[counterKey] = maxId;
170
+ } else {
171
+ state[stateKey] = [];
172
+ state[counterKey] = 0;
173
+ }
174
+ };
175
+ }
176
+
177
+ function wrapWithSeeder(
178
+ seeder: (state: Record<string, unknown>) => void,
179
+ generator: Schmock.GeneratorFunction,
180
+ ): Schmock.GeneratorFunction {
181
+ return (ctx: Schmock.RequestContext) => {
182
+ seeder(ctx.state);
183
+ return generator(ctx);
184
+ };
185
+ }
package/src/seed.ts ADDED
@@ -0,0 +1,62 @@
1
+ /// <reference path="../../../types/schmock.d.ts" />
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import type { CrudResource } from "./crud-detector.js";
5
+ import { generateSeedItems } from "./generators.js";
6
+
7
+ export type SeedSource = unknown[] | string | { count: number };
8
+
9
+ export type SeedConfig = Record<string, SeedSource>;
10
+
11
+ /**
12
+ * Load seed data for CRUD resources.
13
+ *
14
+ * Sources:
15
+ * - unknown[]: inline array of objects
16
+ * - string: file path to a JSON array
17
+ * - { count: number }: auto-generate N items from resource schema
18
+ */
19
+ export function loadSeed(
20
+ config: SeedConfig,
21
+ resources: CrudResource[],
22
+ ): Map<string, unknown[]> {
23
+ const result = new Map<string, unknown[]>();
24
+
25
+ for (const [resourceName, source] of Object.entries(config)) {
26
+ const resource = resources.find((r) => r.name === resourceName);
27
+
28
+ if (Array.isArray(source)) {
29
+ // Inline array
30
+ result.set(resourceName, [...source]);
31
+ } else if (typeof source === "string") {
32
+ // File path
33
+ const content = readFileSync(source, "utf-8");
34
+ const parsed: unknown = JSON.parse(content);
35
+ if (!Array.isArray(parsed)) {
36
+ throw new Error(
37
+ `Seed file "${source}" for resource "${resourceName}" must contain a JSON array`,
38
+ );
39
+ }
40
+ result.set(resourceName, parsed);
41
+ } else if (
42
+ typeof source === "object" &&
43
+ source !== null &&
44
+ "count" in source
45
+ ) {
46
+ // Auto-generate from schema
47
+ if (!resource?.schema) {
48
+ throw new Error(
49
+ `Cannot auto-generate seed for "${resourceName}": no schema found in spec`,
50
+ );
51
+ }
52
+ const items = generateSeedItems(
53
+ resource.schema,
54
+ source.count,
55
+ resource.idParam,
56
+ );
57
+ result.set(resourceName, items);
58
+ }
59
+ }
60
+
61
+ return result;
62
+ }
@@ -0,0 +1,132 @@
1
+ import { resolve } from "node:path";
2
+ import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
3
+ import { schmock } from "@schmock/core";
4
+ import { expect } from "vitest";
5
+ import { openapi } from "../plugin";
6
+
7
+ const feature = await loadFeature("../../features/openapi-crud.feature");
8
+ const fixturesDir = resolve(import.meta.dirname, "../__fixtures__");
9
+
10
+ describeFeature(feature, ({ Scenario }) => {
11
+ let mock: Schmock.CallableMockInstance;
12
+ let response: Schmock.Response;
13
+ let createdId: number;
14
+
15
+ Scenario("Full CRUD lifecycle", ({ Given, When, Then, And }) => {
16
+ Given("a mock with the Petstore spec loaded", async () => {
17
+ mock = schmock({ state: {} });
18
+ mock.pipe(
19
+ await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }),
20
+ );
21
+ });
22
+
23
+ When("I create a pet named {string}", async (_, name: string) => {
24
+ response = await mock.handle("POST", "/pets", {
25
+ body: { name, tag: "dog" },
26
+ });
27
+ const body = response.body as Record<string, unknown>;
28
+ createdId = body.petId as number;
29
+ });
30
+
31
+ Then("the create response has status 201", () => {
32
+ expect(response.status).toBe(201);
33
+ });
34
+
35
+ And("the created pet has name {string}", (_, name: string) => {
36
+ const body = response.body as Record<string, unknown>;
37
+ expect(body.name).toBe(name);
38
+ });
39
+
40
+ When("I read the created pet", async () => {
41
+ response = await mock.handle("GET", `/pets/${createdId}`);
42
+ });
43
+
44
+ Then("the read response has status 200", () => {
45
+ expect(response.status).toBe(200);
46
+ });
47
+
48
+ And("the pet has name {string}", (_, name: string) => {
49
+ const body = response.body as Record<string, unknown>;
50
+ expect(body.name).toBe(name);
51
+ });
52
+
53
+ When("I update the pet name to {string}", async (_, name: string) => {
54
+ response = await mock.handle("PUT", `/pets/${createdId}`, {
55
+ body: { name },
56
+ });
57
+ });
58
+
59
+ Then("the update response has status 200", () => {
60
+ expect(response.status).toBe(200);
61
+ });
62
+
63
+ When("I list all pets", async () => {
64
+ response = await mock.handle("GET", "/pets");
65
+ });
66
+
67
+ Then("the list contains {int} item", (_, count: number) => {
68
+ expect(response.body).toHaveLength(count);
69
+ });
70
+
71
+ When("I delete the pet", async () => {
72
+ response = await mock.handle("DELETE", `/pets/${createdId}`);
73
+ });
74
+
75
+ Then("the delete response has status 204", () => {
76
+ expect(response.status).toBe(204);
77
+ });
78
+
79
+ When("I list all pets after deletion", async () => {
80
+ response = await mock.handle("GET", "/pets");
81
+ });
82
+
83
+ Then("the list is empty", () => {
84
+ expect(response.body).toEqual([]);
85
+ });
86
+ });
87
+
88
+ Scenario(
89
+ "Read non-existent resource returns 404",
90
+ ({ Given, When, Then, And }) => {
91
+ Given("a mock with the Petstore spec loaded", async () => {
92
+ mock = schmock({ state: {} });
93
+ mock.pipe(
94
+ await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }),
95
+ );
96
+ });
97
+
98
+ When("I read pet with id 999", async () => {
99
+ response = await mock.handle("GET", "/pets/999");
100
+ });
101
+
102
+ Then("the response status is 404", () => {
103
+ expect(response.status).toBe(404);
104
+ });
105
+
106
+ And("the response has error code {string}", (_, code: string) => {
107
+ const body = response.body as Record<string, unknown>;
108
+ expect(body.code).toBe(code);
109
+ });
110
+ },
111
+ );
112
+
113
+ Scenario(
114
+ "Delete non-existent resource returns 404",
115
+ ({ Given, When, Then }) => {
116
+ Given("a mock with the Petstore spec loaded", async () => {
117
+ mock = schmock({ state: {} });
118
+ mock.pipe(
119
+ await openapi({ spec: `${fixturesDir}/petstore-swagger2.json` }),
120
+ );
121
+ });
122
+
123
+ When("I delete pet with id 999", async () => {
124
+ response = await mock.handle("DELETE", "/pets/999");
125
+ });
126
+
127
+ Then("the response status is 404", () => {
128
+ expect(response.status).toBe(404);
129
+ });
130
+ },
131
+ );
132
+ });
@@ -0,0 +1,111 @@
1
+ import { resolve } from "node:path";
2
+ import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
3
+ import { schmock } from "@schmock/core";
4
+ import { expect } from "vitest";
5
+ import type { ParsedSpec } from "../parser";
6
+ import { parseSpec } from "../parser";
7
+ import { openapi } from "../plugin";
8
+
9
+ const feature = await loadFeature("../../features/openapi-parsing.feature");
10
+ const fixturesDir = resolve(import.meta.dirname, "../__fixtures__");
11
+
12
+ describeFeature(feature, ({ Scenario }) => {
13
+ let parsedSpec: ParsedSpec;
14
+ let mock: Schmock.CallableMockInstance;
15
+
16
+ Scenario("Parse Swagger 2.0 spec", ({ Given, When, Then, And }) => {
17
+ Given("a Swagger 2.0 Petstore spec", () => {
18
+ // Spec will be loaded in the When step
19
+ });
20
+
21
+ When("I create an openapi plugin from the spec", async () => {
22
+ parsedSpec = await parseSpec(`${fixturesDir}/petstore-swagger2.json`);
23
+ });
24
+
25
+ Then("the parsed spec has title {string}", (_, title: string) => {
26
+ expect(parsedSpec.title).toBe(title);
27
+ });
28
+
29
+ And("the parsed spec has version {string}", (_, version: string) => {
30
+ expect(parsedSpec.version).toBe(version);
31
+ });
32
+
33
+ And("the parsed spec has basePath {string}", (_, basePath: string) => {
34
+ expect(parsedSpec.basePath).toBe(basePath);
35
+ });
36
+ });
37
+
38
+ Scenario("Parse OpenAPI 3.0 spec", ({ Given, When, Then, And }) => {
39
+ Given("an OpenAPI 3.0 Petstore spec", () => {
40
+ // Spec will be loaded in the When step
41
+ });
42
+
43
+ When("I create an openapi plugin from the spec", async () => {
44
+ parsedSpec = await parseSpec(`${fixturesDir}/petstore-openapi3.json`);
45
+ mock = schmock({ state: {} });
46
+ mock.pipe(
47
+ await openapi({ spec: `${fixturesDir}/petstore-openapi3.json` }),
48
+ );
49
+ });
50
+
51
+ Then("the parsed spec has title {string}", (_, title: string) => {
52
+ expect(parsedSpec.title).toBe(title);
53
+ });
54
+
55
+ And("the parsed spec has version {string}", (_, version: string) => {
56
+ expect(parsedSpec.version).toBe(version);
57
+ });
58
+
59
+ And("routes are auto-registered from the spec", async () => {
60
+ const response = await mock.handle("GET", "/pets");
61
+ expect(response.status).toBe(200);
62
+ });
63
+ });
64
+
65
+ Scenario("Parse inline spec object", ({ Given, When, Then }) => {
66
+ Given("an inline OpenAPI spec object", () => {
67
+ // Inline spec prepared in When step
68
+ });
69
+
70
+ When("I create an openapi plugin from the inline spec", async () => {
71
+ mock = schmock({ state: {} });
72
+ mock.pipe(
73
+ await openapi({
74
+ spec: {
75
+ openapi: "3.0.3",
76
+ info: { title: "Inline", version: "1.0.0" },
77
+ paths: {
78
+ "/hello": {
79
+ get: {
80
+ responses: {
81
+ "200": {
82
+ description: "Hello",
83
+ content: {
84
+ "application/json": {
85
+ schema: {
86
+ type: "object",
87
+ properties: { msg: { type: "string" } },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ },
93
+ },
94
+ post: {
95
+ responses: {
96
+ "201": { description: "Created" },
97
+ },
98
+ },
99
+ },
100
+ },
101
+ },
102
+ }),
103
+ );
104
+ });
105
+
106
+ Then("routes are registered from the inline spec", async () => {
107
+ const response = await mock.handle("GET", "/hello");
108
+ expect(response.status).toBe(200);
109
+ });
110
+ });
111
+ });