Skip to content
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
183 changes: 183 additions & 0 deletions __tests__/app/admin/security-pages.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";

import SecurityAdvisoriesPage from "@/app/admin/security/advisories/page";
import SecurityAuditLogPage from "@/app/admin/security/audit-log/page";

const mockPush = vi.fn();

vi.mock("next/navigation", () => ({
useRouter: () => ({ push: mockPush }),
useSearchParams: () => new URLSearchParams("org=acme-corp"),
}));

vi.mock("@/lib/auth", () => ({
useAuth: () => ({
token: "test-token",
isAuthenticated: true,
login: vi.fn(),
logout: vi.fn(),
}),
}));

vi.mock("@/components/ui/toast", () => ({
useToast: () => ({
success: vi.fn(),
error: vi.fn(),
}),
}));

function mockAdminFetch() {
return vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);

if (url.endsWith("/api/v3/user")) {
return {
ok: true,
status: 200,
json: async () => ({ login: "alice" }),
};
}

if (url.endsWith("/api/v3/orgs/acme-corp/members")) {
return {
ok: true,
status: 200,
json: async () => [{ login: "alice", role: "admin" }],
};
}

if (url.includes("/api/v3/orgs/acme-corp/security-advisories")) {
return {
ok: true,
status: 200,
json: async () => [
{
id: "1",
ghsa_id: "GHSA-xxxx-yyyy-zzzz",
severity: "high",
summary: "Example advisory",
state: "open",
affected_package: "example-pkg",
repository: {
owner: { login: "acme-corp" },
name: "demo",
},
},
],
};
}

if (url.includes("/api/v3/orgs/acme-corp/audit-log/export")) {
return {
ok: true,
status: 200,
json: async () => ({ job_id: "export-123" }),
};
}

if (
url.includes("/api/v3/orgs/acme-corp/audit-log") &&
!url.includes("/export")
) {
return {
ok: true,
status: 200,
json: async () => [
{
id: "log-1",
actor_login: "alice",
action: "repo.delete",
ip_address: "192.168.1.10",
created_at: "2024-01-01T00:00:00Z",
},
],
};
}

return {
ok: false,
status: 404,
json: async () => ({}),
};
});
}

describe("Security admin pages", () => {
beforeEach(() => {
mockPush.mockClear();
vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", "http://localhost:8080");
});

it("renders advisories for org admins", async () => {
vi.stubGlobal("fetch", mockAdminFetch());

render(<SecurityAdvisoriesPage />);

await waitFor(() => {
expect(screen.getByText("GHSA-xxxx-yyyy-zzzz")).toBeInTheDocument();
});
});

it("shows access denied for non-admin members on advisories page", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);

if (url.endsWith("/api/v3/user")) {
return {
ok: true,
status: 200,
json: async () => ({ login: "bob" }),
};
}

if (url.endsWith("/api/v3/orgs/acme-corp/members")) {
return {
ok: true,
status: 200,
json: async () => [{ login: "bob", role: "member" }],
};
}

return { ok: false, status: 404, json: async () => ({}) };
}),
);

render(<SecurityAdvisoriesPage />);

await waitFor(() => {
expect(screen.getByText("Access Denied")).toBeInTheDocument();
});
});

it("uses action filter param and masks IP addresses on audit log page", async () => {
const fetchMock = mockAdminFetch();
vi.stubGlobal("fetch", fetchMock);

render(<SecurityAuditLogPage />);

await waitFor(() => {
expect(screen.getByText("repo.delete")).toBeInTheDocument();
});

expect(screen.getByText("192.168.*.*")).toBeInTheDocument();

await userEvent.selectOptions(
screen.getByLabelText("Action"),
"repo.delete",
);
await userEvent.click(screen.getByRole("button", { name: "Search" }));

await waitFor(() => {
expect(
fetchMock.mock.calls.some(([url]) => {
const parsed = new URL(String(url));
return parsed.searchParams.get("action") === "repo.delete";
}),
).toBe(true);
});
});
});
92 changes: 92 additions & 0 deletions __tests__/lib/admin/security.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, expect, it } from "vitest";

import {
buildOrgQueryString,
datetimeLocalToIso,
getSeverityBadgeClass,
isAdminOrgRole,
maskIpAddress,
sanitizeAuditSearchPhrase,
sanitizeOrgLogin,
} from "@/lib/admin/security";

describe("sanitizeOrgLogin", () => {
it("accepts valid org logins", () => {
expect(sanitizeOrgLogin("acme-corp")).toBe("acme-corp");
expect(sanitizeOrgLogin(" acme ")).toBe("acme");
});

it("rejects invalid org logins", () => {
expect(sanitizeOrgLogin("acme/evil")).toBeNull();
expect(sanitizeOrgLogin("https://evil.example")).toBeNull();
expect(sanitizeOrgLogin("")).toBeNull();
expect(sanitizeOrgLogin(null)).toBeNull();
});
});

describe("buildOrgQueryString", () => {
it("builds a safe org query string", () => {
expect(buildOrgQueryString("acme-corp")).toBe("?org=acme-corp");
expect(buildOrgQueryString("acme/evil")).toBe("");
});
});

describe("isAdminOrgRole", () => {
it("returns true for admin and owner roles", () => {
expect(isAdminOrgRole("admin")).toBe(true);
expect(isAdminOrgRole("owner")).toBe(true);
expect(isAdminOrgRole("OWNER")).toBe(true);
});

it("returns false for non-admin roles", () => {
expect(isAdminOrgRole("member")).toBe(false);
expect(isAdminOrgRole("read")).toBe(false);
});
});

describe("sanitizeAuditSearchPhrase", () => {
it("trims, strips control characters, and caps length", () => {
expect(sanitizeAuditSearchPhrase(" repo.delete ")).toBe("repo.delete");
expect(sanitizeAuditSearchPhrase("a\u0000b")).toBe("ab");
expect(sanitizeAuditSearchPhrase("x".repeat(300))).toHaveLength(256);
});
});

describe("datetimeLocalToIso", () => {
it("converts datetime-local values using local time", () => {
const iso = datetimeLocalToIso("2024-06-15T10:30");
expect(iso).toMatch(/^2024-06-15T/);
expect(iso.endsWith("Z") || iso.includes("+") || iso.includes("-")).toBe(true);
});

it("returns empty string for invalid values", () => {
expect(datetimeLocalToIso("")).toBe("");
expect(datetimeLocalToIso("not-a-date")).toBe("");
});
});

describe("maskIpAddress", () => {
it("masks IPv4 addresses", () => {
expect(maskIpAddress("192.168.1.42")).toBe("192.168.*.*");
});

it("masks IPv6 addresses", () => {
expect(maskIpAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334")).toBe(
"2001:****",
);
});

it("returns em dash for missing values", () => {
expect(maskIpAddress(null)).toBe("—");
});
});

describe("getSeverityBadgeClass", () => {
it("returns a class for known severities", () => {
expect(getSeverityBadgeClass("critical")).toContain("bg-[#cf222e]");
});

it("falls back to low severity styling", () => {
expect(getSeverityBadgeClass("unknown")).toBe(getSeverityBadgeClass("low"));
});
});
Loading
Loading