@@ -217,6 +217,62 @@ describe("integration with expandDyns", () => {
217217 } ) ;
218218} ) ;
219219
220+ describe ( "integration with expandDyns({ resolveMatchers: false })" , ( ) => {
221+ const config : ResolverConfig = { baseUrl : "http://localhost:3000" } ;
222+
223+ it ( "should preserve matcher objects instead of resolving them to plain strings" , ( ) => {
224+ const content = { timestamp : match . dateTime . rfc3339 ( "2022-08-26T18:38:00.000Z" ) } ;
225+ const expanded = expandDyns ( content , config , { resolveMatchers : false } ) ;
226+ // Matcher must survive as a matcher, not be converted to a plain string
227+ expect ( isMatcher ( expanded . timestamp ) ) . toBe ( true ) ;
228+ } ) ;
229+
230+ it ( "should allow matchValues to do semantic datetime comparison after expandDyns with resolveMatchers:false" , ( ) => {
231+ // Regression test: query params/headers with datetime matchers must use semantic comparison.
232+ // Without resolveMatchers:false, expandDyns converts the matcher to the plain string
233+ // "2022-08-26T18:38:00.000Z", and a strict === comparison against the actual value
234+ // "2022-08-26T18:38:00Z" (no milliseconds) would fail even though they represent the
235+ // same point in time.
236+ const queryDef = { input : match . dateTime . utcRfc3339 ( "2022-08-26T18:38:00.000Z" ) } ;
237+ const expanded = expandDyns ( queryDef , config , { resolveMatchers : false } ) ;
238+
239+ // The actual query string received from an HTTP request (no milliseconds)
240+ const actualQueryValue = "2022-08-26T18:38:00Z" ;
241+
242+ // Simulates what createHandler does: isMatcher → deepEqual → matchValues → matcher.check()
243+ expect ( isMatcher ( expanded . input ) ) . toBe ( true ) ;
244+ expectPass ( matchValues ( actualQueryValue , expanded . input , "$" , config ) ) ;
245+ } ) ;
246+
247+ it ( "should allow matchValues to do semantic datetime comparison for header values after expandDyns with resolveMatchers:false" , ( ) => {
248+ // Regression test: headers with datetime matchers must use semantic comparison, same as query params.
249+ // Without resolveMatchers:false the matcher is serialized early and isMatcher() returns false,
250+ // so the code falls through to containsHeader() with String(value) — a strict string equality
251+ // that fails for semantically equivalent but format-different datetime strings.
252+ const headerDef = { "x-ms-date" : match . dateTime . rfc7231 ( "Fri, 26 Aug 2022 18:38:00 GMT" ) } ;
253+ const expanded = expandDyns ( headerDef , config , { resolveMatchers : false } ) ;
254+
255+ // isMatcher must still be true so createHandler routes through deepEqual / matchValues
256+ expect ( isMatcher ( expanded [ "x-ms-date" ] ) ) . toBe ( true ) ;
257+ // Semantic check passes for the exact same RFC 7231 string
258+ expectPass ( matchValues ( "Fri, 26 Aug 2022 18:38:00 GMT" , expanded [ "x-ms-date" ] , "$" , config ) ) ;
259+ } ) ;
260+
261+ it ( "should demonstrate why resolveMatchers:true (default) breaks semantic query param matching" , ( ) => {
262+ // With the default resolveMatchers:true, the matcher is eagerly converted to a plain string.
263+ // A strict string comparison then fails for semantically equivalent but format-different values.
264+ const queryDef = { input : match . dateTime . utcRfc3339 ( "2022-08-26T18:38:00.000Z" ) } ;
265+ const expandedWithResolve = expandDyns ( queryDef , config ) ; // resolveMatchers: true (default)
266+
267+ // The matcher is gone — replaced by its serialized string
268+ expect ( isMatcher ( expandedWithResolve . input ) ) . toBe ( false ) ;
269+ expect ( expandedWithResolve . input ) . toBe ( "2022-08-26T18:38:00.000Z" ) ;
270+
271+ // Strict string comparison fails for an equivalent datetime without milliseconds
272+ expect ( expandedWithResolve . input === "2022-08-26T18:38:00Z" ) . toBe ( false ) ;
273+ } ) ;
274+ } ) ;
275+
220276describe ( "integration with json() Resolver" , ( ) => {
221277 const config : ResolverConfig = { baseUrl : "http://localhost:3000" } ;
222278
0 commit comments