Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
79e879d
Spector datetime matchers
timotheeguerin Mar 12, 2026
91c88ff
organize
timotheeguerin Mar 12, 2026
280ca8a
tweaks
timotheeguerin Mar 12, 2026
881e1ec
cleanup
timotheeguerin Mar 12, 2026
121d5eb
cleanup
timotheeguerin Mar 12, 2026
ff3f3d2
rfc7231
timotheeguerin Mar 12, 2026
b1e207d
Simplify some more
timotheeguerin Mar 12, 2026
34d4e6f
better errors
timotheeguerin Mar 12, 2026
72741ab
format
timotheeguerin Mar 12, 2026
61fc557
Fix headers and query
timotheeguerin Mar 13, 2026
a6c8b36
Merge branch 'main' of https://github.com/Microsoft/typespec into fea…
timotheeguerin Mar 18, 2026
d7f0024
base url too
timotheeguerin Mar 18, 2026
a830dbe
progress
timotheeguerin Mar 18, 2026
fec25f0
simplify
timotheeguerin Mar 18, 2026
617c50b
organize
timotheeguerin Mar 18, 2026
08cfba3
cleanup
timotheeguerin Mar 18, 2026
f4f44bc
Tweaks
timotheeguerin Mar 18, 2026
0a78444
Better names
timotheeguerin Mar 18, 2026
f92aef2
cleaner
timotheeguerin Mar 18, 2026
2041735
fix lint and format
timotheeguerin Mar 18, 2026
34ea8f8
specific utc enforce
timotheeguerin Mar 18, 2026
f3e6045
do xml datetime too
timotheeguerin Mar 18, 2026
16607f2
Merge branch 'main' of https://github.com/Microsoft/typespec into fea…
timotheeguerin Mar 18, 2026
5ae72d3
fix order
timotheeguerin Mar 18, 2026
af44f9c
fix
timotheeguerin Mar 18, 2026
afe2634
fix xml validation
timotheeguerin Mar 19, 2026
e493a70
simplify
timotheeguerin Mar 19, 2026
b62486d
simplify test
timotheeguerin Mar 19, 2026
3a84f53
Merge branch 'main' into feat/spector-matchers
timotheeguerin Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .chronus/changes/feat-spector-matchers-2026-2-12-14-17-25.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
changeKind: feature
packages:
- "@typespec/spec-api"
- "@typespec/spector"
- "@typespec/http-specs"
---

