Profile page UI and functionality implemented
This commit is contained in:
@@ -1,47 +1,73 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { renderWithProviders as render, screen } from "../utils/test-utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { CompletedScreen } from "../../app/(app)/create/screens/completed/CompletedScreen";
|
||||
import { CREATE_FLOW_LAST_PUBLISHED_KEY } from "../../lib/create/lastPublishedRule";
|
||||
|
||||
const storedRuleFixture = {
|
||||
id: "rule-fixture-1",
|
||||
title: "Fixture Community Rule",
|
||||
summary: "A short summary for tests.",
|
||||
document: {
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Values",
|
||||
entries: [
|
||||
{
|
||||
title: "Fixture value title",
|
||||
body: "Fixture value body text for the test document.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
categoryName: "Communication",
|
||||
entries: [
|
||||
{
|
||||
title: "Fixture channel",
|
||||
body: "How we talk to each other.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe("CompletedScreen", () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.removeItem(CREATE_FLOW_LAST_PUBLISHED_KEY);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sessionStorage.removeItem(CREATE_FLOW_LAST_PUBLISHED_KEY);
|
||||
});
|
||||
|
||||
it("renders without crashing", () => {
|
||||
render(<CompletedScreen />);
|
||||
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders HeaderLockup with expected title", () => {
|
||||
it("shows no placeholder title or document when session is empty", () => {
|
||||
render(<CompletedScreen />);
|
||||
const h1 = screen.getByRole("heading", { level: 1 });
|
||||
expect(h1.textContent).toBe("");
|
||||
expect(screen.queryByText("Values")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders header and document from sessionStorage", () => {
|
||||
sessionStorage.setItem(
|
||||
CREATE_FLOW_LAST_PUBLISHED_KEY,
|
||||
JSON.stringify(storedRuleFixture),
|
||||
);
|
||||
render(<CompletedScreen />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "Mutual Aid Mondays",
|
||||
name: "Fixture Community Rule",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders HeaderLockup with expected description", () => {
|
||||
render(<CompletedScreen />);
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Mutual Aid Monday is a grassroots community in Denver, founded in November 2020 by Kelsang Virya, dedicated to supporting neighbors experiencing homelessness./i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Community Rule document with section labels", () => {
|
||||
render(<CompletedScreen />);
|
||||
expect(screen.getByText("A short summary for tests.")).toBeInTheDocument();
|
||||
expect(screen.getByText("Values")).toBeInTheDocument();
|
||||
expect(screen.getByText("Communication")).toBeInTheDocument();
|
||||
expect(screen.getByText("Membership")).toBeInTheDocument();
|
||||
expect(screen.getByText("Decision-making")).toBeInTheDocument();
|
||||
expect(screen.getByText("Conflict management")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders document entry titles", () => {
|
||||
render(<CompletedScreen />);
|
||||
expect(screen.getByText("Solidarity Forever")).toBeInTheDocument();
|
||||
expect(screen.getByText("Shared Leadership")).toBeInTheDocument();
|
||||
expect(screen.getByText("Organizing Offline")).toBeInTheDocument();
|
||||
expect(screen.getByText("Circular Food Systems")).toBeInTheDocument();
|
||||
expect(screen.getByText("Fixture value title")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders toast alert when page loads", () => {
|
||||
|
||||
@@ -59,11 +59,11 @@ describe("Create", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses login yellow backdrop when backdropVariant is loginYellow", () => {
|
||||
it("uses blurred yellow backdrop when backdropVariant is blurredYellow", () => {
|
||||
renderWithProviders(
|
||||
<Create
|
||||
{...defaultProps}
|
||||
backdropVariant="loginYellow"
|
||||
backdropVariant="blurredYellow"
|
||||
headerContent={<div>Header</div>}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { screen, fireEvent } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { renderWithProviders } from "../utils/test-utils";
|
||||
import Dialog from "../../app/components/modals/Dialog";
|
||||
|
||||
type Props = React.ComponentProps<typeof Dialog>;
|
||||
|
||||
describe("Dialog", () => {
|
||||
const defaultProps: Props = {
|
||||
isOpen: true,
|
||||
onClose: vi.fn(),
|
||||
title: "Confirm action",
|
||||
description: "This cannot be undone.",
|
||||
footer: (
|
||||
<>
|
||||
<button type="button">Cancel</button>
|
||||
<button type="button">Confirm</button>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders when isOpen is true", () => {
|
||||
renderWithProviders(<Dialog {...defaultProps} />);
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
expect(screen.getByText("Confirm action")).toBeInTheDocument();
|
||||
expect(screen.getByText("This cannot be undone.")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
expect(screen.getByText("Confirm")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render when isOpen is false", () => {
|
||||
renderWithProviders(<Dialog {...defaultProps} isOpen={false} />);
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onClose when close control is activated", () => {
|
||||
const onClose = vi.fn();
|
||||
renderWithProviders(<Dialog {...defaultProps} onClose={onClose} />);
|
||||
fireEvent.click(screen.getByLabelText("Close dialog"));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onClose when Escape is pressed", () => {
|
||||
const onClose = vi.fn();
|
||||
renderWithProviders(<Dialog {...defaultProps} onClose={onClose} />);
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("locks body scroll when open", () => {
|
||||
renderWithProviders(<Dialog {...defaultProps} />);
|
||||
expect(document.body.style.overflow).toBe("hidden");
|
||||
});
|
||||
});
|
||||
@@ -89,4 +89,12 @@ describe("HeaderLockup (behavioral tests)", () => {
|
||||
render(<HeaderLockup title="Test Title" justification="left" size="L" />);
|
||||
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("forwards titleId to the h1", () => {
|
||||
render(<HeaderLockup title="Test Title" titleId="profile-welcome" />);
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveAttribute(
|
||||
"id",
|
||||
"profile-welcome",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,4 +46,11 @@ describe("TextInput (size tests)", () => {
|
||||
expect(input).toHaveClass("h-[32px]");
|
||||
});
|
||||
|
||||
it("forwards maxLength to the native input", () => {
|
||||
const { container } = render(
|
||||
<TextInput label="Test" maxLength={200} />,
|
||||
);
|
||||
const input = container.querySelector("input");
|
||||
expect(input).toHaveAttribute("maxLength", "200");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import List from "../../../app/components/layout/List";
|
||||
import {
|
||||
componentTestSuite,
|
||||
type ComponentTestSuiteConfig,
|
||||
} from "../../utils/componentTestSuite";
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: "a",
|
||||
title: "First",
|
||||
description: "First description",
|
||||
href: "/first",
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
title: "Second",
|
||||
description: "Second description",
|
||||
onClick: () => {},
|
||||
},
|
||||
];
|
||||
|
||||
type Props = React.ComponentProps<typeof List>;
|
||||
|
||||
const config: ComponentTestSuiteConfig<Props> = {
|
||||
component: List,
|
||||
name: "List",
|
||||
props: { items } as Props,
|
||||
primaryRole: "list",
|
||||
testCases: {
|
||||
renders: true,
|
||||
accessibility: true,
|
||||
keyboardNavigation: false,
|
||||
disabledState: false,
|
||||
errorState: false,
|
||||
},
|
||||
};
|
||||
|
||||
describe("List", () => {
|
||||
componentTestSuite<Props>(config);
|
||||
|
||||
it("renders a link row when item has href", () => {
|
||||
render(
|
||||
<List
|
||||
items={[
|
||||
{
|
||||
id: "1",
|
||||
title: "T",
|
||||
description: "D",
|
||||
href: "/x",
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("link", { name: /T/ })).toHaveAttribute("href", "/x");
|
||||
});
|
||||
|
||||
it("calls onClick for button rows", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = vi.fn();
|
||||
render(
|
||||
<List
|
||||
items={[
|
||||
{
|
||||
id: "1",
|
||||
title: "Action",
|
||||
description: "Desc",
|
||||
onClick,
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: /Action/ }));
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("applies size s and l Figma data attributes on the list root", () => {
|
||||
const { container: a, rerender } = render(<List items={items} size="s" />);
|
||||
expect(
|
||||
a.querySelector('[data-figma-node="21863:45631"]'),
|
||||
).toBeInTheDocument();
|
||||
rerender(<List items={items} size="l" />);
|
||||
expect(
|
||||
a.querySelector('[data-figma-node="21844:4405"]'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import ListEntry from "../../../app/components/layout/ListEntry";
|
||||
import {
|
||||
componentTestSuite,
|
||||
type ComponentTestSuiteConfig,
|
||||
} from "../../utils/componentTestSuite";
|
||||
|
||||
type Props = React.ComponentProps<typeof ListEntry>;
|
||||
|
||||
const base: Props = {
|
||||
title: "Item",
|
||||
description: "Description",
|
||||
href: "#",
|
||||
topDivider: false,
|
||||
bottomDivider: true,
|
||||
};
|
||||
|
||||
const config: ComponentTestSuiteConfig<Props> = {
|
||||
component: ListEntry,
|
||||
name: "ListEntry",
|
||||
props: base,
|
||||
primaryRole: "link",
|
||||
testCases: {
|
||||
renders: true,
|
||||
accessibility: true,
|
||||
keyboardNavigation: true,
|
||||
disabledState: false,
|
||||
errorState: false,
|
||||
},
|
||||
};
|
||||
|
||||
describe("ListEntry", () => {
|
||||
componentTestSuite<Props>(config);
|
||||
|
||||
it("uses Base / Interactive Figma id for size m", () => {
|
||||
const { container } = render(
|
||||
<ListEntry
|
||||
title="A"
|
||||
description="B"
|
||||
size="m"
|
||||
href="#"
|
||||
topDivider={false}
|
||||
bottomDivider={false}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container.querySelector('[data-figma-node="21863:45422"]'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("omits description when showDescription is false", () => {
|
||||
render(
|
||||
<ListEntry
|
||||
title="Only"
|
||||
description="Hidden"
|
||||
showDescription={false}
|
||||
href="#"
|
||||
topDivider={false}
|
||||
bottomDivider={false}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("link", { name: "Only" })).toBeInTheDocument();
|
||||
expect(screen.queryByText("Hidden")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("button fires onClick", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = vi.fn();
|
||||
render(
|
||||
<ListEntry
|
||||
title="Tap"
|
||||
description="D"
|
||||
onClick={onClick}
|
||||
topDivider={false}
|
||||
bottomDivider={false}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: /Tap/ }));
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import Link from "../../../app/components/navigation/Link";
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({
|
||||
children,
|
||||
href,
|
||||
...props
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
href?: string;
|
||||
[key: string]: unknown;
|
||||
}) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
const FIGMA = "21861:21428";
|
||||
|
||||
describe("Link (Navigation)", () => {
|
||||
it("renders an anchor with href and data-figma-node", () => {
|
||||
const { container } = render(
|
||||
<Link
|
||||
href="/rules/1"
|
||||
variant="paragraph"
|
||||
type="primary"
|
||||
theme="light"
|
||||
>
|
||||
View
|
||||
</Link>,
|
||||
);
|
||||
const a = screen.getByRole("link", { name: /view/i });
|
||||
expect(a).toHaveAttribute("href", "/rules/1");
|
||||
expect(a).toHaveAttribute("data-figma-node", FIGMA);
|
||||
expect(container.querySelector("a")?.className).toMatch(
|
||||
/text-\[var\(--color-link-primary\)\]/,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders a button when href is omitted", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = vi.fn();
|
||||
render(
|
||||
<Link
|
||||
variant="paragraph"
|
||||
type="primary"
|
||||
theme="light"
|
||||
onClick={onClick}
|
||||
>
|
||||
Delete
|
||||
</Link>,
|
||||
);
|
||||
const btn = screen.getByRole("button", { name: /delete/i });
|
||||
expect(btn).toHaveAttribute("data-figma-node", FIGMA);
|
||||
expect(btn).toHaveAttribute("type", "button");
|
||||
await user.click(btn);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("applies secondary + dark classes for that combination", () => {
|
||||
const { container } = render(
|
||||
<Link href="#" variant="paragraph" type="secondary" theme="dark">
|
||||
More
|
||||
</Link>,
|
||||
);
|
||||
const el = container.querySelector("a");
|
||||
expect(el?.className).toMatch(/text-\[var\(--color-link-invert-secondary\)\]/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render } from "@testing-library/react";
|
||||
import Divider from "../../../app/components/utility/Divider";
|
||||
import {
|
||||
componentTestSuite,
|
||||
type ComponentTestSuiteConfig,
|
||||
} from "../../utils/componentTestSuite";
|
||||
|
||||
type Props = React.ComponentProps<typeof Divider>;
|
||||
|
||||
const config: ComponentTestSuiteConfig<Props> = {
|
||||
component: Divider,
|
||||
name: "Divider",
|
||||
props: {} as Props,
|
||||
testCases: {
|
||||
renders: true,
|
||||
accessibility: true,
|
||||
keyboardNavigation: false,
|
||||
disabledState: false,
|
||||
errorState: false,
|
||||
},
|
||||
};
|
||||
|
||||
describe("Divider", () => {
|
||||
componentTestSuite<Props>(config);
|
||||
|
||||
it("renders horizontal content line with Figma line node", () => {
|
||||
const { container } = render(
|
||||
<Divider type="content" orientation="horizontal" />,
|
||||
);
|
||||
expect(
|
||||
container.querySelector('[data-figma-node="6894:22989"]'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders menu horizontal with tertiary line", () => {
|
||||
const { container } = render(
|
||||
<Divider type="menu" orientation="horizontal" />,
|
||||
);
|
||||
const line = container.querySelector('[data-figma-node="2002:30856"]');
|
||||
expect(line).toBeInTheDocument();
|
||||
expect(line).toHaveClass("bg-[var(--color-border-default-tertiary)]");
|
||||
});
|
||||
|
||||
it("renders vertical content bar", () => {
|
||||
const { container } = render(
|
||||
<Divider type="content" orientation="vertical" />,
|
||||
);
|
||||
expect(
|
||||
container.querySelector('[data-figma-node="6894:22990"]'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -79,6 +79,13 @@ describe("createFlowStateSchema", () => {
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects communityContext longer than 200 chars", () => {
|
||||
const r = createFlowStateSchema.safeParse({
|
||||
communityContext: "x".repeat(201),
|
||||
});
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts communityStructureChipSnapshots with custom chip rows", () => {
|
||||
const r = createFlowStateSchema.safeParse({
|
||||
communityStructureChipSnapshots: {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const isDatabaseConfiguredMock = vi.fn();
|
||||
const findUniqueMock = vi.fn();
|
||||
const deleteMock = vi.fn();
|
||||
const getSessionUserMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
isDatabaseConfigured: () => isDatabaseConfiguredMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/db", () => ({
|
||||
prisma: {
|
||||
publishedRule: {
|
||||
findUnique: (...args: unknown[]) => findUniqueMock(...args),
|
||||
delete: (...args: unknown[]) => deleteMock(...args),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/session", () => ({
|
||||
getSessionUser: () => getSessionUserMock(),
|
||||
}));
|
||||
|
||||
import { DELETE } from "../../app/api/rules/[id]/route";
|
||||
|
||||
function makeContext(id: string) {
|
||||
return { params: Promise.resolve({ id }) };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
isDatabaseConfiguredMock.mockReset();
|
||||
findUniqueMock.mockReset();
|
||||
deleteMock.mockReset();
|
||||
getSessionUserMock.mockReset();
|
||||
});
|
||||
|
||||
describe("DELETE /api/rules/[id]", () => {
|
||||
it("returns 401 when not signed in", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue(null);
|
||||
const res = await DELETE(
|
||||
new NextRequest("https://x.test/api/rules/r1"),
|
||||
makeContext("r1"),
|
||||
);
|
||||
expect(res.status).toBe(401);
|
||||
expect(findUniqueMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 404 when the rule does not exist", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue({ id: "u1", email: "a@b.c" });
|
||||
findUniqueMock.mockResolvedValueOnce(null);
|
||||
const res = await DELETE(
|
||||
new NextRequest("https://x.test/api/rules/missing"),
|
||||
makeContext("missing"),
|
||||
);
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("returns 403 when the rule is owned by another user", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue({ id: "u1", email: "a@b.c" });
|
||||
findUniqueMock.mockResolvedValueOnce({
|
||||
id: "r1",
|
||||
userId: "other",
|
||||
});
|
||||
const res = await DELETE(
|
||||
new NextRequest("https://x.test/api/rules/r1"),
|
||||
makeContext("r1"),
|
||||
);
|
||||
expect(res.status).toBe(403);
|
||||
expect(deleteMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("deletes and returns 200 when the user owns the rule", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue({ id: "u1", email: "a@b.c" });
|
||||
findUniqueMock.mockResolvedValueOnce({
|
||||
id: "r1",
|
||||
userId: "u1",
|
||||
});
|
||||
deleteMock.mockResolvedValueOnce(undefined);
|
||||
const res = await DELETE(
|
||||
new NextRequest("https://x.test/api/rules/r1"),
|
||||
makeContext("r1"),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(deleteMock).toHaveBeenCalledWith({ where: { id: "r1" } });
|
||||
const body = (await res.json()) as { ok: boolean };
|
||||
expect(body.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const isDatabaseConfiguredMock = vi.fn();
|
||||
const findUniqueMock = vi.fn();
|
||||
const getSessionUserMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
isDatabaseConfigured: () => isDatabaseConfiguredMock(),
|
||||
@@ -16,6 +17,10 @@ vi.mock("../../lib/server/db", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/session", () => ({
|
||||
getSessionUser: () => getSessionUserMock(),
|
||||
}));
|
||||
|
||||
import { GET } from "../../app/api/rules/[id]/route";
|
||||
|
||||
function makeContext(id: string) {
|
||||
@@ -25,6 +30,8 @@ function makeContext(id: string) {
|
||||
beforeEach(() => {
|
||||
isDatabaseConfiguredMock.mockReset();
|
||||
findUniqueMock.mockReset();
|
||||
getSessionUserMock.mockReset();
|
||||
getSessionUserMock.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
describe("GET /api/rules/[id]", () => {
|
||||
@@ -79,7 +86,7 @@ describe("GET /api/rules/[id]", () => {
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("returns 200 with { rule } when a published rule exists", async () => {
|
||||
it("returns 200 with { rule, viewerIsOwner: false } when a published rule exists and the viewer is anonymous", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
const row = {
|
||||
id: "rule-1",
|
||||
@@ -97,9 +104,59 @@ describe("GET /api/rules/[id]", () => {
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as {
|
||||
rule: { id: string; title: string; summary: string | null };
|
||||
viewerIsOwner: boolean;
|
||||
};
|
||||
expect(body.rule.id).toBe("rule-1");
|
||||
expect(body.rule.title).toBe("Mutual Aid Mondays");
|
||||
expect(body.rule.summary).toBe("A grassroots community in Denver.");
|
||||
expect(body.viewerIsOwner).toBe(false);
|
||||
expect(findUniqueMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns viewerIsOwner true when the signed-in user owns the rule", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue({ id: "user-1", email: "a@b.c" });
|
||||
const row = {
|
||||
id: "rule-1",
|
||||
title: "Mutual Aid Mondays",
|
||||
summary: "A grassroots community in Denver.",
|
||||
document: { sections: [] },
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-02T00:00:00Z"),
|
||||
};
|
||||
findUniqueMock
|
||||
.mockResolvedValueOnce(row)
|
||||
.mockResolvedValueOnce({ userId: "user-1" });
|
||||
const res = await GET(
|
||||
new NextRequest("https://x.test/api/rules/rule-1"),
|
||||
makeContext("rule-1"),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { viewerIsOwner: boolean };
|
||||
expect(body.viewerIsOwner).toBe(true);
|
||||
expect(findUniqueMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("returns viewerIsOwner false when the signed-in user does not own the rule", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue({ id: "user-1", email: "a@b.c" });
|
||||
const row = {
|
||||
id: "rule-1",
|
||||
title: "Mutual Aid Mondays",
|
||||
summary: "A grassroots community in Denver.",
|
||||
document: { sections: [] },
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-02T00:00:00Z"),
|
||||
};
|
||||
findUniqueMock
|
||||
.mockResolvedValueOnce(row)
|
||||
.mockResolvedValueOnce({ userId: "other" });
|
||||
const res = await GET(
|
||||
new NextRequest("https://x.test/api/rules/rule-1"),
|
||||
makeContext("rule-1"),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { viewerIsOwner: boolean };
|
||||
expect(body.viewerIsOwner).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const isDatabaseConfiguredMock = vi.fn();
|
||||
const findUniqueMock = vi.fn();
|
||||
const createMock = vi.fn();
|
||||
const getSessionUserMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
isDatabaseConfigured: () => isDatabaseConfiguredMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/db", () => ({
|
||||
prisma: {
|
||||
publishedRule: {
|
||||
findUnique: (...args: unknown[]) => findUniqueMock(...args),
|
||||
create: (...args: unknown[]) => createMock(...args),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/session", () => ({
|
||||
getSessionUser: () => getSessionUserMock(),
|
||||
}));
|
||||
|
||||
import { POST } from "../../app/api/rules/[id]/duplicate/route";
|
||||
|
||||
function makeContext(id: string) {
|
||||
return { params: Promise.resolve({ id }) };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
isDatabaseConfiguredMock.mockReset();
|
||||
findUniqueMock.mockReset();
|
||||
createMock.mockReset();
|
||||
getSessionUserMock.mockReset();
|
||||
});
|
||||
|
||||
describe("POST /api/rules/[id]/duplicate", () => {
|
||||
it("returns 401 when not signed in", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue(null);
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/rules/r1/duplicate"),
|
||||
makeContext("r1"),
|
||||
);
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("returns 404 when the source rule does not exist", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue({ id: "u1", email: "a@b.c" });
|
||||
findUniqueMock.mockResolvedValueOnce(null);
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/rules/missing/duplicate"),
|
||||
makeContext("missing"),
|
||||
);
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("returns 403 when the user does not own the source rule", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue({ id: "u1", email: "a@b.c" });
|
||||
findUniqueMock.mockResolvedValueOnce({
|
||||
id: "r1",
|
||||
userId: "other",
|
||||
title: "T",
|
||||
summary: null,
|
||||
document: {},
|
||||
});
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/rules/r1/duplicate"),
|
||||
makeContext("r1"),
|
||||
);
|
||||
expect(res.status).toBe(403);
|
||||
expect(createMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 201-style 200 with the new rule when duplicate succeeds", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue({ id: "u1", email: "a@b.c" });
|
||||
findUniqueMock.mockResolvedValueOnce({
|
||||
id: "r1",
|
||||
userId: "u1",
|
||||
title: "Original",
|
||||
summary: "S",
|
||||
document: { x: 1 },
|
||||
});
|
||||
createMock.mockResolvedValueOnce({
|
||||
id: "r2",
|
||||
title: "Original (Copy)",
|
||||
summary: "S",
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-01T00:00:00Z"),
|
||||
});
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/rules/r1/duplicate"),
|
||||
makeContext("r1"),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { rule: { id: string; title: string } };
|
||||
expect(body.rule.id).toBe("r2");
|
||||
expect(body.rule.title).toBe("Original (Copy)");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const isDatabaseConfiguredMock = vi.fn();
|
||||
const listForUserMock = vi.fn();
|
||||
const getSessionUserMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
isDatabaseConfigured: () => isDatabaseConfiguredMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/publishedRules", () => ({
|
||||
listPublishedRulesForUser: (...args: unknown[]) => listForUserMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/session", () => ({
|
||||
getSessionUser: () => getSessionUserMock(),
|
||||
}));
|
||||
|
||||
import { GET } from "../../app/api/rules/me/route";
|
||||
|
||||
beforeEach(() => {
|
||||
isDatabaseConfiguredMock.mockReset();
|
||||
listForUserMock.mockReset();
|
||||
getSessionUserMock.mockReset();
|
||||
});
|
||||
|
||||
describe("GET /api/rules/me", () => {
|
||||
it("returns 503 when the database is not configured", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(false);
|
||||
const res = await GET(
|
||||
new NextRequest("https://x.test/api/rules/me"),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(503);
|
||||
expect(getSessionUserMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 401 when not signed in", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue(null);
|
||||
const res = await GET(
|
||||
new NextRequest("https://x.test/api/rules/me"),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(401);
|
||||
expect(listForUserMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 200 with { rules } for the session user", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue({ id: "user-1", email: "a@b.c" });
|
||||
const rows = [
|
||||
{
|
||||
id: "r1",
|
||||
title: "Rule A",
|
||||
summary: "S",
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-02T00:00:00Z"),
|
||||
},
|
||||
];
|
||||
listForUserMock.mockResolvedValueOnce(rows);
|
||||
const res = await GET(
|
||||
new NextRequest("https://x.test/api/rules/me?limit=10"),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(listForUserMock).toHaveBeenCalledWith("user-1", 10);
|
||||
const body = (await res.json()) as {
|
||||
rules: Array<{ id: string; title: string }>;
|
||||
};
|
||||
expect(body.rules).toHaveLength(1);
|
||||
expect(body.rules[0].id).toBe("r1");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const isDatabaseConfiguredMock = vi.fn();
|
||||
const userDeleteMock = vi.fn();
|
||||
const getSessionUserMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
isDatabaseConfigured: () => isDatabaseConfiguredMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/db", () => ({
|
||||
prisma: {
|
||||
user: {
|
||||
delete: (...args: unknown[]) => userDeleteMock(...args),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/session", () => ({
|
||||
getSessionUser: () => getSessionUserMock(),
|
||||
clearSessionCookie: vi.fn(),
|
||||
}));
|
||||
|
||||
import { DELETE } from "../../app/api/user/me/route";
|
||||
|
||||
beforeEach(() => {
|
||||
isDatabaseConfiguredMock.mockReset();
|
||||
userDeleteMock.mockReset();
|
||||
getSessionUserMock.mockReset();
|
||||
});
|
||||
|
||||
describe("DELETE /api/user/me", () => {
|
||||
it("returns 503 when the database is not configured", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(false);
|
||||
const res = await DELETE(
|
||||
new NextRequest("https://x.test/api/user/me"),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(503);
|
||||
});
|
||||
|
||||
it("returns 401 when not signed in", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue(null);
|
||||
const res = await DELETE(
|
||||
new NextRequest("https://x.test/api/user/me"),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(401);
|
||||
expect(userDeleteMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("deletes the user and returns 200 when signed in", async () => {
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
getSessionUserMock.mockResolvedValue({ id: "u1", email: "a@b.c" });
|
||||
userDeleteMock.mockResolvedValueOnce(undefined);
|
||||
const res = await DELETE(
|
||||
new NextRequest("https://x.test/api/user/me"),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(userDeleteMock).toHaveBeenCalledWith({ where: { id: "u1" } });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user