feat(notifications): add account-scoped inbox and persistent preferences
This commit is contained in:
1 parent
8f55ff2d17
commit
1eee6661f9
39 files changed
+1559
-3
No files matched your search
@@ -0,0 +1,117 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
auth: vi.fn(),
|
||||
feed: vi.fn(),
|
||||
mark: vi.fn(),
|
||||
preferences: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/auth", () => ({ auth: mocks.auth }));
|
||||
vi.mock("@/features/notifications/server", () => ({ notifications: mocks }));
|
||||
vi.mock("@/lib/rate-limit", () => ({ rateLimit: async () => ({ ok: true }) }));
|
||||
|
||||
import { GET, POST } from "./route";
|
||||
|
||||
function request(body: unknown, origin = "https://hotel.test") {
|
||||
return new Request("https://hotel.test/api/notifications", {
|
||||
method: "POST",
|
||||
headers: { origin, "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.auth.mockResolvedValue({ user: { id: "7" } });
|
||||
mocks.feed.mockResolvedValue({ items: [] });
|
||||
mocks.mark.mockResolvedValue(true);
|
||||
});
|
||||
describe("notification route authorization", () => {
|
||||
it("rejects anonymous reads and writes without accessing data", async () => {
|
||||
mocks.auth.mockResolvedValue(null);
|
||||
expect(
|
||||
(await GET(new Request("https://hotel.test/api/notifications"))).status,
|
||||
).toBe(401);
|
||||
expect(
|
||||
(await POST(request({ action: "read", key: "support:1" }))).status,
|
||||
).toBe(401);
|
||||
expect(mocks.feed).not.toHaveBeenCalled();
|
||||
expect(mocks.mark).not.toHaveBeenCalled();
|
||||
});
|
||||
it("uses the session identity even when a query supplies another account", async () => {
|
||||
await GET(
|
||||
new Request("https://hotel.test/api/notifications?userId=99&page=2"),
|
||||
);
|
||||
expect(mocks.feed).toHaveBeenCalledWith(7, 2);
|
||||
});
|
||||
it("rejects cross-origin mutations", async () => {
|
||||
expect(
|
||||
(
|
||||
await POST(
|
||||
request(
|
||||
{ action: "read", key: "support:1" },
|
||||
"https://attacker.test",
|
||||
),
|
||||
)
|
||||
).status,
|
||||
).toBe(403);
|
||||
expect(mocks.mark).not.toHaveBeenCalled();
|
||||
});
|
||||
it("rejects an injected account in writes", async () => {
|
||||
expect(
|
||||
(await POST(request({ action: "read", key: "support:1", userId: 99 })))
|
||||
.status,
|
||||
).toBe(400);
|
||||
expect(mocks.mark).not.toHaveBeenCalled();
|
||||
});
|
||||
it("rejects unavailable and other account notifications", async () => {
|
||||
mocks.mark.mockResolvedValue(false);
|
||||
expect(
|
||||
(await POST(request({ action: "read", key: "support:1" }))).status,
|
||||
).toBe(404);
|
||||
expect(mocks.mark).toHaveBeenCalledWith(7, "support:1");
|
||||
});
|
||||
it("persists category preferences under the authenticated account", async () => {
|
||||
const preferences = { support: false, friends: true, events: false };
|
||||
expect(
|
||||
(await POST(request({ action: "preferences", preferences }))).status,
|
||||
).toBe(200);
|
||||
expect(mocks.preferences).toHaveBeenCalledWith(7, preferences);
|
||||
});
|
||||
it("does not turn an infrastructure failure into an empty feed", async () => {
|
||||
mocks.feed.mockRejectedValue(new Error("Database unavailable"));
|
||||
expect(
|
||||
(await GET(new Request("https://hotel.test/api/notifications"))).status,
|
||||
).toBe(503);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a failure when saving read state or preferences fails", async () => {
|
||||
mocks.mark.mockRejectedValue(new Error("Database unavailable"));
|
||||
expect(
|
||||
(await POST(request({ action: "read", key: "support:1" }))).status,
|
||||
).toBe(503);
|
||||
mocks.preferences.mockRejectedValue(new Error("Database unavailable"));
|
||||
expect(
|
||||
(
|
||||
await POST(
|
||||
request({
|
||||
action: "preferences",
|
||||
preferences: { support: true, friends: false, events: true },
|
||||
}),
|
||||
)
|
||||
).status,
|
||||
).toBe(503);
|
||||
});
|
||||
|
||||
it("rejects incomplete and foreign-account preferences", async () => {
|
||||
for (const body of [
|
||||
{ action: "preferences", preferences: { support: false } },
|
||||
{
|
||||
action: "preferences",
|
||||
userId: 99,
|
||||
preferences: { support: false, friends: true, events: true },
|
||||
},
|
||||
])
|
||||
expect((await POST(request(body))).status).toBe(400);
|
||||
expect(mocks.preferences).not.toHaveBeenCalled();
|
||||
});
|
||||
Reference in new issue
Block a user