Skip to content

Commit 7529f73

Browse files
chore: type-safe msw endpoint helper (#288)
* chore: util function for MSW endpoint * chore: apply msw endpoint to handlers * chore: mockers for API responses * chore: add eslint rule to enforce safe msw usage * chore: apply new MSW approach across codebase
1 parent bb6bc4c commit 7529f73

23 files changed

+363
-245
lines changed

Diff for: eslint.config.js

+14
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ export default tseslint.config(
105105
message:
106106
"Do not directly call `invalidateQueries`. Instead, use the `invalidateQueries` helper function.",
107107
},
108+
{
109+
selector: [
110+
"CallExpression[callee.object.name='http'][callee.property.name='all'] > Literal:first-child",
111+
"CallExpression[callee.object.name='http'][callee.property.name='head'] > Literal:first-child",
112+
"CallExpression[callee.object.name='http'][callee.property.name='get'] > Literal:first-child",
113+
"CallExpression[callee.object.name='http'][callee.property.name='post'] > Literal:first-child",
114+
"CallExpression[callee.object.name='http'][callee.property.name='put'] > Literal:first-child",
115+
"CallExpression[callee.object.name='http'][callee.property.name='delete'] > Literal:first-child",
116+
"CallExpression[callee.object.name='http'][callee.property.name='patch'] > Literal:first-child",
117+
"CallExpression[callee.object.name='http'][callee.property.name='options'] > Literal:first-child",
118+
].join(", "),
119+
message:
120+
"Do not pass a string as the first argument to methods on Mock Service Worker's `http`. Use the `mswEndpoint` helper function instead, which provides type-safe routes based on the OpenAPI spec and the API base URL.",
121+
},
108122
],
109123
"no-restricted-imports": [
110124
"error",

Diff for: src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx

+7-5
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { test } from "vitest";
33
import { http, HttpResponse } from "msw";
44
import { render, waitFor } from "@/lib/test-utils";
55
import { AlertsSummaryMaliciousPkg } from "../alerts-summary-malicious-pkg";
6-
import { makeMockAlert } from "../../mocks/alert.mock";
6+
7+
import { mswEndpoint } from "@/test/msw-endpoint";
8+
import { mockAlert } from "@/mocks/msw/mockers/alert.mock";
79

810
test("shows correct count when there is a malicious alert", async () => {
911
server.use(
10-
http.get("*/api/v1/workspaces/:name/alerts", () => {
11-
return HttpResponse.json([makeMockAlert({ type: "malicious" })]);
12+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
13+
return HttpResponse.json([mockAlert({ type: "malicious" })]);
1214
}),
1315
);
1416

@@ -21,8 +23,8 @@ test("shows correct count when there is a malicious alert", async () => {
2123

2224
test("shows correct count when there is no malicious alert", async () => {
2325
server.use(
24-
http.get("*/api/v1/workspaces/:name/alerts", () => {
25-
return HttpResponse.json([makeMockAlert({ type: "secret" })]);
26+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
27+
return HttpResponse.json([mockAlert({ type: "secret" })]);
2628
}),
2729
);
2830

Diff for: src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { http, HttpResponse } from "msw";
44
import { render, waitFor } from "@/lib/test-utils";
55

66
import { AlertsSummaryMaliciousSecrets } from "../alerts-summary-secrets";
7-
import { makeMockAlert } from "../../mocks/alert.mock";
7+
import { mswEndpoint } from "@/test/msw-endpoint";
8+
import { mockAlert } from "@/mocks/msw/mockers/alert.mock";
89

910
test("shows correct count when there is a secret alert", async () => {
1011
server.use(
11-
http.get("*/api/v1/workspaces/:name/alerts", () => {
12-
return HttpResponse.json([makeMockAlert({ type: "secret" })]);
12+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
13+
return HttpResponse.json([mockAlert({ type: "secret" })]);
1314
}),
1415
);
1516

@@ -22,8 +23,8 @@ test("shows correct count when there is a secret alert", async () => {
2223

2324
test("shows correct count when there is no malicious alert", async () => {
2425
server.use(
25-
http.get("*/api/v1/workspaces/:name/alerts", () => {
26-
return HttpResponse.json([makeMockAlert({ type: "malicious" })]);
26+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
27+
return HttpResponse.json([mockAlert({ type: "malicious" })]);
2728
}),
2829
);
2930

Diff for: src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx

+15-7
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ import { http, HttpResponse } from "msw";
44
import { render, waitFor } from "@/lib/test-utils";
55

66
import { AlertsSummaryWorkspaceTokenUsage } from "../alerts-summary-workspace-token-usage";
7-
import { TOKEN_USAGE_AGG } from "../../mocks/token-usage.mock";
7+
88
import { formatNumberCompact } from "@/lib/format-number";
9+
import { mswEndpoint } from "@/test/msw-endpoint";
10+
import { TOKEN_USAGE_AGG } from "@/mocks/msw/mockers/token-usage.mock";
911

1012
test("shows correct count when there is token usage", async () => {
1113
server.use(
12-
http.get("*/api/v1/workspaces/:name/token-usage", () => {
13-
return HttpResponse.json(TOKEN_USAGE_AGG);
14-
}),
14+
http.get(
15+
mswEndpoint("/api/v1/workspaces/:workspace_name/token-usage"),
16+
() => {
17+
return HttpResponse.json(TOKEN_USAGE_AGG);
18+
},
19+
),
1520
);
1621

1722
const { getByTestId } = render(<AlertsSummaryWorkspaceTokenUsage />);
@@ -28,9 +33,12 @@ test("shows correct count when there is token usage", async () => {
2833

2934
test("shows correct count when there is no token usage", async () => {
3035
server.use(
31-
http.get("*/api/v1/workspaces/:name/token-usage", () => {
32-
return HttpResponse.json({});
33-
}),
36+
http.get(
37+
mswEndpoint("/api/v1/workspaces/:workspace_name/token-usage"),
38+
() => {
39+
return HttpResponse.json({});
40+
},
41+
),
3442
);
3543

3644
const { getByTestId } = render(<AlertsSummaryWorkspaceTokenUsage />);

Diff for: src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx

+22-24
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { server } from "@/mocks/msw/node";
44
import { emptyStateStrings } from "../../constants/strings";
55
import { useSearchParams } from "react-router-dom";
66
import { delay, http, HttpHandler, HttpResponse } from "msw";
7-
import { makeMockAlert } from "../../mocks/alert.mock";
7+
88
import { AlertsFilterView } from "../../hooks/use-alerts-filter-search-params";
99
import { TableAlerts } from "../table-alerts";
1010
import { hrefs } from "@/lib/hrefs";
11+
import { mswEndpoint } from "@/test/msw-endpoint";
12+
import { mockAlert } from "@/mocks/msw/mockers/alert.mock";
1113

1214
enum IllustrationTestId {
1315
ALERT = "illustration-alert",
@@ -78,7 +80,7 @@ const TEST_CASES: TestCase[] = [
7880
{
7981
testDescription: "Loading state",
8082
handlers: [
81-
http.get("*/api/v1/workspaces", () => {
83+
http.get(mswEndpoint("/api/v1/workspaces"), () => {
8284
delay("infinite");
8385
}),
8486
],
@@ -96,7 +98,7 @@ const TEST_CASES: TestCase[] = [
9698
{
9799
testDescription: "Only 1 workspace, no alerts",
98100
handlers: [
99-
http.get("*/api/v1/workspaces", () => {
101+
http.get(mswEndpoint("/api/v1/workspaces"), () => {
100102
return HttpResponse.json({
101103
workspaces: [
102104
{
@@ -106,12 +108,12 @@ const TEST_CASES: TestCase[] = [
106108
],
107109
});
108110
}),
109-
http.get("*/api/v1/workspaces/archive", () => {
111+
http.get(mswEndpoint("/api/v1/workspaces/archive"), () => {
110112
return HttpResponse.json({
111113
workspaces: [],
112114
});
113115
}),
114-
http.get("*/api/v1/workspaces/:name/alerts", () => {
116+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
115117
return HttpResponse.json([]);
116118
}),
117119
],
@@ -135,7 +137,7 @@ const TEST_CASES: TestCase[] = [
135137
{
136138
testDescription: "No search results",
137139
handlers: [
138-
http.get("*/api/v1/workspaces", () => {
140+
http.get(mswEndpoint("/api/v1/workspaces"), () => {
139141
return HttpResponse.json({
140142
workspaces: [
141143
{
@@ -145,16 +147,14 @@ const TEST_CASES: TestCase[] = [
145147
],
146148
});
147149
}),
148-
http.get("*/api/v1/workspaces/archive", () => {
150+
http.get(mswEndpoint("/api/v1/workspaces/archive"), () => {
149151
return HttpResponse.json({
150152
workspaces: [],
151153
});
152154
}),
153-
http.get("*/api/v1/workspaces/:name/alerts", () => {
155+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
154156
return HttpResponse.json(
155-
Array.from({ length: 10 }, () =>
156-
makeMockAlert({ type: "malicious" }),
157-
),
157+
Array.from({ length: 10 }, () => mockAlert({ type: "malicious" })),
158158
);
159159
}),
160160
],
@@ -174,7 +174,7 @@ const TEST_CASES: TestCase[] = [
174174
{
175175
testDescription: "No alerts, multiple workspaces",
176176
handlers: [
177-
http.get("*/api/v1/workspaces", () => {
177+
http.get(mswEndpoint("/api/v1/workspaces"), () => {
178178
return HttpResponse.json({
179179
workspaces: [
180180
{
@@ -188,12 +188,12 @@ const TEST_CASES: TestCase[] = [
188188
],
189189
});
190190
}),
191-
http.get("*/api/v1/workspaces/archive", () => {
191+
http.get(mswEndpoint("/api/v1/workspaces/archive"), () => {
192192
return HttpResponse.json({
193193
workspaces: [],
194194
});
195195
}),
196-
http.get("*/api/v1/workspaces/:name/alerts", () => {
196+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
197197
return HttpResponse.json([]);
198198
}),
199199
],
@@ -217,7 +217,7 @@ const TEST_CASES: TestCase[] = [
217217
{
218218
testDescription: 'Has alerts, view is "malicious"',
219219
handlers: [
220-
http.get("*/api/v1/workspaces", () => {
220+
http.get(mswEndpoint("/api/v1/workspaces"), () => {
221221
return HttpResponse.json({
222222
workspaces: [
223223
{
@@ -231,16 +231,14 @@ const TEST_CASES: TestCase[] = [
231231
],
232232
});
233233
}),
234-
http.get("*/api/v1/workspaces/archive", () => {
234+
http.get(mswEndpoint("/api/v1/workspaces/archive"), () => {
235235
return HttpResponse.json({
236236
workspaces: [],
237237
});
238238
}),
239-
http.get("*/api/v1/workspaces/:name/alerts", () => {
239+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
240240
return HttpResponse.json(
241-
Array.from({ length: 10 }).map(() =>
242-
makeMockAlert({ type: "secret" }),
243-
),
241+
Array.from({ length: 10 }).map(() => mockAlert({ type: "secret" })),
244242
);
245243
}),
246244
],
@@ -258,7 +256,7 @@ const TEST_CASES: TestCase[] = [
258256
{
259257
testDescription: 'Has alerts, view is "secret"',
260258
handlers: [
261-
http.get("*/api/v1/workspaces", () => {
259+
http.get(mswEndpoint("/api/v1/workspaces"), () => {
262260
return HttpResponse.json({
263261
workspaces: [
264262
{
@@ -272,15 +270,15 @@ const TEST_CASES: TestCase[] = [
272270
],
273271
});
274272
}),
275-
http.get("*/api/v1/workspaces/archive", () => {
273+
http.get(mswEndpoint("/api/v1/workspaces/archive"), () => {
276274
return HttpResponse.json({
277275
workspaces: [],
278276
});
279277
}),
280-
http.get("*/api/v1/workspaces/:name/alerts", () => {
278+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
281279
return HttpResponse.json(
282280
Array.from({ length: 10 }).map(() =>
283-
makeMockAlert({ type: "malicious" }),
281+
mockAlert({ type: "malicious" }),
284282
),
285283
);
286284
}),

Diff for: src/features/alerts/components/__tests__/table-alerts.test.tsx

+18-6
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { TableAlerts } from "../table-alerts";
33
import { render, waitFor } from "@/lib/test-utils";
44
import { server } from "@/mocks/msw/node";
55
import { http, HttpResponse } from "msw";
6-
import { makeMockAlert } from "../../mocks/alert.mock";
7-
import { TOKEN_USAGE_AGG } from "../../mocks/token-usage.mock";
86
import { formatNumberCompact } from "@/lib/format-number";
7+
import { mswEndpoint } from "@/test/msw-endpoint";
8+
import { mockAlert } from "@/mocks/msw/mockers/alert.mock";
9+
import { TOKEN_USAGE_AGG } from "@/mocks/msw/mockers/token-usage.mock";
10+
import { mockConversation } from "@/mocks/msw/mockers/conversation.mock";
911

1012
vi.mock("@untitled-ui/icons-react", async () => {
1113
const original = await vi.importActual<
@@ -28,9 +30,14 @@ const OUTPUT_TOKENS =
2830

2931
test("renders token usage cell correctly", async () => {
3032
server.use(
31-
http.get("*/workspaces/:name/alerts", () => {
33+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
3234
return HttpResponse.json([
33-
makeMockAlert({ token_usage: true, type: "malicious" }),
35+
{
36+
...mockAlert({ type: "malicious" }),
37+
conversation: mockConversation({
38+
withTokenUsage: true,
39+
}),
40+
},
3441
]);
3542
}),
3643
);
@@ -53,9 +60,14 @@ test("renders token usage cell correctly", async () => {
5360

5461
test("renders N/A when token usage is missing", async () => {
5562
server.use(
56-
http.get("*/workspaces/:name/alerts", () => {
63+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
5764
return HttpResponse.json([
58-
makeMockAlert({ token_usage: false, type: "malicious" }),
65+
{
66+
...mockAlert({ type: "malicious" }),
67+
conversation: mockConversation({
68+
withTokenUsage: false,
69+
}),
70+
},
5971
]);
6072
}),
6173
);

Diff for: src/features/alerts/components/__tests__/tabs-alerts.test.tsx

+10-12
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { server } from "@/mocks/msw/node";
22
import { http, HttpResponse } from "msw";
3-
import { makeMockAlert } from "../../mocks/alert.mock";
3+
44
import { render, waitFor } from "@/lib/test-utils";
55
import { TabsAlerts } from "../tabs-alerts";
6+
import { mswEndpoint } from "@/test/msw-endpoint";
7+
import { mockAlert } from "@/mocks/msw/mockers/alert.mock";
68

79
test("shows correct count of all packages", async () => {
810
server.use(
9-
http.get("*/workspaces/:name/alerts", () => {
11+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
1012
return HttpResponse.json([
13+
...Array.from({ length: 13 }).map(() => mockAlert({ type: "secret" })),
1114
...Array.from({ length: 13 }).map(() =>
12-
makeMockAlert({ type: "secret" }),
13-
),
14-
...Array.from({ length: 13 }).map(() =>
15-
makeMockAlert({ type: "malicious" }),
15+
mockAlert({ type: "malicious" }),
1616
),
1717
]);
1818
}),
@@ -31,11 +31,9 @@ test("shows correct count of all packages", async () => {
3131

3232
test("shows correct count of malicious packages", async () => {
3333
server.use(
34-
http.get("*/workspaces/:name/alerts", () => {
34+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
3535
return HttpResponse.json(
36-
Array.from({ length: 13 }).map(() =>
37-
makeMockAlert({ type: "malicious" }),
38-
),
36+
Array.from({ length: 13 }).map(() => mockAlert({ type: "malicious" })),
3937
);
4038
}),
4139
);
@@ -53,9 +51,9 @@ test("shows correct count of malicious packages", async () => {
5351

5452
test("shows correct count of secret packages", async () => {
5553
server.use(
56-
http.get("*/workspaces/:name/alerts", () => {
54+
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
5755
return HttpResponse.json(
58-
Array.from({ length: 13 }).map(() => makeMockAlert({ type: "secret" })),
56+
Array.from({ length: 13 }).map(() => mockAlert({ type: "secret" })),
5957
);
6058
}),
6159
);
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { test, expect } from "vitest";
22
import { isAlertMalicious } from "../is-alert-malicious";
3-
import { makeMockAlert } from "../../mocks/alert.mock";
3+
import { mockAlert } from "@/mocks/msw/mockers/alert.mock";
44

55
test("matches malicious alert", () => {
6-
expect(isAlertMalicious(makeMockAlert({ type: "malicious" }))).toBe(true);
6+
expect(isAlertMalicious(mockAlert({ type: "malicious" }))).toBe(true);
77
});
88

99
test("doesn't match secret", () => {
10-
expect(isAlertMalicious(makeMockAlert({ type: "secret" }))).toBe(false);
10+
expect(isAlertMalicious(mockAlert({ type: "secret" }))).toBe(false);
1111
});

0 commit comments

Comments
 (0)