Add matcher framework for flexible value comparison in scenarios. `match.dateTime()` enables semantic datetime comparison that handles precision and timezone differences across languages.
165 changes: 29 additions & 136 deletions packages/http-specs/specs/encode/datetime/mockapi.ts
Original file line number Diff line number Diff line change
@@ -1,274 +1,167 @@
import {
CollectionFormat,
json,
MockRequest,
passOnSuccess,
ScenarioMockApi,
validateValueFormat,
ValidationError,
} from "@typespec/spec-api";
import { json, match, MockRequest, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api";

export const Scenarios: Record<string, ScenarioMockApi> = {};

function createQueryServerTests(
uri: string,
paramData: any,
format: "rfc7231" | "rfc3339" | undefined,
value: any,
collectionFormat?: CollectionFormat,
format: "rfc7231" | "rfc3339" | "utcRfc3339" | undefined,
) {
return passOnSuccess({
uri,
method: "get",
request: {
query: paramData,
query: { value: format ? match.dateTime[format](value) : value },
},
response: {
status: 204,
},
handler(req: MockRequest) {
if (format) {
validateValueFormat(req.query["value"] as string, format);
if (Date.parse(req.query["value"] as string) !== Date.parse(value)) {
throw new ValidationError(`Wrong value`, value, req.query["value"]);
}
} else {
req.expect.containsQueryParam("value", value, collectionFormat);
}
return {
status: 204,
};
},
kind: "MockApiDefinition",
});
}
Scenarios.Encode_Datetime_Query_default = createQueryServerTests(
"/encode/datetime/query/default",
{
value: "2022-08-26T18:38:00.000Z",
},
"rfc3339",
"2022-08-26T18:38:00.000Z",
"utcRfc3339",
);
Scenarios.Encode_Datetime_Query_rfc3339 = createQueryServerTests(
"/encode/datetime/query/rfc3339",
{
value: "2022-08-26T18:38:00.000Z",
},
"rfc3339",
"2022-08-26T18:38:00.000Z",
"utcRfc3339",
);
Scenarios.Encode_Datetime_Query_rfc7231 = createQueryServerTests(
"/encode/datetime/query/rfc7231",
{
value: "Fri, 26 Aug 2022 14:38:00 GMT",
},
"rfc7231",
"Fri, 26 Aug 2022 14:38:00 GMT",
"rfc7231",
);
Scenarios.Encode_Datetime_Query_unixTimestamp = createQueryServerTests(
"/encode/datetime/query/unix-timestamp",
{
value: 1686566864,
},
undefined,
"1686566864",
undefined,
);
Scenarios.Encode_Datetime_Query_unixTimestampArray = createQueryServerTests(
"/encode/datetime/query/unix-timestamp-array",
{
value: [1686566864, 1686734256].join(","),
},
[1686566864, 1686734256].join(","),
undefined,
["1686566864", "1686734256"],
"csv",
);
function createPropertyServerTests(
uri: string,
data: any,
format: "rfc7231" | "rfc3339" | undefined,
value: any,
format: "rfc7231" | "rfc3339" | "utcRfc3339" | undefined,
) {
const matcherBody = { value: format ? match.dateTime[format](value) : value };
return passOnSuccess({
uri,
method: "post",
request: {
body: json(data),
body: json(matcherBody),
},
response: {
status: 200,
},
handler: (req: MockRequest) => {
if (format) {
validateValueFormat(req.body["value"], format);
if (Date.parse(req.body["value"]) !== Date.parse(value)) {
throw new ValidationError(`Wrong value`, value, req.body["value"]);
}
} else {
req.expect.coercedBodyEquals({ value: value });
}
return {
status: 200,
body: json({ value: value }),
};
body: json(matcherBody),
},
kind: "MockApiDefinition",
});
}
Scenarios.Encode_Datetime_Property_default = createPropertyServerTests(
"/encode/datetime/property/default",
{
value: "2022-08-26T18:38:00.000Z",
},
"rfc3339",
"2022-08-26T18:38:00.000Z",
"utcRfc3339",
);
Scenarios.Encode_Datetime_Property_rfc3339 = createPropertyServerTests(
"/encode/datetime/property/rfc3339",
{
value: "2022-08-26T18:38:00.000Z",
},
"rfc3339",
"2022-08-26T18:38:00.000Z",
"utcRfc3339",
);
Scenarios.Encode_Datetime_Property_rfc7231 = createPropertyServerTests(
"/encode/datetime/property/rfc7231",
{
value: "Fri, 26 Aug 2022 14:38:00 GMT",
},
"rfc7231",
"Fri, 26 Aug 2022 14:38:00 GMT",
"rfc7231",
);
Scenarios.Encode_Datetime_Property_unixTimestamp = createPropertyServerTests(
"/encode/datetime/property/unix-timestamp",
{
value: 1686566864,
},
undefined,
1686566864,
undefined,
);
Scenarios.Encode_Datetime_Property_unixTimestampArray = createPropertyServerTests(
"/encode/datetime/property/unix-timestamp-array",
{
value: [1686566864, 1686734256],
},
undefined,
[1686566864, 1686734256],
undefined,
);
function createHeaderServerTests(
uri: string,
data: any,
format: "rfc7231" | "rfc3339" | undefined,
value: any,
format: "rfc7231" | "rfc3339" | "utcRfc3339" | undefined,
) {
const matcherHeaders = { value: format ? match.dateTime[format](value) : value };
return passOnSuccess({
uri,
method: "get",
request: {
headers: data,
headers: matcherHeaders,
},
response: {
status: 204,
},
handler(req: MockRequest) {
if (format) {
validateValueFormat(req.headers["value"], format);
if (Date.parse(req.headers["value"]) !== Date.parse(value)) {
throw new ValidationError(`Wrong value`, value, req.headers["value"]);
}
} else {
req.expect.containsHeader("value", value);
}
return {
status: 204,
};
},
kind: "MockApiDefinition",
});
}
Scenarios.Encode_Datetime_Header_default = createHeaderServerTests(
"/encode/datetime/header/default",
{
value: "Fri, 26 Aug 2022 14:38:00 GMT",
},
"rfc7231",
"Fri, 26 Aug 2022 14:38:00 GMT",
"rfc7231",
);
Scenarios.Encode_Datetime_Header_rfc3339 = createHeaderServerTests(
"/encode/datetime/header/rfc3339",
{
value: "2022-08-26T18:38:00.000Z",
},
"rfc3339",
"2022-08-26T18:38:00.000Z",
"utcRfc3339",
);
Scenarios.Encode_Datetime_Header_rfc7231 = createHeaderServerTests(
"/encode/datetime/header/rfc7231",
{
value: "Fri, 26 Aug 2022 14:38:00 GMT",
},
"rfc7231",
"Fri, 26 Aug 2022 14:38:00 GMT",
"rfc7231",
);
Scenarios.Encode_Datetime_Header_unixTimestamp = createHeaderServerTests(
"/encode/datetime/header/unix-timestamp",
{
value: 1686566864,
},
1686566864,
undefined,
"1686566864",
);
Scenarios.Encode_Datetime_Header_unixTimestampArray = createHeaderServerTests(
"/encode/datetime/header/unix-timestamp-array",
{
value: [1686566864, 1686734256].join(","),
},
[1686566864, 1686734256].join(","),
undefined,
"1686566864,1686734256",
);
function createResponseHeaderServerTests(uri: string, data: any, value: any) {
function createResponseHeaderServerTests(uri: string, value: any) {
return passOnSuccess({
uri,
method: "get",
request: {},
response: {
status: 204,
headers: data,
headers: { value },
},
handler: (req: MockRequest) => {
return {
status: 204,
headers: { value: value },
headers: { value },
};
},
kind: "MockApiDefinition",
});
}
Scenarios.Encode_Datetime_ResponseHeader_default = createResponseHeaderServerTests(
"/encode/datetime/responseheader/default",
{
value: "Fri, 26 Aug 2022 14:38:00 GMT",
},
"Fri, 26 Aug 2022 14:38:00 GMT",
);
Scenarios.Encode_Datetime_ResponseHeader_rfc3339 = createResponseHeaderServerTests(
"/encode/datetime/responseheader/rfc3339",
{
value: "2022-08-26T18:38:00.000Z",
},
"2022-08-26T18:38:00.000Z",
);
Scenarios.Encode_Datetime_ResponseHeader_rfc7231 = createResponseHeaderServerTests(
"/encode/datetime/responseheader/rfc7231",
{
value: "Fri, 26 Aug 2022 14:38:00 GMT",
},
"Fri, 26 Aug 2022 14:38:00 GMT",
);
Scenarios.Encode_Datetime_ResponseHeader_unixTimestamp = createResponseHeaderServerTests(
"/encode/datetime/responseheader/unix-timestamp",
{
value: "1686566864",
},
1686566864,
"1686566864",
);
49 changes: 16 additions & 33 deletions packages/http-specs/specs/payload/pageable/mockapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {
dyn,
dynItem,
json,
match,
MockRequest,
passOnSuccess,
ResolverConfig,
ScenarioMockApi,
ValidationError,
xml,
Expand Down Expand Up @@ -650,22 +650,6 @@ Scenarios.Payload_Pageable_XmlPagination_listWithContinuation = passOnSuccess([
},
]);

const xmlNextLinkFirstPage = (baseUrl: string) => `
<PetListResult>
<Pets>
<Pet>
<Id>1</Id>
<Name>dog</Name>
</Pet>
<Pet>
<Id>2</Id>
<Name>cat</Name>
</Pet>
</Pets>
<NextLink>${baseUrl}/payload/pageable/xml/list-with-next-link/nextPage</NextLink>
</PetListResult>
`;

const XmlNextLinkSecondPage = `
<PetListResult>
<Pets>
Expand All @@ -688,26 +672,25 @@ Scenarios.Payload_Pageable_XmlPagination_listWithNextLink = passOnSuccess([
request: {},
response: {
status: 200,
body: {
contentType: "application/xml",
rawContent: {
serialize: (config: ResolverConfig) =>
`<?xml version='1.0' encoding='UTF-8'?>` + xmlNextLinkFirstPage(config.baseUrl),
},
},
body: xml`
<PetListResult>
<Pets>
<Pet>
<Id>1</Id>
<Name>dog</Name>
</Pet>
<Pet>
<Id>2</Id>
<Name>cat</Name>
</Pet>
</Pets>
<NextLink>${match.localUrl("/payload/pageable/xml/list-with-next-link/nextPage")}</NextLink>
</PetListResult>
`,
headers: {
"content-type": "application/xml; charset=utf-8",
},
},
handler: (req: MockRequest) => {
return {
status: 200,
body: xml(xmlNextLinkFirstPage(req.baseUrl)),
headers: {
"content-type": "application/xml",
},
};
},
kind: "MockApiDefinition",
},
{
Expand Down
Loading
Loading