Skip to content

chore: type-safe msw endpoint helper #288

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ export default tseslint.config(
message:
"Do not directly call `invalidateQueries`. Instead, use the `invalidateQueries` helper function.",
},
{
selector: [
"CallExpression[callee.object.name='http'][callee.property.name='all'] > Literal:first-child",
"CallExpression[callee.object.name='http'][callee.property.name='head'] > Literal:first-child",
"CallExpression[callee.object.name='http'][callee.property.name='get'] > Literal:first-child",
"CallExpression[callee.object.name='http'][callee.property.name='post'] > Literal:first-child",
"CallExpression[callee.object.name='http'][callee.property.name='put'] > Literal:first-child",
"CallExpression[callee.object.name='http'][callee.property.name='delete'] > Literal:first-child",
"CallExpression[callee.object.name='http'][callee.property.name='patch'] > Literal:first-child",
"CallExpression[callee.object.name='http'][callee.property.name='options'] > Literal:first-child",
].join(", "),
message:
"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.",
},
],
"no-restricted-imports": [
"error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { test } from "vitest";
import { http, HttpResponse } from "msw";
import { render, waitFor } from "@/lib/test-utils";
import { AlertsSummaryMaliciousPkg } from "../alerts-summary-malicious-pkg";
import { makeMockAlert } from "../../mocks/alert.mock";

import { mswEndpoint } from "@/test/msw-endpoint";
import { mockAlert } from "@/mocks/msw/mockers/alert.mock";

test("shows correct count when there is a malicious alert", async () => {
server.use(
http.get("*/api/v1/workspaces/:name/alerts", () => {
return HttpResponse.json([makeMockAlert({ type: "malicious" })]);
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json([mockAlert({ type: "malicious" })]);
}),
);

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { http, HttpResponse } from "msw";
import { render, waitFor } from "@/lib/test-utils";

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

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

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import { http, HttpResponse } from "msw";
import { render, waitFor } from "@/lib/test-utils";

import { AlertsSummaryWorkspaceTokenUsage } from "../alerts-summary-workspace-token-usage";
import { TOKEN_USAGE_AGG } from "../../mocks/token-usage.mock";

import { formatNumberCompact } from "@/lib/format-number";
import { mswEndpoint } from "@/test/msw-endpoint";
import { TOKEN_USAGE_AGG } from "@/mocks/msw/mockers/token-usage.mock";

test("shows correct count when there is token usage", async () => {
server.use(
http.get("*/api/v1/workspaces/:name/token-usage", () => {
return HttpResponse.json(TOKEN_USAGE_AGG);
}),
http.get(
mswEndpoint("/api/v1/workspaces/:workspace_name/token-usage"),
() => {
return HttpResponse.json(TOKEN_USAGE_AGG);
},
),
);

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

test("shows correct count when there is no token usage", async () => {
server.use(
http.get("*/api/v1/workspaces/:name/token-usage", () => {
return HttpResponse.json({});
}),
http.get(
mswEndpoint("/api/v1/workspaces/:workspace_name/token-usage"),
() => {
return HttpResponse.json({});
},
),
);

const { getByTestId } = render(<AlertsSummaryWorkspaceTokenUsage />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { server } from "@/mocks/msw/node";
import { emptyStateStrings } from "../../constants/strings";
import { useSearchParams } from "react-router-dom";
import { delay, http, HttpHandler, HttpResponse } from "msw";
import { makeMockAlert } from "../../mocks/alert.mock";

import { AlertsFilterView } from "../../hooks/use-alerts-filter-search-params";
import { TableAlerts } from "../table-alerts";
import { hrefs } from "@/lib/hrefs";
import { mswEndpoint } from "@/test/msw-endpoint";
import { mockAlert } from "@/mocks/msw/mockers/alert.mock";

enum IllustrationTestId {
ALERT = "illustration-alert",
Expand Down Expand Up @@ -78,7 +80,7 @@ const TEST_CASES: TestCase[] = [
{
testDescription: "Loading state",
handlers: [
http.get("*/api/v1/workspaces", () => {
http.get(mswEndpoint("/api/v1/workspaces"), () => {
delay("infinite");
}),
],
Expand All @@ -96,7 +98,7 @@ const TEST_CASES: TestCase[] = [
{
testDescription: "Only 1 workspace, no alerts",
handlers: [
http.get("*/api/v1/workspaces", () => {
http.get(mswEndpoint("/api/v1/workspaces"), () => {
return HttpResponse.json({
workspaces: [
{
Expand All @@ -106,12 +108,12 @@ const TEST_CASES: TestCase[] = [
],
});
}),
http.get("*/api/v1/workspaces/archive", () => {
http.get(mswEndpoint("/api/v1/workspaces/archive"), () => {
return HttpResponse.json({
workspaces: [],
});
}),
http.get("*/api/v1/workspaces/:name/alerts", () => {
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json([]);
}),
],
Expand All @@ -135,7 +137,7 @@ const TEST_CASES: TestCase[] = [
{
testDescription: "No search results",
handlers: [
http.get("*/api/v1/workspaces", () => {
http.get(mswEndpoint("/api/v1/workspaces"), () => {
return HttpResponse.json({
workspaces: [
{
Expand All @@ -145,16 +147,14 @@ const TEST_CASES: TestCase[] = [
],
});
}),
http.get("*/api/v1/workspaces/archive", () => {
http.get(mswEndpoint("/api/v1/workspaces/archive"), () => {
return HttpResponse.json({
workspaces: [],
});
}),
http.get("*/api/v1/workspaces/:name/alerts", () => {
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json(
Array.from({ length: 10 }, () =>
makeMockAlert({ type: "malicious" }),
),
Array.from({ length: 10 }, () => mockAlert({ type: "malicious" })),
);
}),
],
Expand All @@ -174,7 +174,7 @@ const TEST_CASES: TestCase[] = [
{
testDescription: "No alerts, multiple workspaces",
handlers: [
http.get("*/api/v1/workspaces", () => {
http.get(mswEndpoint("/api/v1/workspaces"), () => {
return HttpResponse.json({
workspaces: [
{
Expand All @@ -188,12 +188,12 @@ const TEST_CASES: TestCase[] = [
],
});
}),
http.get("*/api/v1/workspaces/archive", () => {
http.get(mswEndpoint("/api/v1/workspaces/archive"), () => {
return HttpResponse.json({
workspaces: [],
});
}),
http.get("*/api/v1/workspaces/:name/alerts", () => {
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json([]);
}),
],
Expand All @@ -217,7 +217,7 @@ const TEST_CASES: TestCase[] = [
{
testDescription: 'Has alerts, view is "malicious"',
handlers: [
http.get("*/api/v1/workspaces", () => {
http.get(mswEndpoint("/api/v1/workspaces"), () => {
return HttpResponse.json({
workspaces: [
{
Expand All @@ -231,16 +231,14 @@ const TEST_CASES: TestCase[] = [
],
});
}),
http.get("*/api/v1/workspaces/archive", () => {
http.get(mswEndpoint("/api/v1/workspaces/archive"), () => {
return HttpResponse.json({
workspaces: [],
});
}),
http.get("*/api/v1/workspaces/:name/alerts", () => {
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json(
Array.from({ length: 10 }).map(() =>
makeMockAlert({ type: "secret" }),
),
Array.from({ length: 10 }).map(() => mockAlert({ type: "secret" })),
);
}),
],
Expand All @@ -258,7 +256,7 @@ const TEST_CASES: TestCase[] = [
{
testDescription: 'Has alerts, view is "secret"',
handlers: [
http.get("*/api/v1/workspaces", () => {
http.get(mswEndpoint("/api/v1/workspaces"), () => {
return HttpResponse.json({
workspaces: [
{
Expand All @@ -272,15 +270,15 @@ const TEST_CASES: TestCase[] = [
],
});
}),
http.get("*/api/v1/workspaces/archive", () => {
http.get(mswEndpoint("/api/v1/workspaces/archive"), () => {
return HttpResponse.json({
workspaces: [],
});
}),
http.get("*/api/v1/workspaces/:name/alerts", () => {
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json(
Array.from({ length: 10 }).map(() =>
makeMockAlert({ type: "malicious" }),
mockAlert({ type: "malicious" }),
),
);
}),
Expand Down
24 changes: 18 additions & 6 deletions src/features/alerts/components/__tests__/table-alerts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { TableAlerts } from "../table-alerts";
import { render, waitFor } from "@/lib/test-utils";
import { server } from "@/mocks/msw/node";
import { http, HttpResponse } from "msw";
import { makeMockAlert } from "../../mocks/alert.mock";
import { TOKEN_USAGE_AGG } from "../../mocks/token-usage.mock";
import { formatNumberCompact } from "@/lib/format-number";
import { mswEndpoint } from "@/test/msw-endpoint";
import { mockAlert } from "@/mocks/msw/mockers/alert.mock";
import { TOKEN_USAGE_AGG } from "@/mocks/msw/mockers/token-usage.mock";
import { mockConversation } from "@/mocks/msw/mockers/conversation.mock";

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

test("renders token usage cell correctly", async () => {
server.use(
http.get("*/workspaces/:name/alerts", () => {
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json([
makeMockAlert({ token_usage: true, type: "malicious" }),
{
...mockAlert({ type: "malicious" }),
conversation: mockConversation({
withTokenUsage: true,
}),
},
]);
}),
);
Expand All @@ -53,9 +60,14 @@ test("renders token usage cell correctly", async () => {

test("renders N/A when token usage is missing", async () => {
server.use(
http.get("*/workspaces/:name/alerts", () => {
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json([
makeMockAlert({ token_usage: false, type: "malicious" }),
{
...mockAlert({ type: "malicious" }),
conversation: mockConversation({
withTokenUsage: false,
}),
},
]);
}),
);
Expand Down
22 changes: 10 additions & 12 deletions src/features/alerts/components/__tests__/tabs-alerts.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { server } from "@/mocks/msw/node";
import { http, HttpResponse } from "msw";
import { makeMockAlert } from "../../mocks/alert.mock";

import { render, waitFor } from "@/lib/test-utils";
import { TabsAlerts } from "../tabs-alerts";
import { mswEndpoint } from "@/test/msw-endpoint";
import { mockAlert } from "@/mocks/msw/mockers/alert.mock";

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

test("shows correct count of malicious packages", async () => {
server.use(
http.get("*/workspaces/:name/alerts", () => {
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json(
Array.from({ length: 13 }).map(() =>
makeMockAlert({ type: "malicious" }),
),
Array.from({ length: 13 }).map(() => mockAlert({ type: "malicious" })),
);
}),
);
Expand All @@ -53,9 +51,9 @@ test("shows correct count of malicious packages", async () => {

test("shows correct count of secret packages", async () => {
server.use(
http.get("*/workspaces/:name/alerts", () => {
http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json(
Array.from({ length: 13 }).map(() => makeMockAlert({ type: "secret" })),
Array.from({ length: 13 }).map(() => mockAlert({ type: "secret" })),
);
}),
);
Expand Down
6 changes: 3 additions & 3 deletions src/features/alerts/lib/__tests__/is-alert-malicious.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { test, expect } from "vitest";
import { isAlertMalicious } from "../is-alert-malicious";
import { makeMockAlert } from "../../mocks/alert.mock";
import { mockAlert } from "@/mocks/msw/mockers/alert.mock";

test("matches malicious alert", () => {
expect(isAlertMalicious(makeMockAlert({ type: "malicious" }))).toBe(true);
expect(isAlertMalicious(mockAlert({ type: "malicious" }))).toBe(true);
});

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