@schmock/core 1.0.2 → 1.0.3

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.
package/dist/builder.d.ts CHANGED
@@ -42,8 +42,8 @@ export declare class CallableMockInstance {
42
42
  private runPluginPipeline;
43
43
  /**
44
44
  * Find a route that matches the given method and path
45
- * Uses two-pass matching: exact routes first, then parameterized routes
46
- * Searches in reverse order to prefer most recently defined routes
45
+ * Uses two-pass matching: static routes first, then parameterized routes
46
+ * Matches routes in registration order (first registered wins)
47
47
  * @param method - HTTP method to match
48
48
  * @param path - Request path to match
49
49
  * @returns Matched compiled route or undefined if no match
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,SAAS,EAET,YAAY,EACZ,UAAU,EACV,MAAM,EAGN,cAAc,EACd,QAAQ,EACR,WAAW,EACX,QAAQ,EACT,MAAM,YAAY,CAAC;AA4CpB;;;;GAIG;AACH,qBAAa,oBAAoB;IAKnB,OAAO,CAAC,YAAY;IAJhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAc;gBAER,YAAY,GAAE,YAAiB;IAanD,WAAW,CACT,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,WAAW,GAClB,IAAI;IA4DP,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAepB,MAAM,CACV,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,QAAQ,CAAC;IAiLpB;;;;OAIG;YACW,UAAU;IAcxB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IA0DrB;;;;;;;;;;OAUG;YACW,iBAAiB;IAmF/B;;;;;;;;OAQG;IACH,OAAO,CAAC,SAAS;IA+BjB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;CActB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,SAAS,EAET,YAAY,EACZ,UAAU,EACV,MAAM,EAGN,cAAc,EACd,QAAQ,EACR,WAAW,EACX,QAAQ,EACT,MAAM,YAAY,CAAC;AA4CpB;;;;GAIG;AACH,qBAAa,oBAAoB;IAKnB,OAAO,CAAC,YAAY;IAJhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAc;gBAER,YAAY,GAAE,YAAiB;IAanD,WAAW,CACT,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,WAAW,GAClB,IAAI;IA4DP,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAepB,MAAM,CACV,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,QAAQ,CAAC;IAiLpB;;;;OAIG;YACW,UAAU;IAcxB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IA0DrB;;;;;;;;;;OAUG;YACW,iBAAiB;IAmF/B;;;;;;;;OAQG;IACH,OAAO,CAAC,SAAS;IA6BjB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;CActB"}
package/dist/builder.js CHANGED
@@ -383,17 +383,16 @@ export class CallableMockInstance {
383
383
  }
384
384
  /**
385
385
  * Find a route that matches the given method and path
386
- * Uses two-pass matching: exact routes first, then parameterized routes
387
- * Searches in reverse order to prefer most recently defined routes
386
+ * Uses two-pass matching: static routes first, then parameterized routes
387
+ * Matches routes in registration order (first registered wins)
388
388
  * @param method - HTTP method to match
389
389
  * @param path - Request path to match
390
390
  * @returns Matched compiled route or undefined if no match
391
391
  * @private
392
392
  */
393
393
  findRoute(method, path) {
394
- // First pass: Look for exact matches (routes without parameters)
395
- for (let i = this.routes.length - 1; i >= 0; i--) {
396
- const route = this.routes[i];
394
+ // First pass: Look for static routes (routes without parameters)
395
+ for (const route of this.routes) {
397
396
  if (route.method === method &&
398
397
  route.params.length === 0 &&
399
398
  route.pattern.test(path)) {
@@ -401,8 +400,7 @@ export class CallableMockInstance {
401
400
  }
402
401
  }
403
402
  // Second pass: Look for parameterized routes
404
- for (let i = this.routes.length - 1; i >= 0; i--) {
405
- const route = this.routes[i];
403
+ for (const route of this.routes) {
406
404
  if (route.method === method &&
407
405
  route.params.length > 0 &&
408
406
  route.pattern.test(path)) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@schmock/core",
3
3
  "description": "Core functionality for Schmock",
4
- "version": "1.0.2",
4
+ "version": "1.0.3",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -32,7 +32,7 @@
32
32
  "license": "MIT",
33
33
  "devDependencies": {
34
34
  "@amiceli/vitest-cucumber": "^6.1.0",
35
- "@types/node": "^24.9.1",
35
+ "@types/node": "^25.0.0",
36
36
  "@vitest/ui": "^4.0.15",
37
37
  "vitest": "^4.0.15"
38
38
  }
package/src/builder.ts CHANGED
@@ -525,8 +525,8 @@ export class CallableMockInstance {
525
525
 
526
526
  /**
527
527
  * Find a route that matches the given method and path
528
- * Uses two-pass matching: exact routes first, then parameterized routes
529
- * Searches in reverse order to prefer most recently defined routes
528
+ * Uses two-pass matching: static routes first, then parameterized routes
529
+ * Matches routes in registration order (first registered wins)
530
530
  * @param method - HTTP method to match
531
531
  * @param path - Request path to match
532
532
  * @returns Matched compiled route or undefined if no match
@@ -536,9 +536,8 @@ export class CallableMockInstance {
536
536
  method: HttpMethod,
537
537
  path: string,
538
538
  ): CompiledCallableRoute | undefined {
539
- // First pass: Look for exact matches (routes without parameters)
540
- for (let i = this.routes.length - 1; i >= 0; i--) {
541
- const route = this.routes[i];
539
+ // First pass: Look for static routes (routes without parameters)
540
+ for (const route of this.routes) {
542
541
  if (
543
542
  route.method === method &&
544
543
  route.params.length === 0 &&
@@ -549,8 +548,7 @@ export class CallableMockInstance {
549
548
  }
550
549
 
551
550
  // Second pass: Look for parameterized routes
552
- for (let i = this.routes.length - 1; i >= 0; i--) {
553
- const route = this.routes[i];
551
+ for (const route of this.routes) {
554
552
  if (
555
553
  route.method === method &&
556
554
  route.params.length > 0 &&
@@ -128,7 +128,7 @@ describe("route matching", () => {
128
128
  });
129
129
 
130
130
  describe("route precedence and conflicts", () => {
131
- it("matches most recently defined route when patterns overlap", async () => {
131
+ it("prioritizes static routes over parameterized routes", async () => {
132
132
  const mock = schmock();
133
133
  mock("GET /users/:id", "parameterized");
134
134
  mock("GET /users/special", "static");
@@ -136,7 +136,7 @@ describe("route matching", () => {
136
136
  const paramResponse = await mock.handle("GET", "/users/123");
137
137
  const staticResponse = await mock.handle("GET", "/users/special");
138
138
 
139
- // The static route should be matched since it was defined later
139
+ // Static routes should always be checked before parameterized routes
140
140
  expect(paramResponse.body).toBe("parameterized");
141
141
  expect(staticResponse.body).toBe("static");
142
142
  });
@@ -153,18 +153,61 @@ describe("route matching", () => {
153
153
  expect(v1Response.body).toBe("v1-specific");
154
154
  });
155
155
 
156
- it("matches first matching route in definition order", async () => {
156
+ it("matches routes in registration order (first registered wins)", async () => {
157
157
  const mock = schmock();
158
158
  mock("GET /:type/items", "first");
159
159
  mock("GET /shop/:category", "second");
160
160
 
161
161
  const response = await mock.handle("GET", "/shop/items");
162
162
 
163
- // Both routes match, but with reverse order search, the second route should win
164
- // /:type/items matches with type="shop"
165
- // /shop/:category matches with category="items"
166
- // Since we search in reverse, /shop/:category (more specific) should match
167
- expect(response.body).toBe("second");
163
+ // Both routes match, but the first registered route should win
164
+ // This matches the behavior of Express, Hono, Fastify, etc.
165
+ expect(response.body).toBe("first");
166
+ });
167
+
168
+ it("matches specific routes before wildcard when registered in natural order", async () => {
169
+ // Bug report reproduction: natural order (specific before wildcard)
170
+ const mock = schmock();
171
+ mock("GET /api/items/special", () => ({ type: "special" }));
172
+ mock("GET /api/items/:id", () => ({ type: "generic" }));
173
+
174
+ const specialResult = await mock.handle("GET", "/api/items/special");
175
+ const genericResult = await mock.handle("GET", "/api/items/123");
176
+
177
+ // Static route should match for /api/items/special
178
+ expect(specialResult.body).toEqual({ type: "special" });
179
+ // Parameterized route should match for /api/items/123
180
+ expect(genericResult.body).toEqual({ type: "generic" });
181
+ });
182
+
183
+ it("matches multiple specific routes before wildcard", async () => {
184
+ // Bug report scenario with multiple specific routes
185
+ const mock = schmock();
186
+ mock("GET /api/vulns/aggregated", "aggregated");
187
+ mock("GET /api/vulns/count", "count");
188
+ mock("GET /api/vulns/familyList", "familyList");
189
+ mock("GET /api/vulns/:vulnId", "byId");
190
+
191
+ const aggregatedRes = await mock.handle("GET", "/api/vulns/aggregated");
192
+ const countRes = await mock.handle("GET", "/api/vulns/count");
193
+ const familyListRes = await mock.handle("GET", "/api/vulns/familyList");
194
+ const byIdRes = await mock.handle("GET", "/api/vulns/CVE-2024-1234");
195
+
196
+ expect(aggregatedRes.body).toBe("aggregated");
197
+ expect(countRes.body).toBe("count");
198
+ expect(familyListRes.body).toBe("familyList");
199
+ expect(byIdRes.body).toBe("byId");
200
+ });
201
+
202
+ it("matches overlapping parameterized routes in registration order", async () => {
203
+ const mock = schmock();
204
+ mock("GET /api/:org/users/:id", "first-pattern");
205
+ mock("GET /api/:version/users/:userId", "second-pattern");
206
+
207
+ const response = await mock.handle("GET", "/api/acme/users/123");
208
+
209
+ // When both routes are parameterized and match, first registered wins
210
+ expect(response.body).toBe("first-pattern");
168
211
  });
169
212
  });
170
213