Advertisement.species existed in the schema but was never wired up — the admin form never let you set it and the feed's ad query ignored it entirely, showing every active ad to every pet regardless of species. Adds a new Advertisement.breeds field alongside it and wires both through the whole path: a targeting UI in the ad create/edit dialog (species checkboxes, each expandable into its own breed checklist), and a filter in admin.getActiveAds (now scoped to the viewing pet) so cat food ads stop reaching dog owners. Empty species/breeds = shown to everyone, unchanged from today's behavior.
484 lines
19 KiB
TypeScript
484 lines
19 KiB
TypeScript
/**
|
|
* Admin Router Unit Tests — deleteComment (Cluster A counter fix)
|
|
*
|
|
* admin.ts had zero test coverage before this change. Scope is limited to
|
|
* deleteComment, which previously deleted a Comment without ever decrementing
|
|
* the parent Post's commentCount — a real gap fixed alongside the Post
|
|
* denormalized-counter rollout (see comments.ts / reactions.ts for the same
|
|
* pattern on the user-facing mutation paths).
|
|
*
|
|
* Tests:
|
|
* - deleteComment decrements Post.commentCount and writes a moderation log
|
|
* - deleteComment throws FORBIDDEN for a non-admin caller
|
|
*/
|
|
|
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
vi.mock("server-only", () => ({}));
|
|
const mockGetUserList = vi.fn();
|
|
vi.mock("@clerk/nextjs/server", () => ({
|
|
auth: vi.fn().mockResolvedValue({ userId: "admin-1" }),
|
|
clerkClient: vi.fn().mockResolvedValue({ users: { getUserList: mockGetUserList } }),
|
|
}));
|
|
vi.mock("@/lib/supabase-storage", () => ({
|
|
createSignedUploadUrl: vi.fn(),
|
|
ALLOWED_CONTENT_TYPES: ["image/jpeg", "image/png", "image/webp"],
|
|
MAX_FILE_SIZE: 10 * 1024 * 1024,
|
|
}));
|
|
vi.mock("@/lib/prisma", () => ({ prisma: {} }));
|
|
|
|
const mockBuildDataExport = vi.fn();
|
|
vi.mock("@/lib/gdpr-export", () => ({
|
|
buildDataExport: (...args: unknown[]) => mockBuildDataExport(...args),
|
|
}));
|
|
|
|
const mockUploadExport = vi.fn();
|
|
vi.mock("@/lib/data-export-storage", () => ({
|
|
uploadExport: (...args: unknown[]) => mockUploadExport(...args),
|
|
}));
|
|
|
|
const mockSendMail = vi.fn();
|
|
vi.mock("@/lib/mail", () => ({
|
|
sendMail: (...args: unknown[]) => mockSendMail(...args),
|
|
}));
|
|
|
|
import { createMockPrisma } from "@/__tests__/helpers/prisma-mock";
|
|
|
|
async function createAdminCaller(overrides?: {
|
|
userId?: string | null;
|
|
prisma?: ReturnType<typeof createMockPrisma>;
|
|
}) {
|
|
const { adminRouter } = await import("@/trpc/routers/admin");
|
|
const { initTRPC } = await import("@trpc/server");
|
|
|
|
const mockPrisma = overrides?.prisma ?? createMockPrisma();
|
|
const userId = overrides?.userId !== undefined ? overrides.userId : "admin-1";
|
|
// deleteComment uses the interactive-transaction callback form; other sites
|
|
// in this codebase use the array form — support both shapes here.
|
|
mockPrisma.$transaction.mockImplementation((arg: unknown) =>
|
|
typeof arg === "function"
|
|
? (arg as (tx: typeof mockPrisma) => unknown)(mockPrisma)
|
|
: Promise.all(arg as unknown[])
|
|
);
|
|
|
|
const t = initTRPC
|
|
.context<{ userId: string | null; prisma: typeof mockPrisma }>()
|
|
.create();
|
|
|
|
const testRouter = t.router({ admin: adminRouter });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const caller = testRouter.createCaller({ userId, prisma: mockPrisma as any });
|
|
return { caller, mockPrisma };
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
mockBuildDataExport.mockReset();
|
|
mockUploadExport.mockReset();
|
|
mockSendMail.mockReset();
|
|
});
|
|
|
|
describe("admin.deleteComment", () => {
|
|
it("decrements Post.commentCount and writes a moderation log", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
|
|
mockPrisma.comment.delete.mockResolvedValue({ id: "cmt-1", postId: "post-1" });
|
|
mockPrisma.post.update.mockResolvedValue({ id: "post-1", commentCount: 4 });
|
|
mockPrisma.moderationLog.create.mockResolvedValue({ id: "log-1" });
|
|
|
|
const result = await caller.admin.deleteComment({ commentId: "cmt-1", reason: "spam" });
|
|
|
|
expect(mockPrisma.comment.delete).toHaveBeenCalledWith({ where: { id: "cmt-1" } });
|
|
expect(mockPrisma.post.update).toHaveBeenCalledWith({
|
|
where: { id: "post-1" },
|
|
data: { commentCount: { decrement: 1 } },
|
|
});
|
|
expect(mockPrisma.moderationLog.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({ action: "DELETE_COMMENT", targetId: "cmt-1" }),
|
|
})
|
|
);
|
|
expect(result).toEqual({ success: true });
|
|
});
|
|
|
|
it("throws FORBIDDEN for a non-admin caller", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "someone-else");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
|
|
mockPrisma.adminRole.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(
|
|
caller.admin.deleteComment({ commentId: "cmt-1" })
|
|
).rejects.toThrow(TRPCError);
|
|
expect(mockPrisma.comment.delete).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// Cluster H: deletePost/banUser/unbanUser/dismissReport/deleteReportedPost
|
|
// now wrap the mutation + moderation-log write in $transaction so a failed
|
|
// mutation can never leave behind a log entry describing an action that
|
|
// didn't actually happen.
|
|
describe("admin transactional moderation actions", () => {
|
|
it("deletePost wraps delete + log in a transaction", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
mockPrisma.post.findUnique.mockResolvedValue({ repost: null });
|
|
mockPrisma.post.delete.mockResolvedValue({ id: "post-1" });
|
|
mockPrisma.moderationLog.create.mockResolvedValue({ id: "log-1" });
|
|
|
|
const result = await caller.admin.deletePost({ postId: "post-1", reason: "spam" });
|
|
|
|
expect(mockPrisma.$transaction).toHaveBeenCalledWith(expect.any(Array));
|
|
expect(mockPrisma.post.delete).toHaveBeenCalledWith({ where: { id: "post-1" } });
|
|
expect(mockPrisma.post.update).not.toHaveBeenCalled();
|
|
expect(result).toEqual({ success: true });
|
|
});
|
|
|
|
it("deletePost decrements the original post's repostCount when the deleted post is itself a repost (2026-07-22 audit fix)", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
mockPrisma.post.findUnique.mockResolvedValue({
|
|
repost: { originalPostId: "original-post-1" },
|
|
});
|
|
mockPrisma.post.delete.mockResolvedValue({ id: "repost-post-1" });
|
|
mockPrisma.post.update.mockResolvedValue({ id: "original-post-1" });
|
|
mockPrisma.moderationLog.create.mockResolvedValue({ id: "log-1" });
|
|
|
|
const result = await caller.admin.deletePost({ postId: "repost-post-1" });
|
|
|
|
expect(mockPrisma.post.update).toHaveBeenCalledWith({
|
|
where: { id: "original-post-1" },
|
|
data: { repostCount: { decrement: 1 } },
|
|
});
|
|
expect(result).toEqual({ success: true });
|
|
});
|
|
|
|
it("banUser wraps upsert + log in a transaction", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
mockPrisma.userBan.upsert.mockResolvedValue({ ownerId: "owner-2" });
|
|
mockPrisma.moderationLog.create.mockResolvedValue({ id: "log-1" });
|
|
|
|
const result = await caller.admin.banUser({ ownerId: "owner-2", reason: "abuse" });
|
|
|
|
expect(mockPrisma.$transaction).toHaveBeenCalledWith(expect.any(Array));
|
|
expect(mockPrisma.userBan.upsert).toHaveBeenCalled();
|
|
expect(result).toEqual({ success: true });
|
|
});
|
|
|
|
it("unbanUser wraps delete + log in a transaction", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
mockPrisma.userBan.delete.mockResolvedValue({ ownerId: "owner-2" });
|
|
mockPrisma.moderationLog.create.mockResolvedValue({ id: "log-1" });
|
|
|
|
const result = await caller.admin.unbanUser({ ownerId: "owner-2" });
|
|
|
|
expect(mockPrisma.$transaction).toHaveBeenCalledWith(expect.any(Array));
|
|
expect(mockPrisma.userBan.delete).toHaveBeenCalledWith({ where: { ownerId: "owner-2" } });
|
|
expect(result).toEqual({ success: true });
|
|
});
|
|
|
|
it("dismissReport wraps delete + log in a transaction", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
mockPrisma.report.findUnique.mockResolvedValue({ id: "report-1" });
|
|
mockPrisma.report.delete.mockResolvedValue({ id: "report-1" });
|
|
mockPrisma.moderationLog.create.mockResolvedValue({ id: "log-1" });
|
|
|
|
const result = await caller.admin.dismissReport({ reportId: "report-1" });
|
|
|
|
expect(mockPrisma.$transaction).toHaveBeenCalledWith(expect.any(Array));
|
|
expect(mockPrisma.report.delete).toHaveBeenCalledWith({ where: { id: "report-1" } });
|
|
expect(result).toEqual({ success: true });
|
|
});
|
|
|
|
it("deleteReportedPost wraps delete + log in a transaction", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
mockPrisma.post.findUnique.mockResolvedValue({ repost: null });
|
|
mockPrisma.post.delete.mockResolvedValue({ id: "post-1" });
|
|
mockPrisma.moderationLog.create.mockResolvedValue({ id: "log-1" });
|
|
|
|
const result = await caller.admin.deleteReportedPost({ postId: "post-1" });
|
|
|
|
expect(mockPrisma.$transaction).toHaveBeenCalledWith(expect.any(Array));
|
|
expect(mockPrisma.post.delete).toHaveBeenCalledWith({ where: { id: "post-1" } });
|
|
expect(result).toEqual({ success: true });
|
|
});
|
|
});
|
|
|
|
describe("admin dashboard time-series", () => {
|
|
it("getPostsOverTime returns a zero-filled 30-bucket series with today's posts counted correctly", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
|
|
const now = new Date();
|
|
mockPrisma.post.findMany.mockResolvedValue([
|
|
{ createdAt: now },
|
|
{ createdAt: now },
|
|
]);
|
|
|
|
const result = await caller.admin.getPostsOverTime({ granularity: "day" });
|
|
|
|
expect(result).toHaveLength(30);
|
|
expect(result[29]!.value).toBe(2); // today = last bucket
|
|
expect(result.slice(0, 29).every((b) => b.value === 0)).toBe(true);
|
|
});
|
|
|
|
it("getOwnerGrowth returns a cumulative total, starting from the pre-window baseline", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
|
|
mockPrisma.owner.count.mockResolvedValue(5); // owners registered before the window
|
|
const now = new Date();
|
|
mockPrisma.owner.findMany.mockResolvedValue([{ createdAt: now }]);
|
|
|
|
const result = await caller.admin.getOwnerGrowth({ granularity: "day" });
|
|
|
|
expect(result).toHaveLength(30);
|
|
expect(result[0]!.value).toBe(5); // baseline carried through empty leading buckets
|
|
expect(result[29]!.value).toBe(6); // baseline + 1 new owner today
|
|
});
|
|
});
|
|
|
|
describe("admin.listUsers", () => {
|
|
it("merges Clerk email/name onto each owner (Gitea #6)", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
|
|
mockPrisma.owner.findMany.mockResolvedValue([
|
|
{ id: "owner-2", pets: [], ban: null, adminRole: null, _count: { pets: 0 } },
|
|
]);
|
|
mockGetUserList.mockResolvedValue({
|
|
data: [
|
|
{
|
|
id: "owner-2",
|
|
emailAddresses: [{ id: "email-1", emailAddress: "owner2@example.com" }],
|
|
primaryEmailAddressId: "email-1",
|
|
firstName: "Jane",
|
|
lastName: "Doe",
|
|
createdAt: 1700000000000,
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await caller.admin.listUsers({ limit: 20 });
|
|
|
|
expect(result.users[0]!.clerk).toEqual({
|
|
email: "owner2@example.com",
|
|
name: "Jane Doe",
|
|
createdAt: new Date(1700000000000),
|
|
});
|
|
});
|
|
|
|
it("returns clerk: null when the Clerk profile is missing (e.g. deleted account)", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
|
|
mockPrisma.owner.findMany.mockResolvedValue([
|
|
{ id: "owner-3", pets: [], ban: null, adminRole: null, _count: { pets: 0 } },
|
|
]);
|
|
mockGetUserList.mockResolvedValue({ data: [] });
|
|
|
|
const result = await caller.admin.listUsers({ limit: 20 });
|
|
|
|
expect(result.users[0]!.clerk).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("admin.startTeamConversation", () => {
|
|
it("creates the Team pet on first use and starts a conversation", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
|
|
mockPrisma.pet.findUnique.mockResolvedValue({ id: "target-pet-1" });
|
|
mockPrisma.pet.findFirst.mockResolvedValue(null); // no Team pet yet
|
|
mockPrisma.owner.upsert.mockResolvedValue({ id: "admin-1" });
|
|
mockPrisma.species.findFirst.mockResolvedValue({ id: "species-1" });
|
|
mockPrisma.pet.create.mockResolvedValue({ id: "team-pet-1" });
|
|
mockPrisma.conversation.upsert.mockResolvedValue({ id: "conv-1" });
|
|
|
|
const result = await caller.admin.startTeamConversation({ targetPetId: "target-pet-1" });
|
|
|
|
expect(mockPrisma.pet.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({ ownerId: "admin-1", name: "PawFeed Team" }),
|
|
})
|
|
);
|
|
expect(result).toEqual({ conversationId: "conv-1", teamPetId: "team-pet-1" });
|
|
});
|
|
|
|
it("reuses an existing Team pet instead of creating a duplicate", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
|
|
mockPrisma.pet.findUnique.mockResolvedValue({ id: "target-pet-1" });
|
|
mockPrisma.pet.findFirst.mockResolvedValue({ id: "team-pet-existing" });
|
|
mockPrisma.conversation.upsert.mockResolvedValue({ id: "conv-2" });
|
|
|
|
const result = await caller.admin.startTeamConversation({ targetPetId: "target-pet-1" });
|
|
|
|
expect(mockPrisma.pet.create).not.toHaveBeenCalled();
|
|
expect(result.teamPetId).toBe("team-pet-existing");
|
|
});
|
|
|
|
it("throws NOT_FOUND when the target pet does not exist", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
|
|
mockPrisma.pet.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(
|
|
caller.admin.startTeamConversation({ targetPetId: "missing-pet" })
|
|
).rejects.toThrow(TRPCError);
|
|
});
|
|
|
|
it("throws BAD_REQUEST when the target pet is the Team pet itself", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
|
|
mockPrisma.pet.findUnique.mockResolvedValue({ id: "team-pet-existing" });
|
|
mockPrisma.pet.findFirst.mockResolvedValue({ id: "team-pet-existing" });
|
|
|
|
await expect(
|
|
caller.admin.startTeamConversation({ targetPetId: "team-pet-existing" })
|
|
).rejects.toThrow(TRPCError);
|
|
});
|
|
|
|
it("throws FORBIDDEN for a non-admin caller", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "someone-else");
|
|
const { caller } = await createAdminCaller();
|
|
|
|
await expect(
|
|
caller.admin.startTeamConversation({ targetPetId: "target-pet-1" })
|
|
).rejects.toThrow(TRPCError);
|
|
});
|
|
});
|
|
|
|
describe("admin.exportUserData", () => {
|
|
it("uploads the export, creates a download token, emails the pawfeed.org link, and logs a moderation entry", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
vi.stubEnv("NEXT_PUBLIC_APP_URL", "https://pawfeed.org");
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
|
|
mockBuildDataExport.mockResolvedValue({
|
|
account: { clerkProfile: { email: "owner4@example.com" } },
|
|
});
|
|
mockUploadExport.mockResolvedValue(undefined);
|
|
mockPrisma.dataExportToken.create.mockResolvedValue({ token: "export-token-abc" });
|
|
mockSendMail.mockResolvedValue(undefined);
|
|
mockPrisma.moderationLog.create.mockResolvedValue({});
|
|
|
|
const result = await caller.admin.exportUserData({ ownerId: "owner-4" });
|
|
|
|
expect(mockPrisma.dataExportToken.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({ ownerId: "owner-4" }),
|
|
})
|
|
);
|
|
expect(mockSendMail).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
to: "owner4@example.com",
|
|
text: expect.stringContaining("https://pawfeed.org/api/gdpr-export/export-token-abc"),
|
|
})
|
|
);
|
|
expect(mockPrisma.moderationLog.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({
|
|
action: "DATA_EXPORT",
|
|
targetType: "USER",
|
|
targetId: "owner-4",
|
|
}),
|
|
})
|
|
);
|
|
expect(result).toEqual({ success: true, sentTo: "owner4@example.com" });
|
|
});
|
|
|
|
it("throws NOT_FOUND when the owner does not exist", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller } = await createAdminCaller();
|
|
|
|
mockBuildDataExport.mockResolvedValue(null);
|
|
|
|
await expect(
|
|
caller.admin.exportUserData({ ownerId: "missing-owner" })
|
|
).rejects.toThrow(TRPCError);
|
|
expect(mockSendMail).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("throws PRECONDITION_FAILED when the owner has no Clerk email on file", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "admin-1");
|
|
const { caller } = await createAdminCaller();
|
|
|
|
mockBuildDataExport.mockResolvedValue({ account: { clerkProfile: null } });
|
|
|
|
await expect(
|
|
caller.admin.exportUserData({ ownerId: "owner-5" })
|
|
).rejects.toThrow(TRPCError);
|
|
expect(mockSendMail).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("throws FORBIDDEN for a non-admin caller", async () => {
|
|
vi.stubEnv("ADMIN_OWNER_ID", "someone-else");
|
|
const { caller } = await createAdminCaller();
|
|
|
|
await expect(
|
|
caller.admin.exportUserData({ ownerId: "owner-4" })
|
|
).rejects.toThrow(TRPCError);
|
|
expect(mockBuildDataExport).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("admin.getActiveAds", () => {
|
|
const adUnfiltered = { id: "ad-all", species: [], breeds: [] };
|
|
const adDogsOnly = { id: "ad-dogs", species: ["species-dog"], breeds: [] };
|
|
const adSiameseCatsOnly = { id: "ad-siamese", species: ["species-cat"], breeds: ["breed-siamese"] };
|
|
|
|
it("returns [] when the pet does not exist", async () => {
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
mockPrisma.pet.findUnique.mockResolvedValue(null);
|
|
|
|
const result = await caller.admin.getActiveAds({ petId: "missing-pet" });
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("returns untargeted ads and matching-species ads, excludes non-matching species", async () => {
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
mockPrisma.pet.findUnique.mockResolvedValue({ speciesId: "species-dog", breedId: null });
|
|
mockPrisma.advertisement.findMany.mockResolvedValue([adUnfiltered, adDogsOnly, adSiameseCatsOnly]);
|
|
|
|
const result = await caller.admin.getActiveAds({ petId: "pet-dog-1" });
|
|
|
|
expect(result.map((a) => a.id)).toEqual(["ad-all", "ad-dogs"]);
|
|
});
|
|
|
|
it("only shows a breed-narrowed ad to a pet with the matching breed", async () => {
|
|
const { caller, mockPrisma } = await createAdminCaller();
|
|
mockPrisma.advertisement.findMany.mockResolvedValue([adUnfiltered, adSiameseCatsOnly]);
|
|
|
|
mockPrisma.pet.findUnique.mockResolvedValue({ speciesId: "species-cat", breedId: "breed-siamese" });
|
|
const matching = await caller.admin.getActiveAds({ petId: "pet-siamese" });
|
|
expect(matching.map((a) => a.id)).toEqual(["ad-all", "ad-siamese"]);
|
|
|
|
mockPrisma.pet.findUnique.mockResolvedValue({ speciesId: "species-cat", breedId: "breed-persian" });
|
|
const nonMatching = await caller.admin.getActiveAds({ petId: "pet-persian" });
|
|
expect(nonMatching.map((a) => a.id)).toEqual(["ad-all"]);
|
|
|
|
mockPrisma.pet.findUnique.mockResolvedValue({ speciesId: "species-cat", breedId: null });
|
|
const noBreedSet = await caller.admin.getActiveAds({ petId: "pet-mixed-cat" });
|
|
expect(noBreedSet.map((a) => a.id)).toEqual(["ad-all"]);
|
|
});
|
|
|
|
it("does not require admin role — any authenticated owner can fetch ads for their pet", async () => {
|
|
const { caller, mockPrisma } = await createAdminCaller({ userId: "regular-owner" });
|
|
mockPrisma.pet.findUnique.mockResolvedValue({ speciesId: "species-dog", breedId: null });
|
|
mockPrisma.advertisement.findMany.mockResolvedValue([adUnfiltered]);
|
|
|
|
await expect(caller.admin.getActiveAds({ petId: "pet-dog-1" })).resolves.toEqual([adUnfiltered]);
|
|
});
|
|
});
|