@schmock/openapi 1.2.1 → 1.7.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.
@@ -0,0 +1,427 @@
1
+ import { resolve } from "node:path";
2
+ import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
3
+ import { schmock } from "@schmock/core";
4
+ import { expect, vi } from "vitest";
5
+ import { openapi } from "../plugin";
6
+
7
+ const feature = await loadFeature("../../features/openapi-compliance.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 debugOutput: string[];
14
+
15
+ Scenario(
16
+ "Wrapped list response with allOf composition",
17
+ ({ Given, When, Then, And }) => {
18
+ Given("a mock with the Scalar Galaxy spec loaded", async () => {
19
+ mock = schmock({ state: {} });
20
+ mock.pipe(
21
+ await openapi({
22
+ spec: `${fixturesDir}/scalar-galaxy.yaml`,
23
+ seed: {
24
+ planets: [{ planetId: 1, name: "Earth", type: "terrestrial" }],
25
+ },
26
+ }),
27
+ );
28
+ });
29
+
30
+ When("I list all planets", async () => {
31
+ response = await mock.handle("GET", "/planets");
32
+ });
33
+
34
+ Then("the response status is 200", () => {
35
+ expect(response.status).toBe(200);
36
+ });
37
+
38
+ And('the list body has a "data" property with an array', () => {
39
+ const body = response.body as Record<string, unknown>;
40
+ expect(body.data).toBeDefined();
41
+ expect(Array.isArray(body.data)).toBe(true);
42
+ });
43
+
44
+ And('the list body has a "meta" property', () => {
45
+ const body = response.body as Record<string, unknown>;
46
+ expect(body.meta).toBeDefined();
47
+ });
48
+ },
49
+ );
50
+
51
+ Scenario(
52
+ "Wrapped list response with inline object",
53
+ ({ Given, When, Then, And }) => {
54
+ Given("a mock with a Stripe-style spec loaded", async () => {
55
+ mock = schmock({ state: {} });
56
+ mock.pipe(
57
+ await openapi({
58
+ spec: {
59
+ openapi: "3.0.3",
60
+ info: { title: "StripeStyle", version: "1.0.0" },
61
+ paths: {
62
+ "/v1/customers": {
63
+ get: {
64
+ responses: {
65
+ "200": {
66
+ description: "List",
67
+ content: {
68
+ "application/json": {
69
+ schema: {
70
+ type: "object",
71
+ properties: {
72
+ data: {
73
+ type: "array",
74
+ items: {
75
+ type: "object",
76
+ properties: {
77
+ id: { type: "integer" },
78
+ email: { type: "string" },
79
+ },
80
+ },
81
+ },
82
+ has_more: { type: "boolean" },
83
+ object: {
84
+ type: "string",
85
+ enum: ["list"],
86
+ },
87
+ url: { type: "string" },
88
+ },
89
+ required: ["data", "has_more", "object", "url"],
90
+ },
91
+ },
92
+ },
93
+ },
94
+ },
95
+ },
96
+ post: {
97
+ responses: { "201": { description: "Created" } },
98
+ },
99
+ },
100
+ "/v1/customers/{customer}": {
101
+ get: {
102
+ parameters: [
103
+ {
104
+ name: "customer",
105
+ in: "path",
106
+ required: true,
107
+ },
108
+ ],
109
+ responses: { "200": { description: "Customer" } },
110
+ },
111
+ delete: {
112
+ parameters: [
113
+ {
114
+ name: "customer",
115
+ in: "path",
116
+ required: true,
117
+ },
118
+ ],
119
+ responses: { "204": { description: "Deleted" } },
120
+ },
121
+ },
122
+ },
123
+ },
124
+ seed: {
125
+ customers: [{ customer: 1, email: "alice@test.com" }],
126
+ },
127
+ }),
128
+ );
129
+ });
130
+
131
+ When("I list all customers", async () => {
132
+ response = await mock.handle("GET", "/v1/customers");
133
+ });
134
+
135
+ Then("the response status is 200", () => {
136
+ expect(response.status).toBe(200);
137
+ });
138
+
139
+ And('the list body has a "data" property with an array', () => {
140
+ const body = response.body as Record<string, unknown>;
141
+ expect(body.data).toBeDefined();
142
+ expect(Array.isArray(body.data)).toBe(true);
143
+ });
144
+
145
+ And('the list body has an "object" property equal to "list"', () => {
146
+ const body = response.body as Record<string, unknown>;
147
+ expect(body.object).toBe("list");
148
+ });
149
+ },
150
+ );
151
+
152
+ Scenario(
153
+ "Flat list response for plain array spec",
154
+ ({ Given, When, Then, And }) => {
155
+ Given("a mock with the Petstore spec loaded", async () => {
156
+ mock = schmock({ state: {} });
157
+ mock.pipe(
158
+ await openapi({
159
+ spec: `${fixturesDir}/petstore-swagger2.json`,
160
+ seed: { pets: [{ petId: 1, name: "Buddy" }] },
161
+ }),
162
+ );
163
+ });
164
+
165
+ When("I list all pets", async () => {
166
+ response = await mock.handle("GET", "/pets");
167
+ });
168
+
169
+ Then("the response status is 200", () => {
170
+ expect(response.status).toBe(200);
171
+ });
172
+
173
+ And("the list body is a flat array", () => {
174
+ expect(Array.isArray(response.body)).toBe(true);
175
+ });
176
+ },
177
+ );
178
+
179
+ Scenario(
180
+ "Spec-defined error response schema",
181
+ ({ Given, When, Then, And }) => {
182
+ Given("a mock with the Scalar Galaxy spec loaded", async () => {
183
+ mock = schmock({ state: {} });
184
+ mock.pipe(await openapi({ spec: `${fixturesDir}/scalar-galaxy.yaml` }));
185
+ });
186
+
187
+ When("I read planet with id 999", async () => {
188
+ response = await mock.handle("GET", "/planets/999");
189
+ });
190
+
191
+ Then("the response status is 404", () => {
192
+ expect(response.status).toBe(404);
193
+ });
194
+
195
+ And('the error body has a "title" property', () => {
196
+ const body = response.body as Record<string, unknown>;
197
+ expect(body.title).toBeDefined();
198
+ });
199
+
200
+ And('the error body has a "status" property', () => {
201
+ const body = response.body as Record<string, unknown>;
202
+ expect(body.status).toBeDefined();
203
+ });
204
+ },
205
+ );
206
+
207
+ Scenario(
208
+ "Default error format when no error schema defined",
209
+ ({ Given, When, Then, And }) => {
210
+ Given("a mock with the Petstore spec loaded", async () => {
211
+ mock = schmock({ state: {} });
212
+ mock.pipe(
213
+ await openapi({
214
+ spec: `${fixturesDir}/petstore-swagger2.json`,
215
+ }),
216
+ );
217
+ });
218
+
219
+ When("I read pet with id 999", async () => {
220
+ response = await mock.handle("GET", "/pets/999");
221
+ });
222
+
223
+ Then("the response status is 404", () => {
224
+ expect(response.status).toBe(404);
225
+ });
226
+
227
+ And('the error body has property "error" equal to "Not found"', () => {
228
+ const body = response.body as Record<string, unknown>;
229
+ expect(body.error).toBe("Not found");
230
+ });
231
+
232
+ And('the error body has property "code" equal to "NOT_FOUND"', () => {
233
+ const body = response.body as Record<string, unknown>;
234
+ expect(body.code).toBe("NOT_FOUND");
235
+ });
236
+ },
237
+ );
238
+
239
+ Scenario(
240
+ "Response headers from spec definitions",
241
+ ({ Given, When, Then, And }) => {
242
+ Given("a mock with the Scalar Galaxy spec and seed data", async () => {
243
+ mock = schmock({ state: {} });
244
+ mock.pipe(
245
+ await openapi({
246
+ spec: `${fixturesDir}/scalar-galaxy.yaml`,
247
+ seed: {
248
+ planets: [{ planetId: 1, name: "Earth", type: "terrestrial" }],
249
+ },
250
+ }),
251
+ );
252
+ });
253
+
254
+ When("I list all planets", async () => {
255
+ response = await mock.handle("GET", "/planets");
256
+ });
257
+
258
+ Then("the response status is 200", () => {
259
+ expect(response.status).toBe(200);
260
+ });
261
+
262
+ And('the response has header "X-Request-ID"', () => {
263
+ expect(response.headers["X-Request-ID"]).toBeDefined();
264
+ });
265
+
266
+ And('the response has header "X-Pagination-Total"', () => {
267
+ expect(response.headers["X-Pagination-Total"]).toBeDefined();
268
+ });
269
+ },
270
+ );
271
+
272
+ Scenario(
273
+ "Manual override forces wrapping on a flat-array spec",
274
+ ({ Given, When, Then, And }) => {
275
+ Given(
276
+ 'a mock with the Petstore spec and listWrapProperty "items" override',
277
+ async () => {
278
+ mock = schmock({ state: {} });
279
+ mock.pipe(
280
+ await openapi({
281
+ spec: `${fixturesDir}/petstore-swagger2.json`,
282
+ seed: { pets: [{ petId: 1, name: "Buddy" }] },
283
+ resources: {
284
+ pets: { listWrapProperty: "items" },
285
+ },
286
+ }),
287
+ );
288
+ },
289
+ );
290
+
291
+ When("I list all pets", async () => {
292
+ response = await mock.handle("GET", "/pets");
293
+ });
294
+
295
+ Then("the response status is 200", () => {
296
+ expect(response.status).toBe(200);
297
+ });
298
+
299
+ And('the list body has a "items" property with an array', () => {
300
+ const body = response.body as Record<string, unknown>;
301
+ expect(body.items).toBeDefined();
302
+ expect(Array.isArray(body.items)).toBe(true);
303
+ const items = body.items as unknown[];
304
+ expect(items).toHaveLength(1);
305
+ });
306
+ },
307
+ );
308
+
309
+ Scenario(
310
+ "Manual override forces flat on a wrapped spec",
311
+ ({ Given, When, Then, And }) => {
312
+ Given(
313
+ "a mock with the Scalar Galaxy spec and listFlat override",
314
+ async () => {
315
+ mock = schmock({ state: {} });
316
+ mock.pipe(
317
+ await openapi({
318
+ spec: `${fixturesDir}/scalar-galaxy.yaml`,
319
+ seed: {
320
+ planets: [{ planetId: 1, name: "Earth", type: "terrestrial" }],
321
+ },
322
+ resources: {
323
+ planets: { listFlat: true },
324
+ },
325
+ }),
326
+ );
327
+ },
328
+ );
329
+
330
+ When("I list all planets", async () => {
331
+ response = await mock.handle("GET", "/planets");
332
+ });
333
+
334
+ Then("the response status is 200", () => {
335
+ expect(response.status).toBe(200);
336
+ });
337
+
338
+ And("the list body is a flat array", () => {
339
+ expect(Array.isArray(response.body)).toBe(true);
340
+ });
341
+ },
342
+ );
343
+
344
+ Scenario(
345
+ "Manual errorSchema override replaces auto-detected error format",
346
+ ({ Given, When, Then, And }) => {
347
+ Given(
348
+ "a mock with the Petstore spec and custom error schema override",
349
+ async () => {
350
+ mock = schmock({ state: {} });
351
+ mock.pipe(
352
+ await openapi({
353
+ spec: `${fixturesDir}/petstore-swagger2.json`,
354
+ resources: {
355
+ pets: {
356
+ errorSchema: {
357
+ type: "object",
358
+ properties: {
359
+ message: {
360
+ type: "string",
361
+ default: "Resource not found",
362
+ },
363
+ statusCode: {
364
+ type: "integer",
365
+ default: 404,
366
+ },
367
+ },
368
+ required: ["message", "statusCode"],
369
+ },
370
+ },
371
+ },
372
+ }),
373
+ );
374
+ },
375
+ );
376
+
377
+ When("I read pet with id 999", async () => {
378
+ response = await mock.handle("GET", "/pets/999");
379
+ });
380
+
381
+ Then("the response status is 404", () => {
382
+ expect(response.status).toBe(404);
383
+ });
384
+
385
+ And('the error body has a "message" property', () => {
386
+ const body = response.body as Record<string, unknown>;
387
+ expect(body.message).toBeDefined();
388
+ });
389
+
390
+ And('the error body has a "statusCode" property', () => {
391
+ const body = response.body as Record<string, unknown>;
392
+ expect(body.statusCode).toBeDefined();
393
+ });
394
+ },
395
+ );
396
+
397
+ Scenario("Debug mode logs detection results", ({ Given, Then, And }) => {
398
+ Given("a mock with the Petstore spec and debug enabled", async () => {
399
+ debugOutput = [];
400
+ const spy = vi
401
+ .spyOn(console, "log")
402
+ .mockImplementation((...args: unknown[]) => {
403
+ debugOutput.push(args.map(String).join(" "));
404
+ });
405
+
406
+ mock = schmock({ state: {} });
407
+ mock.pipe(
408
+ await openapi({
409
+ spec: `${fixturesDir}/petstore-swagger2.json`,
410
+ debug: true,
411
+ }),
412
+ );
413
+
414
+ spy.mockRestore();
415
+ });
416
+
417
+ Then('the debug output contains "CRUD resources"', () => {
418
+ const joined = debugOutput.join("\n");
419
+ expect(joined).toContain("CRUD resource");
420
+ });
421
+
422
+ And('the debug output contains "pets"', () => {
423
+ const joined = debugOutput.join("\n");
424
+ expect(joined).toContain("pets");
425
+ });
426
+ });
427
+ });
@@ -0,0 +1,140 @@
1
+ import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
+ import { schmock } from "@schmock/core";
3
+ import { expect } from "vitest";
4
+ import { openapi } from "../plugin";
5
+
6
+ const feature = await loadFeature("../../features/prefer-header.feature");
7
+
8
+ const specWith404 = {
9
+ openapi: "3.0.3",
10
+ info: { title: "Test", version: "1.0.0" },
11
+ paths: {
12
+ "/items": {
13
+ get: {
14
+ responses: {
15
+ "200": {
16
+ description: "OK",
17
+ content: {
18
+ "application/json": {
19
+ schema: {
20
+ type: "object",
21
+ properties: {
22
+ id: { type: "integer" },
23
+ name: { type: "string" },
24
+ },
25
+ },
26
+ },
27
+ },
28
+ },
29
+ "404": {
30
+ description: "Not found",
31
+ content: {
32
+ "application/json": {
33
+ schema: {
34
+ type: "object",
35
+ properties: {
36
+ error: { type: "string", default: "Not found" },
37
+ },
38
+ },
39
+ },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ };
47
+
48
+ const specWithExamples = {
49
+ openapi: "3.0.3",
50
+ info: { title: "Test", version: "1.0.0" },
51
+ paths: {
52
+ "/pets": {
53
+ get: {
54
+ responses: {
55
+ "200": {
56
+ description: "OK",
57
+ content: {
58
+ "application/json": {
59
+ schema: {
60
+ type: "object",
61
+ properties: {
62
+ name: { type: "string" },
63
+ type: { type: "string" },
64
+ },
65
+ },
66
+ examples: {
67
+ dog: { value: { name: "Buddy", type: "dog" } },
68
+ cat: { value: { name: "Whiskers", type: "cat" } },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ },
74
+ },
75
+ },
76
+ },
77
+ };
78
+
79
+ describeFeature(feature, ({ Scenario }) => {
80
+ let mock: Schmock.CallableMockInstance;
81
+ let response: Schmock.Response;
82
+
83
+ Scenario("Prefer code returns specific status code", ({ Given, When, Then }) => {
84
+ Given("a mock with an OpenAPI spec with 200 and 404 responses", async () => {
85
+ mock = schmock({ state: {} });
86
+ mock.pipe(await openapi({ spec: specWith404 }));
87
+ });
88
+
89
+ When('I request with Prefer header "code=404"', async () => {
90
+ response = await mock.handle("GET", "/items", {
91
+ headers: { prefer: "code=404" },
92
+ });
93
+ });
94
+
95
+ Then("the response status is 404", () => {
96
+ expect(response.status).toBe(404);
97
+ });
98
+ });
99
+
100
+ Scenario("Prefer example returns named example", ({ Given, When, Then }) => {
101
+ Given("a mock with an OpenAPI spec with named examples", async () => {
102
+ mock = schmock({ state: {} });
103
+ mock.pipe(await openapi({ spec: specWithExamples }));
104
+ });
105
+
106
+ When('I request with Prefer header "example=dog"', async () => {
107
+ response = await mock.handle("GET", "/pets", {
108
+ headers: { prefer: "example=dog" },
109
+ });
110
+ });
111
+
112
+ Then('the response body name is "Buddy"', () => {
113
+ const body = response.body as Record<string, unknown>;
114
+ expect(body.name).toBe("Buddy");
115
+ });
116
+ });
117
+
118
+ Scenario("Prefer dynamic regenerates from schema", ({ Given, When, Then, And }) => {
119
+ Given("a mock with an OpenAPI spec with a response schema", async () => {
120
+ mock = schmock({ state: {} });
121
+ mock.pipe(await openapi({ spec: specWith404 }));
122
+ });
123
+
124
+ When('I request with Prefer header "dynamic=true"', async () => {
125
+ response = await mock.handle("GET", "/items", {
126
+ headers: { prefer: "dynamic=true" },
127
+ });
128
+ });
129
+
130
+ Then('the response body has a "name" property', () => {
131
+ const body = response.body as Record<string, unknown>;
132
+ expect(body.name).toBeDefined();
133
+ });
134
+
135
+ And('the response body has an "id" property', () => {
136
+ const body = response.body as Record<string, unknown>;
137
+ expect(body.id).toBeDefined();
138
+ });
139
+ });
140
+ });