Files
petfeed/src/__tests__/owner.test.ts
T
adminandClaude Sonnet 5 0323e8d449 feat: add Expo push notification infrastructure
Owner.expoPushToken + owner.registerPushToken/clearPushToken, plus a
best-effort Expo push send wired into notify() alongside the existing
in-app Notification row. Missing/expired token or unreachable Expo push
service silently no-ops — push delivery must never break the underlying
action it's attached to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 09:49:02 +02:00

165 lines
5.7 KiB
TypeScript

/**
* Owner Router Unit Tests
*
* Covers the account-bound Dark Mode preference (Gitea #8): the persisted
* value is the source of truth for cross-device sync, while next-themes'
* own localStorage handles same-browser flash-free reloads.
*/
import { describe, it, expect, vi } from "vitest";
vi.mock("server-only", () => ({}));
vi.mock("@clerk/nextjs/server", () => ({
auth: vi.fn().mockResolvedValue({ userId: "owner-1" }),
}));
vi.mock("@/lib/prisma", () => ({ prisma: {} }));
import { createMockPrisma } from "@/__tests__/helpers/prisma-mock";
async function createOwnerCaller(overrides?: {
userId?: string | null;
prisma?: ReturnType<typeof createMockPrisma>;
}) {
const { ownerRouter } = await import("@/trpc/routers/owner");
const { initTRPC } = await import("@trpc/server");
const mockPrisma = overrides?.prisma ?? createMockPrisma();
const userId = overrides?.userId !== undefined ? overrides.userId : "owner-1";
const t = initTRPC
.context<{ userId: string | null; prisma: typeof mockPrisma }>()
.create();
const testRouter = t.router({ owner: ownerRouter });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const caller = testRouter.createCaller({ userId, prisma: mockPrisma as any });
return { caller, mockPrisma };
}
describe("owner.getThemePreference", () => {
it("returns null when the Owner row does not exist yet", async () => {
const { caller, mockPrisma } = await createOwnerCaller();
mockPrisma.owner.findUnique.mockResolvedValue(null);
const result = await caller.owner.getThemePreference();
expect(result).toEqual({ themePreference: null });
});
it("returns null when the Owner exists but never set a preference", async () => {
const { caller, mockPrisma } = await createOwnerCaller();
mockPrisma.owner.findUnique.mockResolvedValue({ themePreference: null });
const result = await caller.owner.getThemePreference();
expect(result).toEqual({ themePreference: null });
});
it("returns the stored preference when set", async () => {
const { caller, mockPrisma } = await createOwnerCaller();
mockPrisma.owner.findUnique.mockResolvedValue({ themePreference: "DARK" });
const result = await caller.owner.getThemePreference();
expect(result).toEqual({ themePreference: "DARK" });
});
});
describe("owner.setThemePreference", () => {
it("upserts the preference on the calling owner (works even before the Owner row exists)", async () => {
const { caller, mockPrisma } = await createOwnerCaller();
mockPrisma.owner.upsert.mockResolvedValue({ id: "owner-1" });
const result = await caller.owner.setThemePreference({ themePreference: "DARK" });
expect(mockPrisma.owner.upsert).toHaveBeenCalledWith({
where: { id: "owner-1" },
create: { id: "owner-1", themePreference: "DARK" },
update: { themePreference: "DARK" },
});
expect(result).toEqual({ ok: true });
});
it("rejects an invalid preference value", async () => {
const { caller } = await createOwnerCaller();
await expect(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
caller.owner.setThemePreference({ themePreference: "BLUE" as any })
).rejects.toThrow();
});
});
describe("owner.getHideSpidersPreference", () => {
it("defaults to false when the Owner row does not exist yet", async () => {
const { caller, mockPrisma } = await createOwnerCaller();
mockPrisma.owner.findUnique.mockResolvedValue(null);
const result = await caller.owner.getHideSpidersPreference();
expect(result).toEqual({ hideSpiders: false });
});
it("returns the stored preference when set", async () => {
const { caller, mockPrisma } = await createOwnerCaller();
mockPrisma.owner.findUnique.mockResolvedValue({ hideSpiders: true });
const result = await caller.owner.getHideSpidersPreference();
expect(result).toEqual({ hideSpiders: true });
});
});
describe("owner.setHideSpidersPreference", () => {
it("upserts the preference on the calling owner (works even before the Owner row exists)", async () => {
const { caller, mockPrisma } = await createOwnerCaller();
mockPrisma.owner.upsert.mockResolvedValue({ id: "owner-1" });
const result = await caller.owner.setHideSpidersPreference({ hideSpiders: true });
expect(mockPrisma.owner.upsert).toHaveBeenCalledWith({
where: { id: "owner-1" },
create: { id: "owner-1", hideSpiders: true },
update: { hideSpiders: true },
});
expect(result).toEqual({ ok: true });
});
});
describe("owner.registerPushToken", () => {
it("upserts the Expo push token on the calling owner", async () => {
const { caller, mockPrisma } = await createOwnerCaller();
mockPrisma.owner.upsert.mockResolvedValue({ id: "owner-1" });
const result = await caller.owner.registerPushToken({ expoPushToken: "ExponentPushToken[abc]" });
expect(mockPrisma.owner.upsert).toHaveBeenCalledWith({
where: { id: "owner-1" },
create: { id: "owner-1", expoPushToken: "ExponentPushToken[abc]" },
update: { expoPushToken: "ExponentPushToken[abc]" },
});
expect(result).toEqual({ ok: true });
});
it("rejects an empty token", async () => {
const { caller } = await createOwnerCaller();
await expect(caller.owner.registerPushToken({ expoPushToken: "" })).rejects.toThrow();
});
});
describe("owner.clearPushToken", () => {
it("clears the stored token for the calling owner", async () => {
const { caller, mockPrisma } = await createOwnerCaller();
mockPrisma.owner.updateMany.mockResolvedValue({ count: 1 });
const result = await caller.owner.clearPushToken();
expect(mockPrisma.owner.updateMany).toHaveBeenCalledWith({
where: { id: "owner-1" },
data: { expoPushToken: null },
});
expect(result).toEqual({ ok: true });
});
});