Skip to content

Commit e89573e

Browse files
authored
replace vitest-fetch-mock with msw (#1592)
* replace vitest-fetch-mock with msw in index.test This replaces vitest-fetch-mock with msw in the index.test.ts file. * Rename getCookies to getRequestCookies * Rename testPath to toAbsoluteURL * Rename TestRequestHandler to MockRequestHandler * Move msw utils to separate fixture * Update v7-beta.test.ts to utilize msw * Update index.bench.js to utilize msw Additionally ensures all clients are setup correctly for GET tests. * remove vitest-fetch-mock dependency * Update testing.md to include short example of msw * Fix failing test * Add MultipleResponse example to api.yaml and run generate-types * Include 'use the selected content' in v7-beta.test.ts
1 parent 2a66a64 commit e89573e

File tree

10 files changed

+1866
-524
lines changed

10 files changed

+1866
-524
lines changed

docs/openapi-fetch/testing.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,51 @@ test("my API call", async () => {
6565
expect(error).toBeUndefined();
6666
});
6767
```
68+
69+
## Mock Service Worker
70+
71+
[Mock Service Worker](https://mswjs.io/) can also be used for testing and mocking actual responses:
72+
73+
```ts
74+
import { http, HttpResponse } from "msw";
75+
import { setupServer } from "msw/node";
76+
import createClient from "openapi-fetch";
77+
import { afterEach, beforeAll, expect, test } from "vitest";
78+
import type { paths } from "./api/v1";
79+
80+
const server = setupServer();
81+
82+
beforeAll(() => {
83+
// NOTE: server.listen must be called before `createClient` is used to ensure
84+
// the msw can inject its version of `fetch` to intercept the requests.
85+
server.listen({
86+
onUnhandledRequest: (request) => {
87+
throw new Error(
88+
`No request handler found for ${request.method} ${request.url}`,
89+
);
90+
},
91+
});
92+
});
93+
94+
afterEach(() => server.resetHandlers());
95+
afterAll(() => server.close());
96+
97+
test("my API call", async () => {
98+
const rawData = { test: { data: "foo" } };
99+
100+
const BASE_URL = "https://my-site.com";
101+
102+
server.use(
103+
http.get(`${BASE_URL}/api/v1/foo`, () => HttpResponse.json(rawData, { status: 200 }))
104+
);
105+
106+
const client = createClient<paths>({
107+
baseUrl: BASE_URL,
108+
});
109+
110+
const { data, error } = await client.GET("/api/v1/foo");
111+
112+
expect(data).toEqual(rawData);
113+
expect(error).toBeUndefined();
114+
});
115+
```

packages/openapi-fetch/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,12 @@
6969
"axios": "^1.6.7",
7070
"del-cli": "^5.1.0",
7171
"esbuild": "^0.20.0",
72+
"msw": "^2.2.3",
7273
"openapi-typescript": "^6.7.4",
7374
"openapi-typescript-codegen": "^0.25.0",
7475
"openapi-typescript-fetch": "^1.1.3",
7576
"superagent": "^8.1.2",
7677
"typescript": "^5.3.3",
77-
"vitest": "^1.3.1",
78-
"vitest-fetch-mock": "^0.2.2"
78+
"vitest": "^1.3.1"
7979
}
8080
}

packages/openapi-fetch/test/fixtures/api.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Do not make direct changes to the file.
44
*/
55

6+
67
export interface paths {
78
"/comment": {
89
put: {
@@ -577,6 +578,7 @@ export type $defs = Record<string, never>;
577578
export type external = Record<string, never>;
578579

579580
export interface operations {
581+
580582
getHeaderParams: {
581583
parameters: {
582584
header: {

packages/openapi-fetch/test/fixtures/api.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,11 @@ paths:
447447
responses:
448448
200:
449449
$ref: '#/components/responses/Contact'
450+
/multiple-response-content:
451+
get:
452+
responses:
453+
200:
454+
$ref: '#/components/responses/MultipleResponse'
450455
components:
451456
schemas:
452457
Post:
@@ -672,3 +677,31 @@ components:
672677
application/json:
673678
schema:
674679
$ref: '#/components/schemas/User'
680+
MultipleResponse:
681+
content:
682+
application/json:
683+
schema:
684+
type: object
685+
properties:
686+
id:
687+
type: string
688+
email:
689+
type: string
690+
name:
691+
type: string
692+
required:
693+
- id
694+
- email
695+
application/ld+json:
696+
schema:
697+
type: object
698+
properties:
699+
'@id':
700+
type: string
701+
email:
702+
type: string
703+
name:
704+
type: string
705+
required:
706+
- '@id'
707+
- email
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import {
2+
http,
3+
HttpResponse,
4+
type JsonBodyType,
5+
type StrictRequest,
6+
type DefaultBodyType,
7+
type HttpResponseResolver,
8+
type PathParams,
9+
type AsyncResponseResolverReturnType,
10+
} from "msw";
11+
import { setupServer } from "msw/node";
12+
13+
/**
14+
* Mock server instance
15+
*/
16+
export const server = setupServer();
17+
18+
/**
19+
* Default baseUrl for tests
20+
*/
21+
export const baseUrl = "https://api.example.com" as const;
22+
23+
/**
24+
* Test path helper, returns a an absolute URL based on
25+
* the given path and base
26+
*/
27+
export function toAbsoluteURL(path: string, base: string = baseUrl) {
28+
// If we have absolute path
29+
if (URL.canParse(path)) {
30+
return new URL(path).toString();
31+
}
32+
33+
// Otherwise we want to support relative paths
34+
// where base may also contain some part of the path
35+
// e.g.
36+
// base = https://api.foo.bar/v1/
37+
// path = /self
38+
// should result in https://api.foo.bar/v1/self
39+
40+
// Construct base URL
41+
const baseUrlInstance = new URL(base);
42+
43+
// prepend base url url pathname to path and ensure only one slash between the URL parts
44+
const newPath = `${baseUrlInstance.pathname}/${path}`.replace(/\/+/g, "/");
45+
46+
return new URL(newPath, baseUrlInstance).toString();
47+
}
48+
49+
export type MswHttpMethod = keyof typeof http;
50+
51+
export interface MockRequestHandlerOptions<
52+
// Recreate the generic signature of the HTTP resolver
53+
// so the arguments passed to http handlers propagate here.
54+
Params extends PathParams<keyof Params> = PathParams,
55+
RequestBodyType extends DefaultBodyType = DefaultBodyType,
56+
ResponseBodyType extends DefaultBodyType = undefined,
57+
> {
58+
baseUrl?: string;
59+
method: MswHttpMethod;
60+
/**
61+
* Relative or absolute path to match.
62+
* When relative, baseUrl will be used as base.
63+
*/
64+
path: string;
65+
body?: JsonBodyType;
66+
headers?: Record<string, string>;
67+
status?: number;
68+
69+
/**
70+
* Optional handler which will be called instead of using the body, headers and status
71+
*/
72+
handler?: HttpResponseResolver<Params, RequestBodyType, ResponseBodyType>;
73+
}
74+
75+
/**
76+
* Configures a msw request handler using the provided options.
77+
*/
78+
export function useMockRequestHandler<
79+
// Recreate the generic signature of the HTTP resolver
80+
// so the arguments passed to http handlers propagate here.
81+
Params extends PathParams<keyof Params> = PathParams,
82+
RequestBodyType extends DefaultBodyType = DefaultBodyType,
83+
ResponseBodyType extends DefaultBodyType = undefined,
84+
>({
85+
baseUrl: requestBaseUrl,
86+
method,
87+
path,
88+
body,
89+
headers,
90+
status,
91+
handler,
92+
}: MockRequestHandlerOptions<Params, RequestBodyType, ResponseBodyType>) {
93+
let requestUrl = "";
94+
let receivedRequest: null | StrictRequest<DefaultBodyType> = null;
95+
let receivedCookies: null | Record<string, string> = null;
96+
97+
const resolvedPath = toAbsoluteURL(path, requestBaseUrl);
98+
99+
server.use(
100+
http[method]<Params, RequestBodyType, ResponseBodyType>(
101+
resolvedPath,
102+
(args) => {
103+
requestUrl = args.request.url;
104+
receivedRequest = args.request.clone();
105+
receivedCookies = { ...args.cookies };
106+
107+
if (handler) {
108+
return handler(args);
109+
}
110+
111+
return HttpResponse.json(body, {
112+
status: status ?? 200,
113+
headers,
114+
}) as AsyncResponseResolverReturnType<ResponseBodyType>;
115+
},
116+
),
117+
);
118+
119+
return {
120+
getRequestCookies: () => receivedCookies!,
121+
getRequest: () => receivedRequest!,
122+
getRequestUrl: () => new URL(requestUrl),
123+
};
124+
}

packages/openapi-fetch/test/fixtures/v7-beta.d.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,33 @@ export interface paths {
729729
patch?: never;
730730
trace?: never;
731731
};
732+
"/multiple-response-content": {
733+
parameters: {
734+
query?: never;
735+
header?: never;
736+
path?: never;
737+
cookie?: never;
738+
};
739+
get: {
740+
parameters: {
741+
query?: never;
742+
header?: never;
743+
path?: never;
744+
cookie?: never;
745+
};
746+
requestBody?: never;
747+
responses: {
748+
200: components["responses"]["MultipleResponse"];
749+
};
750+
};
751+
put?: never;
752+
post?: never;
753+
delete?: never;
754+
options?: never;
755+
head?: never;
756+
patch?: never;
757+
trace?: never;
758+
};
732759
}
733760
export type webhooks = Record<string, never>;
734761
export interface components {
@@ -859,6 +886,23 @@ export interface components {
859886
"application/json": components["schemas"]["User"];
860887
};
861888
};
889+
MultipleResponse: {
890+
headers: {
891+
[name: string]: unknown;
892+
};
893+
content: {
894+
"application/json": {
895+
id: string;
896+
email: string;
897+
name?: string;
898+
};
899+
"application/ld+json": {
900+
"@id": string;
901+
email: string;
902+
name?: string;
903+
};
904+
};
905+
};
862906
};
863907
parameters: never;
864908
requestBodies: {

0 commit comments

Comments
 (0)