Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions scripts/mutation/equivalent-mutants/shared-db.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,9 @@ src/shared/sumup/transaction.ts::readSumupTransaction.events~0aphohd ?? → ||
# sessionId that is a key of that same map. requiredMapValue can never miss
# there, and no input can observe the message on that unreachable throw.
src/shared/db/payment-claim.ts::settleAttendeeRows~1iaqh2o Refund settlement lost a payment row→"" # unreachable: the IN-clause is built from the same map's keys

# backup dump (shared/db/backup-snapshot.ts) — exportTable internals whose
# intermediate values are overwritten or type-guaranteed before any read.
src/shared/db/backup-snapshot.ts::exportTable.colList~1el369a → "mutated" # colList is only read when INSERT statements are built, which happens after the first non-empty page assigned it in the rowCount === 0 branch; the initializer is overwritten before every read (an empty table breaks out before any read)
src/shared/db/backup-snapshot.ts::exportTable.rows~0k5qahc ?? → || # suppliedPage is BackupRow[] | undefined: an array (even an empty one) is truthy and undefined is falsy, so ?? and || pick the same side for every possible value
src/shared/db/backup-snapshot.ts::exportTable.colList~0ou5ceg = → += # the rowCount === 0 branch runs at most once (rowCount grows past zero with the same page that enters it), so this appends to the initial empty string, and "" + x === x
7 changes: 7 additions & 0 deletions scripts/mutation/equivalent-mutants/ui-templates.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ src/ui/templates/admin/questions.tsx::questionListingsFor.listingNames~1vj5u51
src/ui/templates/admin/questions.tsx::adminQuestionsPage.columns.key~0eka7sm question → "" # a column `key` is read only by renderTable's columnKeys/hiddenKeys lookups and saved-filter value rendering; reorderableListPage renders the questions table with only reorder options — no columnKeys, hiddenKeys, or filter — so this key is never read
src/ui/templates/admin/questions.tsx::adminQuestionsPage.columns.key~1c98v7j answers → "" # same as questions.tsx:170:14 — the questions list table is rendered without columnKeys, hiddenKeys, or filters, so its column keys are never read
src/ui/templates/admin/questions.tsx::adminQuestionsPage.columns.key~08wyblh listings → "" # same as questions.tsx:170:14 — the questions list table is rendered without columnKeys, hiddenKeys, or filters, so its column keys are never read

# backups page (ui/templates/admin/backup.tsx) — column keys the page never
# reads back, same class as the questions list table above.
src/ui/templates/admin/backup.tsx::backupsTable.key~1353jpe created → "" # a column `key` is read only by renderTable's columnKeys/hiddenKeys lookups and saved-filter value rendering; adminBackupPage renders the backups table with none of those options, so this key is never read
src/ui/templates/admin/backup.tsx::backupsTable.key~1o7pzgq timestamp → "" # same as backup.tsx's created key — the backups table is rendered without columnKeys, hiddenKeys, or filters, so its column keys are never read
src/ui/templates/admin/backup.tsx::backupsTable.key~1tpb30l size → "" # same as backup.tsx's created key — the backups table is rendered without columnKeys, hiddenKeys, or filters, so its column keys are never read
src/ui/templates/admin/backup.tsx::backupsTable.key~120g5gv actions → "" # same as backup.tsx's created key — the backups table is rendered without columnKeys, hiddenKeys, or filters, so its column keys are never read
42 changes: 42 additions & 0 deletions test/shared/db/backup-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getDb } from "#shared/db/client.ts";
import { initDb, SCHEMA_TABLE_NAMES } from "#shared/db/migrations.ts";
import { describeWithEnv } from "#test-utils/db.ts";
import { createTestListing } from "#test-utils/db-helpers/listings.ts";
import { withEnv } from "#test-utils/env.ts";

