Skip to content

Commit 018c110

Browse files
Spector datetime matchers (#10011)
fix #9997
1 parent e22670a commit 018c110

21 files changed

Lines changed: 1434 additions & 323 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@typespec/spec-api"
5+
- "@typespec/spector"
6+
- "@typespec/http-specs"
7+
---
8+
9+
Add matcher framework for flexible value comparison in scenarios. `match.dateTime()` enables semantic datetime comparison that handles precision and timezone differences across languages.
Lines changed: 29 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,274 +1,167 @@
1-
import {
2-
CollectionFormat,
3-
json,
4-
MockRequest,
5-
passOnSuccess,
6-
ScenarioMockApi,
7-
validateValueFormat,
8-
ValidationError,
9-
} from "@typespec/spec-api";
1+
import { json, match, MockRequest, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api";
102

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

135
function createQueryServerTests(
146
uri: string,
15-
paramData: any,
16-
format: "rfc7231" | "rfc3339" | undefined,
177
value: any,
18-
collectionFormat?: CollectionFormat,
8+
format: "rfc7231" | "rfc3339" | "utcRfc3339" | undefined,
199
) {
2010
return passOnSuccess({
2111
uri,
2212
method: "get",
2313
request: {
24-
query: paramData,
14+
query: { value: format ? match.dateTime[format](value) : value },
2515
},
2616
response: {
2717
status: 204,
2818
},
29-
handler(req: MockRequest) {
30-
if (format) {
31-
validateValueFormat(req.query["value"] as string, format);
32-
if (Date.parse(req.query["value"] as string) !== Date.parse(value)) {
33-
throw new ValidationError(`Wrong value`, value, req.query["value"]);
34-
}
35-
} else {
36-
req.expect.containsQueryParam("value", value, collectionFormat);
37-
}
38-
return {
39-
status: 204,
40-
};
41-
},
4219
kind: "MockApiDefinition",
4320
});
4421
}
4522
Scenarios.Encode_Datetime_Query_default = createQueryServerTests(
4623
"/encode/datetime/query/default",
47-
{
48-
value: "2022-08-26T18:38:00.000Z",
49-
},
50-
"rfc3339",
5124
"2022-08-26T18:38:00.000Z",
25+
"utcRfc3339",
5226
);
5327
Scenarios.Encode_Datetime_Query_rfc3339 = createQueryServerTests(
5428
"/encode/datetime/query/rfc3339",
55-
{
56-
value: "2022-08-26T18:38:00.000Z",
57-
},
58-
"rfc3339",
5929
"2022-08-26T18:38:00.000Z",
30+
"utcRfc3339",
6031
);
6132
Scenarios.Encode_Datetime_Query_rfc7231 = createQueryServerTests(
6233
"/encode/datetime/query/rfc7231",
63-
{
64-
value: "Fri, 26 Aug 2022 14:38:00 GMT",
65-
},
66-
"rfc7231",
6734
"Fri, 26 Aug 2022 14:38:00 GMT",
35+
"rfc7231",
6836
);
6937
Scenarios.Encode_Datetime_Query_unixTimestamp = createQueryServerTests(
7038
"/encode/datetime/query/unix-timestamp",
71-
{
72-
value: 1686566864,
73-
},
74-
undefined,
7539
"1686566864",
40+
undefined,
7641
);
7742
Scenarios.Encode_Datetime_Query_unixTimestampArray = createQueryServerTests(
7843
"/encode/datetime/query/unix-timestamp-array",
79-
{
80-
value: [1686566864, 1686734256].join(","),
81-
},
44+
[1686566864, 1686734256].join(","),
8245
undefined,
83-
["1686566864", "1686734256"],
84-
"csv",
8546
);
8647
function createPropertyServerTests(
8748
uri: string,
88-
data: any,
89-
format: "rfc7231" | "rfc3339" | undefined,
9049
value: any,
50+
format: "rfc7231" | "rfc3339" | "utcRfc3339" | undefined,
9151
) {
52+
const matcherBody = { value: format ? match.dateTime[format](value) : value };
9253
return passOnSuccess({
9354
uri,
9455
method: "post",
9556
request: {
96-
body: json(data),
57+
body: json(matcherBody),
9758
},
9859
response: {
9960
status: 200,
100-
},
101-
handler: (req: MockRequest) => {
102-
if (format) {
103-
validateValueFormat(req.body["value"], format);
104-
if (Date.parse(req.body["value"]) !== Date.parse(value)) {
105-
throw new ValidationError(`Wrong value`, value, req.body["value"]);
106-
}
107-
} else {
108-
req.expect.coercedBodyEquals({ value: value });
109-
}
110-
return {
111-
status: 200,
112-
body: json({ value: value }),
113-
};
61+
body: json(matcherBody),
11462
},
11563
kind: "MockApiDefinition",
11664
});
11765
}
11866
Scenarios.Encode_Datetime_Property_default = createPropertyServerTests(
11967
"/encode/datetime/property/default",
120-
{
121-
value: "2022-08-26T18:38:00.000Z",
122-
},
123-
"rfc3339",
12468
"2022-08-26T18:38:00.000Z",
69+
"utcRfc3339",
12570
);
12671
Scenarios.Encode_Datetime_Property_rfc3339 = createPropertyServerTests(
12772
"/encode/datetime/property/rfc3339",
128-
{
129-
value: "2022-08-26T18:38:00.000Z",
130-
},
131-
"rfc3339",
13273
"2022-08-26T18:38:00.000Z",
74+
"utcRfc3339",
13375
);
13476
Scenarios.Encode_Datetime_Property_rfc7231 = createPropertyServerTests(
13577
"/encode/datetime/property/rfc7231",
136-
{
137-
value: "Fri, 26 Aug 2022 14:38:00 GMT",
138-
},
139-
"rfc7231",
14078
"Fri, 26 Aug 2022 14:38:00 GMT",
79+
"rfc7231",
14180
);
14281
Scenarios.Encode_Datetime_Property_unixTimestamp = createPropertyServerTests(
14382
"/encode/datetime/property/unix-timestamp",
144-
{
145-
value: 1686566864,
146-
},
147-
undefined,
14883
1686566864,
84+
undefined,
14985
);
15086
Scenarios.Encode_Datetime_Property_unixTimestampArray = createPropertyServerTests(
15187
"/encode/datetime/property/unix-timestamp-array",
152-
{
153-
value: [1686566864, 1686734256],
154-
},
155-
undefined,
15688
[1686566864, 1686734256],
89+
undefined,
15790
);
15891
function createHeaderServerTests(
15992
uri: string,
160-
data: any,
161-
format: "rfc7231" | "rfc3339" | undefined,
16293
value: any,
94+
format: "rfc7231" | "rfc3339" | "utcRfc3339" | undefined,
16395
) {
96+
const matcherHeaders = { value: format ? match.dateTime[format](value) : value };
16497
return passOnSuccess({
16598
uri,
16699
method: "get",
167100
request: {
168-
headers: data,
101+
headers: matcherHeaders,
169102
},
170103
response: {
171104
status: 204,
172105
},
173-
handler(req: MockRequest) {
174-
if (format) {
175-
validateValueFormat(req.headers["value"], format);
176-
if (Date.parse(req.headers["value"]) !== Date.parse(value)) {
177-
throw new ValidationError(`Wrong value`, value, req.headers["value"]);
178-
}
179-
} else {
180-
req.expect.containsHeader("value", value);
181-
}
182-
return {
183-
status: 204,
184-
};
185-
},
186106
kind: "MockApiDefinition",
187107
});
188108
}
189109
Scenarios.Encode_Datetime_Header_default = createHeaderServerTests(
190110
"/encode/datetime/header/default",
191-
{
192-
value: "Fri, 26 Aug 2022 14:38:00 GMT",
193-
},
194-
"rfc7231",
195111
"Fri, 26 Aug 2022 14:38:00 GMT",
112+
"rfc7231",
196113
);
197114
Scenarios.Encode_Datetime_Header_rfc3339 = createHeaderServerTests(
198115
"/encode/datetime/header/rfc3339",
199-
{
200-
value: "2022-08-26T18:38:00.000Z",
201-
},
202-
"rfc3339",
203116
"2022-08-26T18:38:00.000Z",
117+
"utcRfc3339",
204118
);
205119
Scenarios.Encode_Datetime_Header_rfc7231 = createHeaderServerTests(
206120
"/encode/datetime/header/rfc7231",
207-
{
208-
value: "Fri, 26 Aug 2022 14:38:00 GMT",
209-
},
210-
"rfc7231",
211121
"Fri, 26 Aug 2022 14:38:00 GMT",
122+
"rfc7231",
212123
);
213124
Scenarios.Encode_Datetime_Header_unixTimestamp = createHeaderServerTests(
214125
"/encode/datetime/header/unix-timestamp",
215-
{
216-
value: 1686566864,
217-
},
126+
1686566864,
218127
undefined,
219-
"1686566864",
220128
);
221129
Scenarios.Encode_Datetime_Header_unixTimestampArray = createHeaderServerTests(
222130
"/encode/datetime/header/unix-timestamp-array",
223-
{
224-
value: [1686566864, 1686734256].join(","),
225-
},
131+
[1686566864, 1686734256].join(","),
226132
undefined,
227-
"1686566864,1686734256",
228133
);
229-
function createResponseHeaderServerTests(uri: string, data: any, value: any) {
134+
function createResponseHeaderServerTests(uri: string, value: any) {
230135
return passOnSuccess({
231136
uri,
232137
method: "get",
233138
request: {},
234139
response: {
235140
status: 204,
236-
headers: data,
141+
headers: { value },
237142
},
238143
handler: (req: MockRequest) => {
239144
return {
240145
status: 204,
241-
headers: { value: value },
146+
headers: { value },
242147
};
243148
},
244149
kind: "MockApiDefinition",
245150
});
246151
}
247152
Scenarios.Encode_Datetime_ResponseHeader_default = createResponseHeaderServerTests(
248153
"/encode/datetime/responseheader/default",
249-
{
250-
value: "Fri, 26 Aug 2022 14:38:00 GMT",
251-
},
252154
"Fri, 26 Aug 2022 14:38:00 GMT",
253155
);
254156
Scenarios.Encode_Datetime_ResponseHeader_rfc3339 = createResponseHeaderServerTests(
255157
"/encode/datetime/responseheader/rfc3339",
256-
{
257-
value: "2022-08-26T18:38:00.000Z",
258-
},
259158
"2022-08-26T18:38:00.000Z",
260159
);
261160
Scenarios.Encode_Datetime_ResponseHeader_rfc7231 = createResponseHeaderServerTests(
262161
"/encode/datetime/responseheader/rfc7231",
263-
{
264-
value: "Fri, 26 Aug 2022 14:38:00 GMT",
265-
},
266162
"Fri, 26 Aug 2022 14:38:00 GMT",
267163
);
268164
Scenarios.Encode_Datetime_ResponseHeader_unixTimestamp = createResponseHeaderServerTests(
269165
"/encode/datetime/responseheader/unix-timestamp",
270-
{
271-
value: "1686566864",
272-
},
273-
1686566864,
166+
"1686566864",
274167
);

packages/http-specs/specs/payload/pageable/mockapi.ts

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import {
22
dyn,
33
dynItem,
44
json,
5+
match,
56
MockRequest,
67
passOnSuccess,
7-
ResolverConfig,
88
ScenarioMockApi,
99
ValidationError,
1010
xml,
@@ -650,22 +650,6 @@ Scenarios.Payload_Pageable_XmlPagination_listWithContinuation = passOnSuccess([
650650
},
651651
]);
652652

653-
const xmlNextLinkFirstPage = (baseUrl: string) => `
654-
<PetListResult>
655-
<Pets>
656-
<Pet>
657-
<Id>1</Id>
658-
<Name>dog</Name>
659-
</Pet>
660-
<Pet>
661-
<Id>2</Id>
662-
<Name>cat</Name>
663-
</Pet>
664-
</Pets>
665-
<NextLink>${baseUrl}/payload/pageable/xml/list-with-next-link/nextPage</NextLink>
666-
</PetListResult>
667-
`;
668-
669653
const XmlNextLinkSecondPage = `
670654
<PetListResult>
671655
<Pets>
@@ -688,26 +672,25 @@ Scenarios.Payload_Pageable_XmlPagination_listWithNextLink = passOnSuccess([
688672
request: {},
689673
response: {
690674
status: 200,
691-
body: {
692-
contentType: "application/xml",
693-
rawContent: {
694-
serialize: (config: ResolverConfig) =>
695-
`<?xml version='1.0' encoding='UTF-8'?>` + xmlNextLinkFirstPage(config.baseUrl),
696-
},
697-
},
675+
body: xml`
676+
<PetListResult>
677+
<Pets>
678+
<Pet>
679+
<Id>1</Id>
680+
<Name>dog</Name>
681+
</Pet>
682+
<Pet>
683+
<Id>2</Id>
684+
<Name>cat</Name>
685+
</Pet>
686+
</Pets>
687+
<NextLink>${match.localUrl("/payload/pageable/xml/list-with-next-link/nextPage")}</NextLink>
688+
</PetListResult>
689+
`,
698690
headers: {
699691
"content-type": "application/xml; charset=utf-8",
700692
},
701693
},
702-
handler: (req: MockRequest) => {
703-
return {
704-
status: 200,
705-
body: xml(xmlNextLinkFirstPage(req.baseUrl)),
706-
headers: {
707-
"content-type": "application/xml",
708-
},
709-
};
710-
},
711694
kind: "MockApiDefinition",
712695
},
713696
{

0 commit comments

Comments
 (0)