diff --git a/eslint.config.js b/eslint.config.js
index 2af2f2e0..3dca070a 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -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",
diff --git a/src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx b/src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx
index ed1d728a..44a4c7b1 100644
--- a/src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx
+++ b/src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx
@@ -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" })]);
}),
);
@@ -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" })]);
}),
);
diff --git a/src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx b/src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx
index 5a99655b..9e3c2484 100644
--- a/src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx
+++ b/src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx
@@ -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" })]);
}),
);
@@ -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" })]);
}),
);
diff --git a/src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx b/src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx
index 56466a27..02138190 100644
--- a/src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx
+++ b/src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx
@@ -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();
@@ -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();
diff --git a/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx b/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx
index c6ce31f4..1db06c0a 100644
--- a/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx
+++ b/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx
@@ -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",
@@ -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");
}),
],
@@ -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: [
{
@@ -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([]);
}),
],
@@ -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: [
{
@@ -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" })),
);
}),
],
@@ -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: [
{
@@ -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([]);
}),
],
@@ -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: [
{
@@ -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" })),
);
}),
],
@@ -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: [
{
@@ -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" }),
),
);
}),
diff --git a/src/features/alerts/components/__tests__/table-alerts.test.tsx b/src/features/alerts/components/__tests__/table-alerts.test.tsx
index 905f31af..404932dc 100644
--- a/src/features/alerts/components/__tests__/table-alerts.test.tsx
+++ b/src/features/alerts/components/__tests__/table-alerts.test.tsx
@@ -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<
@@ -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,
+ }),
+ },
]);
}),
);
@@ -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,
+ }),
+ },
]);
}),
);
diff --git a/src/features/alerts/components/__tests__/tabs-alerts.test.tsx b/src/features/alerts/components/__tests__/tabs-alerts.test.tsx
index 913647d4..da4cd1d6 100644
--- a/src/features/alerts/components/__tests__/tabs-alerts.test.tsx
+++ b/src/features/alerts/components/__tests__/tabs-alerts.test.tsx
@@ -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" }),
),
]);
}),
@@ -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" })),
);
}),
);
@@ -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" })),
);
}),
);
diff --git a/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts b/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts
index b41a9505..dfcd8b3f 100644
--- a/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts
+++ b/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts
@@ -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);
});
diff --git a/src/features/alerts/lib/__tests__/is-alert-secret.test.ts b/src/features/alerts/lib/__tests__/is-alert-secret.test.ts
index 88d32bfa..16f70071 100644
--- a/src/features/alerts/lib/__tests__/is-alert-secret.test.ts
+++ b/src/features/alerts/lib/__tests__/is-alert-secret.test.ts
@@ -1,11 +1,11 @@
import { test, expect } from "vitest";
import { isAlertSecret } from "../is-alert-secret";
-import { makeMockAlert } from "../../mocks/alert.mock";
+import { mockAlert } from "@/mocks/msw/mockers/alert.mock";
test("matches secret alert", () => {
- expect(isAlertSecret(makeMockAlert({ type: "secret" }))).toBe(true);
+ expect(isAlertSecret(mockAlert({ type: "secret" }))).toBe(true);
});
test("doesn't match malicious", () => {
- expect(isAlertSecret(makeMockAlert({ type: "malicious" }))).toBe(false);
+ expect(isAlertSecret(mockAlert({ type: "malicious" }))).toBe(false);
});
diff --git a/src/features/alerts/lib/is-alert-malicious.ts b/src/features/alerts/lib/is-alert-malicious.ts
index 2c9e10ed..457c36e4 100644
--- a/src/features/alerts/lib/is-alert-malicious.ts
+++ b/src/features/alerts/lib/is-alert-malicious.ts
@@ -1,7 +1,7 @@
-import { AlertConversation } from "@/api/generated";
+import { Alert, AlertConversation } from "@/api/generated";
export function isAlertMalicious(
- alert: AlertConversation | null,
+ alert: Alert | AlertConversation | null,
): alert is AlertConversation {
return (
alert?.trigger_category === "critical" &&
diff --git a/src/features/alerts/lib/is-alert-secret.ts b/src/features/alerts/lib/is-alert-secret.ts
index 9e7fd87a..7acc6612 100644
--- a/src/features/alerts/lib/is-alert-secret.ts
+++ b/src/features/alerts/lib/is-alert-secret.ts
@@ -1,6 +1,6 @@
-import { V1GetWorkspaceAlertsResponse } from "@/api/generated";
+import { Alert, AlertConversation } from "@/api/generated";
-export function isAlertSecret(alert: V1GetWorkspaceAlertsResponse[number]) {
+export function isAlertSecret(alert: Alert | AlertConversation | null) {
return (
alert?.trigger_category === "critical" &&
alert.trigger_type === "codegate-secrets"
diff --git a/src/features/alerts/mocks/alert.mock.ts b/src/features/alerts/mocks/alert.mock.ts
deleted file mode 100644
index 945b6aef..00000000
--- a/src/features/alerts/mocks/alert.mock.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import {
- AlertConversation,
- QuestionType,
- TokenUsageAggregate,
-} from "@/api/generated";
-import { faker } from "@faker-js/faker";
-import { TOKEN_USAGE_AGG } from "./token-usage.mock";
-
-export const ALERT_SECRET_FIELDS = {
- trigger_string: "foo",
- trigger_type: "codegate-secrets",
-} satisfies Pick;
-
-export const ALERT_MALICIOUS_FIELDS = {
- trigger_string: {
- name: "invokehttp",
- type: "pypi",
- status: "malicious",
- description: "Python HTTP for Humans.",
- },
- trigger_type: "codegate-context-retriever",
-} satisfies Pick;
-
-const getBaseAlert = ({
- timestamp,
- token_usage_agg,
-}: {
- timestamp: string;
- token_usage_agg: TokenUsageAggregate | null;
-}): Omit => ({
- conversation: {
- question_answers: [
- {
- question: {
- message: "foo",
- timestamp: timestamp,
- message_id: faker.string.uuid(),
- },
- answer: {
- message: "bar",
- timestamp: timestamp,
- message_id: faker.string.uuid(),
- },
- },
- ],
- provider: "anthropic",
- type: QuestionType.CHAT,
- chat_id: faker.string.uuid(),
- conversation_timestamp: timestamp,
- token_usage_agg,
- },
- alert_id: faker.string.uuid(),
- code_snippet: null,
- trigger_category: "critical",
- timestamp: timestamp,
-});
-
-export const makeMockAlert = ({
- token_usage = false,
- type,
-}: {
- token_usage?: boolean;
- type: "secret" | "malicious";
-}): AlertConversation => {
- const timestamp = faker.date.recent().toUTCString();
-
- const base: Omit =
- getBaseAlert({
- timestamp,
- token_usage_agg: token_usage ? TOKEN_USAGE_AGG : null,
- });
-
- switch (type) {
- case "malicious": {
- const result: AlertConversation = {
- ...base,
- ...ALERT_MALICIOUS_FIELDS,
- };
-
- return result;
- }
- case "secret": {
- const result: AlertConversation = {
- ...base,
- ...ALERT_SECRET_FIELDS,
- };
-
- return result;
- }
- }
-};
diff --git a/src/features/header/components/__tests__/header-status-menu.test.tsx b/src/features/header/components/__tests__/header-status-menu.test.tsx
index 154109e0..a957694c 100644
--- a/src/features/header/components/__tests__/header-status-menu.test.tsx
+++ b/src/features/header/components/__tests__/header-status-menu.test.tsx
@@ -5,13 +5,16 @@ import { expect } from "vitest";
import { render, waitFor } from "@/lib/test-utils";
import { HeaderStatusMenu } from "../header-status-menu";
import userEvent from "@testing-library/user-event";
+import { mswEndpoint } from "@/test/msw-endpoint";
const renderComponent = () => render();
describe("CardCodegateStatus", () => {
test("renders 'healthy' state", async () => {
server.use(
- http.get("*/health", () => HttpResponse.json({ status: "healthy" })),
+ http.get(mswEndpoint("/health"), () =>
+ HttpResponse.json({ status: "healthy" }),
+ ),
);
const { getByRole } = renderComponent();
@@ -22,7 +25,11 @@ describe("CardCodegateStatus", () => {
});
test("renders 'unhealthy' state", async () => {
- server.use(http.get("*/health", () => HttpResponse.json({ status: null })));
+ server.use(
+ http.get(mswEndpoint("/health"), () =>
+ HttpResponse.json({ status: null }),
+ ),
+ );
const { getByRole } = renderComponent();
@@ -32,7 +39,7 @@ describe("CardCodegateStatus", () => {
});
test("renders 'error' state when health check request fails", async () => {
- server.use(http.get("*/health", () => HttpResponse.error()));
+ server.use(http.get(mswEndpoint("/health"), () => HttpResponse.error()));
const { getByRole } = renderComponent();
@@ -43,8 +50,10 @@ describe("CardCodegateStatus", () => {
test("renders 'error' state when version check request fails", async () => {
server.use(
- http.get("*/health", () => HttpResponse.json({ status: "healthy" })),
- http.get("*/api/v1/version", () => HttpResponse.error()),
+ http.get(mswEndpoint("/health"), () =>
+ HttpResponse.json({ status: "healthy" }),
+ ),
+ http.get(mswEndpoint("/api/v1/version"), () => HttpResponse.error()),
);
const { getByRole } = renderComponent();
@@ -56,8 +65,10 @@ describe("CardCodegateStatus", () => {
test("renders 'up to date' state", async () => {
server.use(
- http.get("*/health", () => HttpResponse.json({ status: "healthy" })),
- http.get("*/api/v1/version", () =>
+ http.get(mswEndpoint("/health"), () =>
+ HttpResponse.json({ status: "healthy" }),
+ ),
+ http.get(mswEndpoint("/api/v1/version"), () =>
HttpResponse.json({
current_version: "foo",
latest_version: "foo",
@@ -83,8 +94,10 @@ describe("CardCodegateStatus", () => {
test("renders 'update available' state", async () => {
server.use(
- http.get("*/health", () => HttpResponse.json({ status: "healthy" })),
- http.get("*/api/v1/version", () =>
+ http.get(mswEndpoint("/health"), () =>
+ HttpResponse.json({ status: "healthy" }),
+ ),
+ http.get(mswEndpoint("/api/v1/version"), () =>
HttpResponse.json({
current_version: "foo",
latest_version: "bar",
@@ -115,8 +128,10 @@ describe("CardCodegateStatus", () => {
test("renders 'version check error' state", async () => {
server.use(
- http.get("*/health", () => HttpResponse.json({ status: "healthy" })),
- http.get("*/api/v1/version", () =>
+ http.get(mswEndpoint("/health"), () =>
+ HttpResponse.json({ status: "healthy" }),
+ ),
+ http.get(mswEndpoint("/api/v1/version"), () =>
HttpResponse.json({
current_version: "foo",
latest_version: "bar",
diff --git a/src/features/workspace/components/__tests__/archive-workspace.test.tsx b/src/features/workspace/components/__tests__/archive-workspace.test.tsx
index f27d6183..9a6b32de 100644
--- a/src/features/workspace/components/__tests__/archive-workspace.test.tsx
+++ b/src/features/workspace/components/__tests__/archive-workspace.test.tsx
@@ -4,6 +4,7 @@ import userEvent from "@testing-library/user-event";
import { waitFor } from "@testing-library/react";
import { server } from "@/mocks/msw/node";
import { http, HttpResponse } from "msw";
+import { mswEndpoint } from "@/test/msw-endpoint";
test("has correct buttons when not archived", async () => {
const { getByRole, queryByRole } = render(
@@ -66,7 +67,7 @@ test("can permanently delete archived workspace", async () => {
test("can't archive active workspace", async () => {
server.use(
- http.get("*/api/v1/workspaces/active", () =>
+ http.get(mswEndpoint("/api/v1/workspaces/active"), () =>
HttpResponse.json({
workspaces: [
{
diff --git a/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx b/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx
index cf63e5fb..b9d83c1f 100644
--- a/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx
+++ b/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx
@@ -5,6 +5,7 @@ import userEvent from "@testing-library/user-event";
import { server } from "@/mocks/msw/node";
import { http, HttpResponse } from "msw";
import { WorkspaceCustomInstructions } from "../workspace-custom-instructions";
+import { mswEndpoint } from "@/test/msw-endpoint";
vi.mock("@monaco-editor/react", () => {
const FakeEditor = vi.fn((props) => {
@@ -26,9 +27,12 @@ const renderComponent = () =>
test("can update custom instructions", async () => {
server.use(
- http.get("*/api/v1/workspaces/:name/custom-instructions", () => {
- return HttpResponse.json({ prompt: "initial prompt from server" });
- }),
+ http.get(
+ mswEndpoint("/api/v1/workspaces/:workspace_name/custom-instructions"),
+ () => {
+ return HttpResponse.json({ prompt: "initial prompt from server" });
+ },
+ ),
);
const { getByRole, getByText } = renderComponent();
@@ -45,9 +49,12 @@ test("can update custom instructions", async () => {
expect(input).toHaveTextContent("new prompt from test");
server.use(
- http.get("*/api/v1/workspaces/:name/custom-instructions", () => {
- return HttpResponse.json({ prompt: "new prompt from test" });
- }),
+ http.get(
+ mswEndpoint("/api/v1/workspaces/:workspace_name/custom-instructions"),
+ () => {
+ return HttpResponse.json({ prompt: "new prompt from test" });
+ },
+ ),
);
await userEvent.click(getByRole("button", { name: /Save/i }));
diff --git a/src/features/workspace/components/__tests__/workspace-name.test.tsx b/src/features/workspace/components/__tests__/workspace-name.test.tsx
index d92b25b4..84bfc93f 100644
--- a/src/features/workspace/components/__tests__/workspace-name.test.tsx
+++ b/src/features/workspace/components/__tests__/workspace-name.test.tsx
@@ -4,6 +4,7 @@ import { render, waitFor } from "@/lib/test-utils";
import userEvent from "@testing-library/user-event";
import { server } from "@/mocks/msw/node";
import { http, HttpResponse } from "msw";
+import { mswEndpoint } from "@/test/msw-endpoint";
test("can rename workspace", async () => {
const { getByRole, getByText } = render(
@@ -34,7 +35,7 @@ test("can't rename archived workspace", async () => {
test("can't rename active workspace", async () => {
server.use(
- http.get("*/api/v1/workspaces/active", () =>
+ http.get(mswEndpoint("/api/v1/workspaces/active"), () =>
HttpResponse.json({
workspaces: [
{
diff --git a/src/mocks/msw/handlers.ts b/src/mocks/msw/handlers.ts
index 2a4974cb..a852dc8f 100644
--- a/src/mocks/msw/handlers.ts
+++ b/src/mocks/msw/handlers.ts
@@ -1,22 +1,25 @@
import { http, HttpResponse } from "msw";
-import mockedPrompts from "@/mocks/msw/fixtures/GET_MESSAGES.json";
import mockedAlerts from "@/mocks/msw/fixtures/GET_ALERTS.json";
import mockedWorkspaces from "@/mocks/msw/fixtures/GET_WORKSPACES.json";
import mockedProviders from "@/mocks/msw/fixtures/GET_PROVIDERS.json";
import mockedProvidersModels from "@/mocks/msw/fixtures/GET_PROVIDERS_MODELS.json";
import { ProviderType } from "@/api/generated";
+import { mockConversation } from "./mockers/conversation.mock";
+import { mswEndpoint } from "@/test/msw-endpoint";
export const handlers = [
- http.get("*/health", () =>
+ http.get(mswEndpoint("/health"), () =>
HttpResponse.json({
current_version: "foo",
latest_version: "bar",
is_latest: false,
error: null,
- }),
+ })
),
- http.get("*/api/v1/version", () => HttpResponse.json({ status: "healthy" })),
- http.get("*/api/v1/workspaces/active", () =>
+ http.get(mswEndpoint("/api/v1/version"), () =>
+ HttpResponse.json({ status: "healthy" })
+ ),
+ http.get(mswEndpoint("/api/v1/workspaces/active"), () =>
HttpResponse.json({
workspaces: [
{
@@ -25,18 +28,20 @@ export const handlers = [
last_updated: new Date(Date.now()).toISOString(),
},
],
- }),
+ })
),
- http.get("*/api/v1/workspaces/:name/messages", () => {
- return HttpResponse.json(mockedPrompts);
+ http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => {
+ return HttpResponse.json(
+ Array.from({ length: 10 }).map(() => mockConversation())
+ );
}),
- http.get("*/api/v1/workspaces/:name/alerts", () => {
+ http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json(mockedAlerts);
}),
- http.get("*/api/v1/workspaces", () => {
+ http.get(mswEndpoint("/api/v1/workspaces"), () => {
return HttpResponse.json(mockedWorkspaces);
}),
- http.get("*/api/v1/workspaces/archive", () => {
+ http.get(mswEndpoint("/api/v1/workspaces/archive"), () => {
return HttpResponse.json({
workspaces: [
{
@@ -46,55 +51,61 @@ export const handlers = [
],
});
}),
- http.post("*/api/v1/workspaces", () => {
+ http.post(mswEndpoint("/api/v1/workspaces"), () => {
return HttpResponse.json(mockedWorkspaces);
}),
http.post(
- "*/api/v1/workspaces/active",
- () => new HttpResponse(null, { status: 204 }),
+ mswEndpoint("/api/v1/workspaces/active"),
+ () => new HttpResponse(null, { status: 204 })
),
http.post(
- "*/api/v1/workspaces/archive/:workspace_name/recover",
- () => new HttpResponse(null, { status: 204 }),
+ mswEndpoint("/api/v1/workspaces/archive/:workspace_name/recover"),
+ () => new HttpResponse(null, { status: 204 })
),
http.delete(
- "*/api/v1/workspaces/:name",
- () => new HttpResponse(null, { status: 204 }),
+ mswEndpoint("/api/v1/workspaces/:workspace_name"),
+ () => new HttpResponse(null, { status: 204 })
),
http.delete(
- "*/api/v1/workspaces/archive/:name",
- () => new HttpResponse(null, { status: 204 }),
- ),
- http.get("*/api/v1/workspaces/:name/custom-instructions", () => {
- return HttpResponse.json({ prompt: "foo" });
- }),
- http.get("*/api/v1/workspaces/:name/token-usage", () => {
- return HttpResponse.json({
- tokens_by_model: {
- "claude-3-5-sonnet-latest": {
- provider_type: ProviderType.ANTHROPIC,
- model: "claude-3-5-sonnet-latest",
- token_usage: {
- input_tokens: 1183,
- output_tokens: 433,
- input_cost: 0.003549,
- output_cost: 0.006495,
+ mswEndpoint("/api/v1/workspaces/archive/:workspace_name"),
+ () => new HttpResponse(null, { status: 204 })
+ ),
+ http.get(
+ mswEndpoint("/api/v1/workspaces/:workspace_name/custom-instructions"),
+ () => {
+ return HttpResponse.json({ prompt: "foo" });
+ }
+ ),
+ http.get(
+ mswEndpoint("/api/v1/workspaces/:workspace_name/token-usage"),
+ () => {
+ return HttpResponse.json({
+ tokens_by_model: {
+ "claude-3-5-sonnet-latest": {
+ provider_type: ProviderType.ANTHROPIC,
+ model: "claude-3-5-sonnet-latest",
+ token_usage: {
+ input_tokens: 1183,
+ output_tokens: 433,
+ input_cost: 0.003549,
+ output_cost: 0.006495,
+ },
},
},
- },
- token_usage: {
- input_tokens: 1183,
- output_tokens: 433,
- input_cost: 0.003549,
- output_cost: 0.006495,
- },
- });
- }),
+ token_usage: {
+ input_tokens: 1183,
+ output_tokens: 433,
+ input_cost: 0.003549,
+ output_cost: 0.006495,
+ },
+ });
+ }
+ ),
http.put(
- "*/api/v1/workspaces/:name/custom-instructions",
- () => new HttpResponse(null, { status: 204 }),
+ mswEndpoint("/api/v1/workspaces/:workspace_name/custom-instructions"),
+ () => new HttpResponse(null, { status: 204 })
),
- http.get("*/api/v1/workspaces/:workspace_name/muxes", () =>
+ http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/muxes"), () =>
HttpResponse.json([
{
provider_id: "openai",
@@ -107,34 +118,34 @@ export const handlers = [
model: "davinci",
matcher_type: "catch_all",
},
- ]),
+ ])
),
http.put(
- "*/api/v1/workspaces/:workspace_name/muxes",
- () => new HttpResponse(null, { status: 204 }),
+ mswEndpoint("/api/v1/workspaces/:workspace_name/muxes"),
+ () => new HttpResponse(null, { status: 204 })
),
- http.get("*/api/v1/provider-endpoints/:provider_name/models", () =>
- HttpResponse.json(mockedProvidersModels),
+ http.get(mswEndpoint("/api/v1/provider-endpoints/:provider_id/models"), () =>
+ HttpResponse.json(mockedProvidersModels)
),
- http.get("*/api/v1/provider-endpoints/models", () =>
- HttpResponse.json(mockedProvidersModels),
+ http.get(mswEndpoint("/api/v1/provider-endpoints/models"), () =>
+ HttpResponse.json(mockedProvidersModels)
),
- http.get("*/api/v1/provider-endpoints/:provider_id", () =>
- HttpResponse.json(mockedProviders[0]),
+ http.get(mswEndpoint("/api/v1/provider-endpoints/:provider_id"), () =>
+ HttpResponse.json(mockedProviders[0])
),
- http.get("*/api/v1/provider-endpoints", () =>
- HttpResponse.json(mockedProviders),
+ http.get(mswEndpoint("/api/v1/provider-endpoints"), () =>
+ HttpResponse.json(mockedProviders)
),
http.post(
- "*/api/v1/provider-endpoints",
- () => new HttpResponse(null, { status: 204 }),
+ mswEndpoint("/api/v1/provider-endpoints"),
+ () => new HttpResponse(null, { status: 204 })
),
http.put(
- "*/api/v1/provider-endpoints",
- () => new HttpResponse(null, { status: 204 }),
+ mswEndpoint("/api/v1/provider-endpoints"),
+ () => new HttpResponse(null, { status: 204 })
),
http.delete(
- "*/api/v1/provider-endpoints",
- () => new HttpResponse(null, { status: 204 }),
+ mswEndpoint("/api/v1/provider-endpoints"),
+ () => new HttpResponse(null, { status: 204 })
),
];
diff --git a/src/mocks/msw/mockers/alert.mock.ts b/src/mocks/msw/mockers/alert.mock.ts
new file mode 100644
index 00000000..7c96ca03
--- /dev/null
+++ b/src/mocks/msw/mockers/alert.mock.ts
@@ -0,0 +1,60 @@
+import { Alert } from "@/api/generated";
+import { faker } from "@faker-js/faker";
+
+const ALERT_SECRET_FIELDS = {
+ trigger_string: "foo",
+ trigger_type: "codegate-secrets",
+} satisfies Pick;
+
+const ALERT_MALICIOUS_FIELDS = {
+ trigger_string: {
+ name: "invokehttp",
+ type: "pypi",
+ status: "malicious",
+ description: "Python HTTP for Humans.",
+ },
+ trigger_type: "codegate-context-retriever",
+} satisfies Pick;
+
+const getBaseAlert = ({
+ timestamp,
+}: {
+ timestamp: string;
+}): Omit => ({
+ id: faker.string.uuid(),
+ prompt_id: faker.string.uuid(),
+ code_snippet: null,
+ trigger_category: "critical",
+ timestamp: timestamp,
+});
+
+export const mockAlert = ({
+ type,
+}: {
+ type: "secret" | "malicious";
+}): Alert => {
+ const timestamp = faker.date.recent().toISOString();
+
+ const base: Omit = getBaseAlert({
+ timestamp,
+ });
+
+ switch (type) {
+ case "malicious": {
+ const result: Alert = {
+ ...base,
+ ...ALERT_MALICIOUS_FIELDS,
+ };
+
+ return result;
+ }
+ case "secret": {
+ const result: Alert = {
+ ...base,
+ ...ALERT_SECRET_FIELDS,
+ };
+
+ return result;
+ }
+ }
+};
diff --git a/src/mocks/msw/mockers/conversation.mock.ts b/src/mocks/msw/mockers/conversation.mock.ts
new file mode 100644
index 00000000..e0461e68
--- /dev/null
+++ b/src/mocks/msw/mockers/conversation.mock.ts
@@ -0,0 +1,54 @@
+import { Conversation, QuestionType } from "@/api/generated";
+import { faker } from "@faker-js/faker";
+import { TOKEN_USAGE_AGG } from "./token-usage.mock";
+import { mockAlert } from "./alert.mock";
+
+export function mockConversation({
+ type = QuestionType.CHAT,
+ withTokenUsage = true,
+ alertsConfig = {},
+}: {
+ type?: QuestionType;
+ withTokenUsage?: boolean;
+ alertsConfig?: {
+ numAlerts?: number;
+ type?: "secret" | "malicious" | "any";
+ };
+} = {}) {
+ const timestamp = faker.date.recent().toISOString();
+
+ return {
+ question_answers: [
+ {
+ question: {
+ message: faker.lorem.sentence(),
+ timestamp: timestamp,
+ message_id: faker.string.uuid(),
+ },
+ answer: {
+ message: faker.lorem.sentence(),
+ timestamp: timestamp,
+ message_id: faker.string.uuid(),
+ },
+ },
+ ],
+ provider: "vllm",
+ alerts: Array.from({
+ length:
+ typeof alertsConfig?.numAlerts === "number"
+ ? alertsConfig?.numAlerts
+ : faker.number.int({ min: 0, max: 5 }),
+ }).map(() =>
+ mockAlert({
+ type:
+ alertsConfig?.type == null || alertsConfig.type === "any"
+ ? faker.helpers.arrayElement(["secret", "malicious"])
+ : alertsConfig.type,
+ }),
+ ),
+ token_usage_agg: withTokenUsage ? TOKEN_USAGE_AGG : null,
+ type,
+ chat_id: faker.string.uuid(), // NOTE: This isn't a UUID in the API
+ conversation_timestamp: timestamp,
+ } as const satisfies Conversation;
+}
diff --git a/src/features/alerts/mocks/token-usage.mock.ts b/src/mocks/msw/mockers/token-usage.mock.ts
similarity index 100%
rename from src/features/alerts/mocks/token-usage.mock.ts
rename to src/mocks/msw/mockers/token-usage.mock.ts
diff --git a/src/routes/__tests__/route-dashboard.test.tsx b/src/routes/__tests__/route-dashboard.test.tsx
index 0bb5097c..6d422dd4 100644
--- a/src/routes/__tests__/route-dashboard.test.tsx
+++ b/src/routes/__tests__/route-dashboard.test.tsx
@@ -8,6 +8,7 @@ import { HttpResponse, http } from "msw";
import mockedAlerts from "@/mocks/msw/fixtures/GET_ALERTS.json";
import userEvent from "@testing-library/user-event";
import { RouteDashboard } from "../route-dashboard";
+import { mswEndpoint } from "@/test/msw-endpoint";
const fakeConversionation1 = {
conversation: {
@@ -79,7 +80,7 @@ const fakeConversionation2 = {
function mockAlertsWithMaliciousPkg() {
server.use(
- http.get("*/workspaces/:name/alerts", () => {
+ http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json([fakeConversionation1, fakeConversionation2]);
}),
);
@@ -87,7 +88,7 @@ function mockAlertsWithMaliciousPkg() {
function mockManyAlerts() {
server.use(
- http.get("*/workspaces/:name/alerts", () => {
+ http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => {
return HttpResponse.json(
[
...mockedAlerts,
diff --git a/src/test/msw-endpoint.ts b/src/test/msw-endpoint.ts
new file mode 100644
index 00000000..4f6e94a4
--- /dev/null
+++ b/src/test/msw-endpoint.ts
@@ -0,0 +1,25 @@
+import type json from "../api/openapi.json";
+
+/**
+ * OpenAPI spec uses curly braces to denote path parameters
+ * @example
+ * ```
+ * /api/v1/provider-endpoints/{provider_id}/models
+ * ```
+ *
+ * MSW expects a colon prefix for path parameters
+ * @example
+ * ```
+ * /api/v1/provider-endpoints/:provider_id/models
+ * ```
+ */
+type ReplacePathParams =
+ T extends `${infer Start}{${infer Param}}${infer End}`
+ ? `${Start}:${Param}${ReplacePathParams}`
+ : T;
+
+type Endpoint = ReplacePathParams;
+
+export function mswEndpoint(endpoint: Endpoint) {
+ return new URL(endpoint, import.meta.env.VITE_BASE_API_URL).toString();
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 32f4a144..2aa07e67 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1,4 +1,5 @@
///
interface ImportBaseApiEnv {
readonly BASE_API_URL: string;
+ readonly VITE_BASE_API_URL: string;
}