fix(polls): enforce shared persistence invariants
This commit is contained in:
1 parent
c841595458
commit
50598472ad
7 files changed
+655
-169
No files matched your search
+58
-8
@@ -486,14 +486,17 @@ describe("Content database mutation runtime partial updates", () => {
|
||||
});
|
||||
|
||||
it("canonicalizes submitted question options and preserves omitted columns", async () => {
|
||||
database.selected({
|
||||
id: 7,
|
||||
pollId: 3,
|
||||
question: "Favourite colour?",
|
||||
type: "single",
|
||||
sortOrder: 0,
|
||||
options: "Red\nBlue",
|
||||
});
|
||||
database.queueSelected(
|
||||
{
|
||||
id: 7,
|
||||
pollId: 3,
|
||||
question: "Favourite colour?",
|
||||
type: "single",
|
||||
sortOrder: 0,
|
||||
options: "Red\nBlue",
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
await executeContentDatabaseMutation(
|
||||
"poll-question.change",
|
||||
@@ -508,6 +511,53 @@ describe("Content database mutation runtime partial updates", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects semantic question edits after votes exist", async () => {
|
||||
database.queueSelected(
|
||||
{
|
||||
id: 7,
|
||||
pollId: 3,
|
||||
question: "Favourite colour?",
|
||||
type: "single",
|
||||
sortOrder: 0,
|
||||
options: "Red\nBlue",
|
||||
},
|
||||
{ id: 91 },
|
||||
);
|
||||
|
||||
await expect(
|
||||
executeContentDatabaseMutation(
|
||||
"poll-question.change",
|
||||
{ action: "update", id: 7, options: "Green\nYellow" },
|
||||
context,
|
||||
undefined,
|
||||
),
|
||||
).rejects.toMatchObject({ code: "CONFLICT" });
|
||||
expect(database.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows sort-order-only question edits after votes exist", async () => {
|
||||
database.queueSelected(
|
||||
{
|
||||
id: 7,
|
||||
pollId: 3,
|
||||
question: "Favourite colour?",
|
||||
type: "single",
|
||||
sortOrder: 0,
|
||||
options: "Red\nBlue",
|
||||
},
|
||||
{ id: 91 },
|
||||
);
|
||||
|
||||
await executeContentDatabaseMutation(
|
||||
"poll-question.change",
|
||||
{ action: "update", id: 7, sortOrder: 2 },
|
||||
context,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(database.set).toHaveBeenCalledWith({ sortOrder: 2 });
|
||||
});
|
||||
|
||||
it("blocks deletion of an event type referenced by an event", async () => {
|
||||
database.queueSelected({ id: 7, name: "Tournament" }, { id: 11 });
|
||||
|
||||
|
||||
@@ -26,7 +26,13 @@ import {
|
||||
} from "@/lib/db";
|
||||
import { slugify } from "@/lib/format";
|
||||
import { canonicalize } from "@/lib/foundation/security";
|
||||
import { serializePollOptions } from "@/lib/polls/poll-semantics";
|
||||
import {
|
||||
hasPollQuestionSemanticChange,
|
||||
POLL_PUBLIC_QUESTION_LIMIT,
|
||||
type PollQuestionType,
|
||||
pollQuestionLimitReached,
|
||||
serializePollOptions,
|
||||
} from "@/lib/polls/poll-semantics";
|
||||
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||
import {
|
||||
createEventSchema,
|
||||
@@ -682,7 +688,7 @@ async function pollChange(
|
||||
if (!merged.success) throw validation(merged.error);
|
||||
if (
|
||||
merged.data.status === "active" &&
|
||||
(await pollQuestionCount(connection, id)) > 50
|
||||
(await pollQuestionCount(connection, id)) > POLL_PUBLIC_QUESTION_LIMIT
|
||||
) {
|
||||
throw conflict();
|
||||
}
|
||||
@@ -713,10 +719,7 @@ async function pollQuestionChange(
|
||||
.for("update");
|
||||
if (!parent) throw notFound();
|
||||
const questionCount = await pollQuestionCount(connection, pollId);
|
||||
if (
|
||||
questionCount >= 100 ||
|
||||
(parent.status === "active" && questionCount >= 50)
|
||||
) {
|
||||
if (pollQuestionLimitReached(parent.status, questionCount)) {
|
||||
throw conflict();
|
||||
}
|
||||
const parsed = pollQuestionSchema.safeParse(data);
|
||||
@@ -783,6 +786,19 @@ async function pollQuestionChange(
|
||||
merged.data.options,
|
||||
);
|
||||
}
|
||||
if (
|
||||
hasPollQuestionSemanticChange(
|
||||
{ ...existing, type: existing.type as PollQuestionType },
|
||||
merged.data,
|
||||
)
|
||||
) {
|
||||
const [vote] = await connection
|
||||
.select({ id: WebsitePollVote.id })
|
||||
.from(WebsitePollVote)
|
||||
.where(eq(WebsitePollVote.questionId, id))
|
||||
.limit(1);
|
||||
if (vote) throw conflict();
|
||||
}
|
||||
await connection
|
||||
.update(WebsitePollQuestion)
|
||||
.set(values)
|
||||
|
||||
Reference in new issue
Block a user