User asked for the admin review tooling — and the lyra-bot community post / notification text that goes out with each submission — to know whether a submission is a website-domain or a mail-sender-domain. Until now the type lived only on user_custom_domains and the submission inherited it implicitly via the foreign key. Reading it back for the admin list or the lyra prompt meant joining the source row every time. - migration 20260516_domain_submission_type adds a type column to rebreak.domain_submissions with a default of 'web' and backfills every existing row from its linked user_custom_domains.type. The backfill is idempotent (UPDATE … FROM with the type comparison). - Composite index (type, status) so the admin pending-list can scope by category without scanning the whole table. - submitDomainForReview now copies the source row's type into the new submission. The submit endpoint picks it up to vary the auto-generated community-vote post copy: a website framing for type='web' and an "Mail-Absender"-framing for type='mail_domain'. The user's nickname is the only PII referenced. - adminApproveSubmission returns the type alongside the domain so the approve endpoint's Lyra-bot Groq prompt can swap its subject/action labels per category. Reject path unchanged — the notification just carries the bare domain string, no type framing needed. - BlocklistDomain stays type-agnostic on purpose. The mail-daemon's getBlocklistedDomainsSet is a flat string-set match against sender domain or URL host, and works for both categories without splitting. Adding a type there would be redundant work in v1.0 — revisit only if we ever need a UI to surface what category each global entry came from. 38/38 backend tests pass (8 admin/domains, 30 plan-limits including 5 new for the type-copy semantics and community-post text variants).
152 lines
4.7 KiB
TypeScript
152 lines
4.7 KiB
TypeScript
/**
|
|
* Tests für getPendingSubmissions (Domain-Approval-Queue).
|
|
*
|
|
* Schwerpunkt: Sort-Order-Garantie + Deadline-Computation.
|
|
* - Legend > Pro > Free Plan-Priority
|
|
* - Innerhalb gleicher Priority: älteste createdAt zuerst (FIFO)
|
|
* - deadlineAt = createdAt + 24h, msUntilDeadline negativ wenn überfällig
|
|
*/
|
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
const prismaMock = vi.hoisted(() => ({
|
|
domainSubmission: {
|
|
findMany: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("../../server/utils/prisma", () => ({
|
|
usePrisma: () => prismaMock,
|
|
}));
|
|
|
|
import {
|
|
getPendingSubmissions,
|
|
ADMIN_APPROVAL_SLA_MS,
|
|
} from "../../server/db/domains";
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
function makeRow(overrides: Partial<Record<string, unknown>> = {}) {
|
|
return {
|
|
id: "sub-id",
|
|
domain: "example.com",
|
|
type: "web",
|
|
yesVotes: 0,
|
|
noVotes: 0,
|
|
status: "in_review",
|
|
createdAt: new Date("2026-05-09T10:00:00Z"),
|
|
userId: "user-id",
|
|
postId: null,
|
|
customDomain: { id: "cd-id" },
|
|
user: { id: "user-id", nickname: "nick", plan: "free" },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("getPendingSubmissions — Plan-Priority + Deadline", () => {
|
|
it("sortiert Legend vor Pro vor Free (Plan-Priority)", async () => {
|
|
const sameTime = new Date("2026-05-09T10:00:00Z");
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({
|
|
id: "free-1",
|
|
createdAt: sameTime,
|
|
user: { id: "u1", nickname: "a", plan: "free" },
|
|
}),
|
|
makeRow({
|
|
id: "legend-1",
|
|
createdAt: sameTime,
|
|
user: { id: "u2", nickname: "b", plan: "legend" },
|
|
}),
|
|
makeRow({
|
|
id: "pro-1",
|
|
createdAt: sameTime,
|
|
user: { id: "u3", nickname: "c", plan: "pro" },
|
|
}),
|
|
]);
|
|
|
|
const result = await getPendingSubmissions();
|
|
expect(result.map((r) => r.id)).toEqual(["legend-1", "pro-1", "free-1"]);
|
|
});
|
|
|
|
it("innerhalb gleicher Plan-Priority: älteste zuerst (FIFO)", async () => {
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({
|
|
id: "legend-newer",
|
|
createdAt: new Date("2026-05-09T12:00:00Z"),
|
|
user: { id: "u1", nickname: "x", plan: "legend" },
|
|
}),
|
|
makeRow({
|
|
id: "legend-older",
|
|
createdAt: new Date("2026-05-09T08:00:00Z"),
|
|
user: { id: "u2", nickname: "y", plan: "legend" },
|
|
}),
|
|
]);
|
|
|
|
const result = await getPendingSubmissions();
|
|
expect(result.map((r) => r.id)).toEqual(["legend-older", "legend-newer"]);
|
|
});
|
|
|
|
it("berechnet deadlineAt = createdAt + 24h pro row", async () => {
|
|
const created = new Date("2026-05-09T10:00:00Z");
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({ createdAt: created }),
|
|
]);
|
|
|
|
const result = await getPendingSubmissions();
|
|
const expectedDeadline = new Date(
|
|
created.getTime() + ADMIN_APPROVAL_SLA_MS,
|
|
);
|
|
expect(result[0]!.deadlineAt.toISOString()).toBe(
|
|
expectedDeadline.toISOString(),
|
|
);
|
|
});
|
|
|
|
it("msUntilDeadline ist negativ wenn Submission überfällig (>24h alt)", async () => {
|
|
// 30h alte Submission → 6h überfällig → ms negativ
|
|
const created = new Date(Date.now() - 30 * 60 * 60 * 1000);
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({ createdAt: created }),
|
|
]);
|
|
|
|
const result = await getPendingSubmissions();
|
|
expect(result[0]!.msUntilDeadline).toBeLessThan(0);
|
|
});
|
|
|
|
it("planPriority fällt auf 0 zurück wenn user.plan unbekannt / null", async () => {
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({ user: null }),
|
|
]);
|
|
const result = await getPendingSubmissions();
|
|
expect(result[0]!.planPriority).toBe(0);
|
|
});
|
|
|
|
it("returned type='web' für Web-Submission", async () => {
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({ id: "web-sub", domain: "casino.de", type: "web" }),
|
|
]);
|
|
const result = await getPendingSubmissions();
|
|
expect(result[0]!.type).toBe("web");
|
|
});
|
|
|
|
it("returned type='mail_domain' für Mail-Submission", async () => {
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({
|
|
id: "mail-sub",
|
|
domain: "mailing.casino-affiliate.com",
|
|
type: "mail_domain",
|
|
}),
|
|
]);
|
|
const result = await getPendingSubmissions();
|
|
expect(result[0]!.type).toBe("mail_domain");
|
|
});
|
|
|
|
it("type ist im Response-Objekt vorhanden (passthrough)", async () => {
|
|
prismaMock.domainSubmission.findMany.mockResolvedValueOnce([
|
|
makeRow({ type: "mail_domain" }),
|
|
]);
|
|
const result = await getPendingSubmissions();
|
|
expect(Object.prototype.hasOwnProperty.call(result[0], "type")).toBe(true);
|
|
});
|
|
});
|