@schmock/openapi 1.2.1 → 1.4.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,183 @@
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/security-validation.feature");
7
+
8
+ const bearerSpec = {
9
+ openapi: "3.0.3",
10
+ info: { title: "Test", version: "1.0.0" },
11
+ components: {
12
+ securitySchemes: {
13
+ bearerAuth: { type: "http", scheme: "bearer" },
14
+ },
15
+ },
16
+ security: [{ bearerAuth: [] }],
17
+ paths: {
18
+ "/items": {
19
+ get: { responses: { "200": { description: "OK" } } },
20
+ },
21
+ },
22
+ };
23
+
24
+ const apiKeySpec = {
25
+ openapi: "3.0.3",
26
+ info: { title: "Test", version: "1.0.0" },
27
+ components: {
28
+ securitySchemes: {
29
+ apiKey: { type: "apiKey", in: "header", name: "x-api-key" },
30
+ },
31
+ },
32
+ security: [{ apiKey: [] }],
33
+ paths: {
34
+ "/items": {
35
+ get: { responses: { "200": { description: "OK" } } },
36
+ },
37
+ },
38
+ };
39
+
40
+ const basicSpec = {
41
+ openapi: "3.0.3",
42
+ info: { title: "Test", version: "1.0.0" },
43
+ components: {
44
+ securitySchemes: {
45
+ basicAuth: { type: "http", scheme: "basic" },
46
+ },
47
+ },
48
+ security: [{ basicAuth: [] }],
49
+ paths: {
50
+ "/items": {
51
+ get: { responses: { "200": { description: "OK" } } },
52
+ },
53
+ },
54
+ };
55
+
56
+ const mixedSpec = {
57
+ openapi: "3.0.3",
58
+ info: { title: "Test", version: "1.0.0" },
59
+ components: {
60
+ securitySchemes: {
61
+ bearerAuth: { type: "http", scheme: "bearer" },
62
+ },
63
+ },
64
+ security: [{ bearerAuth: [] }],
65
+ paths: {
66
+ "/items": {
67
+ get: { responses: { "200": { description: "OK" } } },
68
+ },
69
+ "/health": {
70
+ get: {
71
+ security: [{}],
72
+ responses: { "200": { description: "OK" } },
73
+ },
74
+ },
75
+ },
76
+ };
77
+
78
+ describeFeature(feature, ({ Scenario }) => {
79
+ let mock: Schmock.CallableMockInstance;
80
+ let response: Schmock.Response;
81
+
82
+ Scenario("Missing Bearer token returns 401", ({ Given, When, Then, And }) => {
83
+ Given("a mock with a spec requiring Bearer auth", async () => {
84
+ mock = schmock({ state: {} });
85
+ mock.pipe(await openapi({ spec: bearerSpec, security: true }));
86
+ });
87
+
88
+ When("I request without an Authorization header", async () => {
89
+ response = await mock.handle("GET", "/items", { headers: {} });
90
+ });
91
+
92
+ Then("the response status is 401", () => {
93
+ expect(response.status).toBe(401);
94
+ });
95
+
96
+ And('the response has a WWW-Authenticate header with "Bearer"', () => {
97
+ expect(response.headers["www-authenticate"]).toContain("Bearer");
98
+ });
99
+ });
100
+
101
+ Scenario("Valid Bearer token returns 200", ({ Given, When, Then }) => {
102
+ Given("a mock with a spec requiring Bearer auth", async () => {
103
+ mock = schmock({ state: {} });
104
+ mock.pipe(await openapi({ spec: bearerSpec, security: true }));
105
+ });
106
+
107
+ When('I request with Authorization header "Bearer my-token"', async () => {
108
+ response = await mock.handle("GET", "/items", {
109
+ headers: { authorization: "Bearer my-token" },
110
+ });
111
+ });
112
+
113
+ Then("the response status is 200", () => {
114
+ expect(response.status).toBe(200);
115
+ });
116
+ });
117
+
118
+ Scenario("API key in header is validated", ({ Given, When, Then }) => {
119
+ Given("a mock with a spec requiring an API key header", async () => {
120
+ mock = schmock({ state: {} });
121
+ mock.pipe(await openapi({ spec: apiKeySpec, security: true }));
122
+ });
123
+
124
+ When("I request without the API key header", async () => {
125
+ response = await mock.handle("GET", "/items", { headers: {} });
126
+ });
127
+
128
+ Then("the response status is 401", () => {
129
+ expect(response.status).toBe(401);
130
+ });
131
+ });
132
+
133
+ Scenario("Valid API key passes through", ({ Given, When, Then }) => {
134
+ Given("a mock with a spec requiring an API key header", async () => {
135
+ mock = schmock({ state: {} });
136
+ mock.pipe(await openapi({ spec: apiKeySpec, security: true }));
137
+ });
138
+
139
+ When("I request with the API key header present", async () => {
140
+ response = await mock.handle("GET", "/items", {
141
+ headers: { "x-api-key": "my-key-123" },
142
+ });
143
+ });
144
+
145
+ Then("the response status is 200", () => {
146
+ expect(response.status).toBe(200);
147
+ });
148
+ });
149
+
150
+ Scenario("Basic auth is validated", ({ Given, When, Then, And }) => {
151
+ Given("a mock with a spec requiring Basic auth", async () => {
152
+ mock = schmock({ state: {} });
153
+ mock.pipe(await openapi({ spec: basicSpec, security: true }));
154
+ });
155
+
156
+ When("I request without an Authorization header", async () => {
157
+ response = await mock.handle("GET", "/items", { headers: {} });
158
+ });
159
+
160
+ Then("the response status is 401", () => {
161
+ expect(response.status).toBe(401);
162
+ });
163
+
164
+ And('the response has a WWW-Authenticate header with "Basic"', () => {
165
+ expect(response.headers["www-authenticate"]).toContain("Basic");
166
+ });
167
+ });
168
+
169
+ Scenario("Public endpoint skips validation", ({ Given, When, Then }) => {
170
+ Given("a mock with a spec where one endpoint is public", async () => {
171
+ mock = schmock({ state: {} });
172
+ mock.pipe(await openapi({ spec: mixedSpec, security: true }));
173
+ });
174
+
175
+ When("I request the public endpoint without auth", async () => {
176
+ response = await mock.handle("GET", "/health", { headers: {} });
177
+ });
178
+
179
+ Then("the response status is 200", () => {
180
+ expect(response.status).toBe(200);
181
+ });
182
+ });
183
+ });
@@ -1,4 +1,4 @@
1
- /// <reference path="../../../types/schmock.d.ts" />
1
+ /// <reference path="../../core/schmock.d.ts" />
2
2
 