describeWithEnv("backup snapshot", { db: true }, () => {
describe("exportTable", () => {
Expand Down Expand Up @@ -46,6 +47,17 @@ describeWithEnv("backup snapshot", { db: true }, () => {
expect(sql).toContain("NULL");
});

test("escapes single quotes by doubling them", async () => {
// Seeded at the storage layer with bound args, so nothing escapes the
// quote on the way in — only the dump's own escaping can double it.
await getDb().execute({
args: ["quote-test", "O'Brien's Gala"],
sql: "INSERT INTO settings (key, value) VALUES (?, ?)",
});
const { sql } = await exportTable("settings");
expect(sql).toContain("'O''Brien''s Gala'");
});

test("keyset-paginates across multiple pages without losing rows", async () => {
const pageOne = await createTestListing({ name: "Page One" });
const pageTwo = await createTestListing({ name: "Page Two" });
Expand All @@ -63,6 +75,20 @@ describeWithEnv("backup snapshot", { db: true }, () => {
expect(sql.match(new RegExp(`\\(${pageOne.id},`, "g"))).toHaveLength(1);
expect(sql.match(new RegExp(`\\(${pageTwo.id},`, "g"))).toHaveLength(1);
expect(sql.match(new RegExp(`\\(${pageThree.id},`, "g"))).toHaveLength(1);
// Statements are newline-separated so a dump stays readable.
expect(sql).toContain(';\nINSERT INTO "listings"');
});

test("the keyset cursor tracks the last row id, not a running sum", async () => {
// Three pages: a summed cursor would agree on page two (0 + last id)
// and only overshoot from page three on, silently dropping rows.
const ids: number[] = [];
for (let n = 1; n <= 5; n++) {
ids.push((await createTestListing({ name: `Cursor ${n}` })).id);
}
const { sql, rowCount } = await exportTable("listings", 2);
expect(rowCount).toBe(5);
expect(sql).toContain(`(${ids[4]},`);
});
});

Expand Down Expand Up @@ -91,6 +117,12 @@ describeWithEnv("backup snapshot", { db: true }, () => {
test("sums extra pages across tables", () => {
expect(backupDumpDatabaseCalls([500, 1000, 499], 500)).toBe(5);
});

test("reads the page size from BACKUP_PAGE_SIZE by default", () => {
using _env = withEnv({ BACKUP_PAGE_SIZE: "1" });
// Three one-row pages past the shared first-page batch: 2 + 3.
expect(backupDumpDatabaseCalls([3])).toBe(5);
});
});

describe("countSchemaTableRows", () => {
Expand All @@ -109,6 +141,16 @@ describeWithEnv("backup snapshot", { db: true }, () => {
expect(backups.map((b) => b.table)).toEqual(SCHEMA_TABLE_NAMES);
});

test("includes each table's first row", async () => {
// The batched first pages start their keyset cursor below every real
// rowid; a cursor of 1 would silently drop each table's first row.
const listing = await createTestListing({ name: "First Row" });
const backups = await createBackup();
const listings = backups.find((backup) => backup.table === "listings");
expect(listings?.rowCount).toBe(1);
expect(listings?.sql).toContain(`(${listing.id},`);
});

test("skips tables that do not exist", async () => {
await getDb().execute("DROP TABLE IF EXISTS holidays");
try {
Expand Down
14 changes: 14 additions & 0 deletions test/shared/storage/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { it as test } from "@std/testing/bdd";
import {
getStorageBackend,
runWithStorageConfig,
storageZoneName,
uploadRaw,
} from "#shared/storage.ts";
import { describeWithEnv } from "#test-utils/db.ts";
Expand Down Expand Up @@ -82,4 +83,17 @@ describeWithEnv("storage environment config", STORAGE_TEST_ENV, () => {
expect(getStorageBackend()).toBe("none");
});
});

test("names the zone when Bunny storage is active", () => {
runWithStorageConfig({ zoneKey: "mykey", zoneName: "myzone" }, () => {
expect(storageZoneName()).toBe("myzone");
});
});

test("has no zone name under local storage", async () => {
await withLocalStorageEnabled(async () => {
await Promise.resolve();
expect(storageZoneName()).toBe(null);
});
});
});