3
3
  import { readFileSync } from "node:fs";
4
4
  import { resolve } from "node:path";
@@ -197,10 +197,11 @@ describe("stress: integration — train-travel.yaml", () => {
197
197
  const trips = await mock.handle("GET", "/trips");
198
198
  expect(trips.status).toBe(200);
199
199
 
200
- // Bookings: CRUD resource
200
+ // Bookings: CRUD resource (wrapped list — allOf with data array)
201
201
  const emptyList = await mock.handle("GET", "/bookings");
202
202
  expect(emptyList.status).toBe(200);
203
- expect(emptyList.body).toEqual([]);
203
+ const emptyBody = emptyList.body as Record<string, unknown>;
204
+ expect(emptyBody.data).toEqual([]);
204
205
  });
205
206
 
206
207
  it("bookings CRUD lifecycle", async () => {
@@ -226,10 +227,11 @@ describe("stress: integration — train-travel.yaml", () => {
226
227
  expect(read.status).toBe(200);
227
228
  expect(read.body).toMatchObject({ passenger_name: "John Doe" });
228
229
 
229
- // List
230
+ // List (wrapped)
230
231
  const list = await mock.handle("GET", "/bookings");
231
232
  expect(list.status).toBe(200);
232
- expect(list.body).toHaveLength(1);
233
+ const listBody = list.body as Record<string, unknown>;
234
+ expect(listBody.data).toHaveLength(1);
233
235
 
234
236
  // Delete
235
237
  const deleted = await mock.handle("DELETE", "/bookings/1");
@@ -270,7 +272,8 @@ describe("stress: integration — train-travel.yaml", () => {
270
272
  }
271
273
 
272
274
  const list = await mock.handle("GET", "/bookings");
273
- expect(list.body).toHaveLength(5);
275
+ const listBody = list.body as Record<string, unknown>;
276
+ expect(listBody.data).toHaveLength(5);
274
277
  });
275
278
  });
276
279
 
@@ -1149,10 +1152,11 @@ describe("stress: scalar-galaxy.yaml — BREAD operations", () => {
1149
1152
  }),
1150
1153
  );
1151
1154
 
1152
- // BROWSE — list all 8 planets
1155
+ // BROWSE — list all 8 planets (wrapped: { data: [...], meta: {...} })
1153
1156
  const allPlanets = await mock.handle("GET", "/planets");
1154
1157
  expect(allPlanets.status).toBe(200);
1155
- expect(allPlanets.body).toHaveLength(8);
1158
+ const allPlanetsBody = allPlanets.body as Record<string, unknown>;
1159
+ expect(allPlanetsBody.data).toHaveLength(8);
1156
1160
 
1157
1161
  // READ — each planet is coherent
1158
1162
  for (let i = 1; i <= 8; i++) {
@@ -1186,9 +1190,9 @@ describe("stress: scalar-galaxy.yaml — BREAD operations", () => {
1186
1190
  expect(newPlanet.name).toBe("Kepler-442b");
1187
1191
  expect(newPlanet.planetId).toBe(9); // auto-incremented past seed max (8)
1188
1192
 
1189
- // BROWSE after ADD — 9 planets
1193
+ // BROWSE after ADD — 9 planets (wrapped)
1190
1194
  const afterAdd = await mock.handle("GET", "/planets");
1191
- expect(afterAdd.body).toHaveLength(9);
1195
+ expect((afterAdd.body as Record<string, unknown>).data).toHaveLength(9);
1192
1196
 
1193
1197
  // EDIT — update Mars terraforming progress
1194
1198
  const edited = await mock.handle("PUT", "/planets/4", {
@@ -1203,9 +1207,9 @@ describe("stress: scalar-galaxy.yaml — BREAD operations", () => {
1203
1207
  const deleted = await mock.handle("DELETE", "/planets/9");
1204
1208
  expect(deleted.status).toBe(204);
1205
1209
 
1206
- // BROWSE after DELETE — back to 8
1210
+ // BROWSE after DELETE — back to 8 (wrapped)
1207
1211
  const afterDelete = await mock.handle("GET", "/planets");
1208
- expect(afterDelete.body).toHaveLength(8);
1212
+ expect((afterDelete.body as Record<string, unknown>).data).toHaveLength(8);
1209
1213
 
1210
1214
  // READ deleted — 404
1211
1215
  const gone = await mock.handle("GET", "/planets/9");
@@ -1495,13 +1499,16 @@ describe("stress: lifecycle and multi-instance flows", () => {
1495
1499
  }),
1496
1500
  );
1497
1501
 
1498
- expect((await mock.handle("GET", "/planets")).body).toHaveLength(8);
1502
+ expect(
1503
+ ((await mock.handle("GET", "/planets")).body as Record<string, unknown>)
1504
+ .data,
1505
+ ).toHaveLength(8);
1499
1506
 
1500
1507
  mock.resetState();
1501
1508
 
1502
1509
  // After reset, collection is empty — seeder runs again on next request
1503
1510
  const list = await mock.handle("GET", "/planets");
1504
- expect(list.body).toHaveLength(8); // re-seeded on access
1511
+ expect((list.body as Record<string, unknown>).data).toHaveLength(8); // re-seeded on access
1505
1512
 
1506
1513
  // Can still create new items
1507
1514
  const created = await mock.handle("POST", "/planets", {
@@ -1572,7 +1579,8 @@ describe("stress: lifecycle and multi-instance flows", () => {
1572
1579
  await mock.handle("POST", "/planets", { body: { name: "Exo-2" } });
1573
1580
 
1574
1581
  const list = await mock.handle("GET", "/planets");
1575
- const items = list.body as Record<string, unknown>[];
1582
+ const listBody = list.body as Record<string, unknown>;
1583
+ const items = listBody.data as Record<string, unknown>[];
1576
1584
  expect(items).toHaveLength(4);
1577
1585
 
1578
1586
  // Seed items at IDs 1-2, runtime items at IDs 3-4
@@ -1595,10 +1603,16 @@ describe("stress: lifecycle and multi-instance flows", () => {
1595
1603
  await mock.handle("POST", "/pets", { body: { name: "Buddy" } });
1596
1604
  expect((await mock.handle("GET", "/pets")).body).toHaveLength(1);
1597
1605
 
1598
- // Galaxy CRUD — completely independent
1599
- expect((await mock.handle("GET", "/planets")).body).toHaveLength(8);
1606
+ // Galaxy CRUD — completely independent (wrapped format)
1607
+ expect(
1608
+ ((await mock.handle("GET", "/planets")).body as Record<string, unknown>)
1609
+ .data,
1610
+ ).toHaveLength(8);
1600
1611
  await mock.handle("POST", "/planets", { body: { name: "Nibiru" } });
1601
- expect((await mock.handle("GET", "/planets")).body).toHaveLength(9);
1612
+ expect(
1613
+ ((await mock.handle("GET", "/planets")).body as Record<string, unknown>)
1614
+ .data,
1615
+ ).toHaveLength(9);
1602
1616
 
1603
1617
  // Petstore still has 1
1604
1618
  expect((await mock.handle("GET", "/pets")).body).toHaveLength(1);
@@ -1858,9 +1872,9 @@ describe("stress: realistic E2E flows", () => {
1858
1872
  const me = await mock.handle("GET", "/me");
1859
1873
  expect(me.status).toBe(200);
1860
1874
 
1861
- // 6. List planets
1875
+ // 6. List planets (wrapped)
1862
1876
  const list = await mock.handle("GET", "/planets");
1863
- expect(list.body).toHaveLength(1);
1877
+ expect((list.body as Record<string, unknown>).data).toHaveLength(1);
1864
1878
  });
1865
1879
 
1866
1880
  it("galaxy: populate solar system then explore", async () => {
@@ -1885,9 +1899,9 @@ describe("stress: realistic E2E flows", () => {
1885
1899
  expect(res.status).toBe(201);
1886
1900
  }
1887
1901
 
1888
- // Verify count
1902
+ // Verify count (wrapped)
1889
1903
  const list = await mock.handle("GET", "/planets");
1890
- expect(list.body).toHaveLength(8);
1904
+ expect((list.body as Record<string, unknown>).data).toHaveLength(8);
1891
1905
 
1892
1906
  // Read each by ID
1893
1907
  for (let i = 1; i <= 8; i++) {
@@ -2609,10 +2623,11 @@ describe("stress: stripe spec — CRUD lifecycle with fixtures", () => {
2609
2623
  custFixture.email,
2610
2624
  );
2611
2625
 
2612
- // List
2626
+ // List (wrapped: { data: [...], has_more, object, url })
2613
2627
  const list = await mock.handle("GET", "/v1/customers");
2614
2628
  expect(list.status).toBe(200);
2615
- expect(list.body).toHaveLength(1);
2629
+ const listBody = list.body as Record<string, unknown>;
2630
+ expect(listBody.data).toHaveLength(1);
2616
2631
 
2617
2632
  // Delete
2618
2633
  const deleted = await mock.handle("DELETE", "/v1/customers/1");
@@ -2641,9 +2656,9 @@ describe("stress: stripe spec — CRUD lifecycle with fixtures", () => {
2641
2656
  expect(read.status).toBe(200);
2642
2657
  expect((read.body as Record<string, unknown>).name).toBe("Premium Plan");
2643
2658
 
2644
- // List
2659
+ // List (wrapped)
2645
2660
  const list = await mock.handle("GET", "/v1/products");
2646
- expect(list.body).toHaveLength(1);
2661
+ expect((list.body as Record<string, unknown>).data).toHaveLength(1);
2647
2662
 
2648
2663
  // Delete
2649
2664
  const del = await mock.handle("DELETE", "/v1/products/1");
@@ -2695,15 +2710,50 @@ describe("stress: stripe spec — CRUD lifecycle with fixtures", () => {
2695
2710
  body: { percent_off: 25 },
2696
2711
  });
2697
2712
 
2698
- // Each resource is independent
2699
- expect((await mock.handle("GET", "/v1/customers")).body).toHaveLength(2);
2700
- expect((await mock.handle("GET", "/v1/products")).body).toHaveLength(1);
2701
- expect((await mock.handle("GET", "/v1/coupons")).body).toHaveLength(1);
2713
+ // Each resource is independent (all wrapped)
2714
+ expect(
2715
+ (
2716
+ (await mock.handle("GET", "/v1/customers")).body as Record<
2717
+ string,
2718
+ unknown
2719
+ >
2720
+ ).data,
2721
+ ).toHaveLength(2);
2722
+ expect(
2723
+ (
2724
+ (await mock.handle("GET", "/v1/products")).body as Record<
2725
+ string,
2726
+ unknown
2727
+ >
2728
+ ).data,
2729
+ ).toHaveLength(1);
2730
+ expect(
2731
+ (
2732
+ (await mock.handle("GET", "/v1/coupons")).body as Record<
2733
+ string,
2734
+ unknown
2735
+ >
2736
+ ).data,
2737
+ ).toHaveLength(1);
2702
2738
 
2703
2739
  // Delete a customer doesn't affect products
2704
2740
  await mock.handle("DELETE", "/v1/customers/1");
2705
- expect((await mock.handle("GET", "/v1/customers")).body).toHaveLength(1);
2706
- expect((await mock.handle("GET", "/v1/products")).body).toHaveLength(1);
2741
+ expect(
2742
+ (
2743
+ (await mock.handle("GET", "/v1/customers")).body as Record<
2744
+ string,
2745
+ unknown
2746
+ >
2747
+ ).data,
2748
+ ).toHaveLength(1);
2749
+ expect(
2750
+ (
2751
+ (await mock.handle("GET", "/v1/products")).body as Record<
2752
+ string,
2753
+ unknown
2754
+ >
2755
+ ).data,
2756
+ ).toHaveLength(1);
2707
2757
  }, 120_000);
2708
2758
 
2709
2759
  it("non-CRUD endpoints respond alongside CRUD", async () => {
@@ -2728,12 +2778,19 @@ describe("stress: stripe spec — CRUD lifecycle with fixtures", () => {
2728
2778
  await mock.handle("POST", "/v1/customers", {
2729
2779
  body: { email: "temp@stripe.com" },
2730
2780
  });
2731
- expect((await mock.handle("GET", "/v1/customers")).body).toHaveLength(1);
2781
+ expect(
2782
+ (
2783
+ (await mock.handle("GET", "/v1/customers")).body as Record<
2784
+ string,
2785
+ unknown
2786
+ >
2787
+ ).data,
2788
+ ).toHaveLength(1);
2732
2789
 
2733
2790
  mock.resetState();
2734
2791
 
2735
2792
  const list = await mock.handle("GET", "/v1/customers");
2736
- expect(list.body).toEqual([]);
2793
+ expect((list.body as Record<string, unknown>).data).toEqual([]);
2737
2794
  }, 120_000);
2738
2795
  });
2739
2796
 
@@ -2770,7 +2827,7 @@ describe("stress: stripe spec — the confused Stripe developer", () => {
2770
2827
  }
2771
2828
 
2772
2829
  const list = await mock.handle("GET", "/v1/customers");
2773
- expect(list.body).toHaveLength(50);
2830
+ expect((list.body as Record<string, unknown>).data).toHaveLength(50);
2774
2831
  }, 120_000);
2775
2832
 
2776
2833
  it("interleaved operations across Stripe resources", async () => {