Files
redefined-designs/backend/tests/integration/setup/testDb.ts
T
bermudalamb 6df32af784 feat(intake): add upload_links and item_drafts, and default an item's price (#222)
The schema for the intake pipeline. A submission becomes an `items` row at status 'pending' — already invisible to every public and storefront query since #90 — with an `item_drafts` row beside it holding the submitter's note, which link it arrived through, and the fields the drafting worker will fill in later.

`upload_links` stores a digest rather than a token, so a leaked database is not also a leaked set of working links, and the admin screen can show a token exactly once. `max_submissions` is nullable for "no cap", but the route will default it to a finite number: an unbounded link should be something asked for, not something that happens when nobody thought about it.

`item_drafts.upload_link_id` is ON DELETE SET NULL rather than CASCADE. Deleting a link must not delete the items that arrived through it — provenance is lost, the goods are not.

`items.price_cents` keeps NOT NULL and gains a default of 80.00, so an arriving item is always priced. That is the decision taken in the design review over making the column nullable: it costs the schema-level guarantee that nothing can publish at a price nobody chose, and buys not having to teach the cart, the checkout and thirteen other files about an item without a price. The protection moves into the review queue, and `price_source` exists so that queue can say whether a number came from a model, the default, or a person.

The number lives in the migration rather than in configuration. Changing a default price is a rare, deliberate act that deserves a record; an environment variable would let it drift silently between environments, and a wrong default is invisible until something has already sold at it.

Verified up, down and up again rather than only forwards — an irreversible migration is one that cannot be tested. Then verified by inspection rather than assumption: the default reads 8000, both tables and the state index exist, and an item inserted with no price comes back at 8000.

Backend: 263 integration, 302 unit, all passing against the new schema.

Ref #222
2026-08-31 15:42:25 -05:00

48 lines
1.9 KiB
TypeScript
Executable File

import { Pool } from 'pg';
import { runner } from 'node-pg-migrate';
import path from 'path';
export const testPool = new Pool({
host: process.env.TEST_PGHOST || 'localhost',
port: parseInt(process.env.TEST_PGPORT || '55432', 10),
user: process.env.TEST_PGUSER || 'redefined_test',
password: process.env.TEST_PGPASSWORD || 'redefined_test',
database: process.env.TEST_PGDATABASE || 'redefined_test'
});
export async function migrate(): Promise<void> {
await runner({
databaseUrl: {
host: process.env.TEST_PGHOST || 'localhost',
port: parseInt(process.env.TEST_PGPORT || '55432', 10),
user: process.env.TEST_PGUSER || 'redefined_test',
password: process.env.TEST_PGPASSWORD || 'redefined_test',
database: process.env.TEST_PGDATABASE || 'redefined_test'
},
dir: path.resolve(__dirname, '..', '..', '..', 'migrations'),
direction: 'up',
migrationsTable: 'pgmigrations',
count: Infinity
});
}
export async function resetDb(): Promise<void> {
await testPool.query(`
TRUNCATE TABLE item_drafts, upload_links, orders, checkout_items, checkouts,
shipping_addresses, cart_items, carts, customer_tokens, customer_sessions, favorites,
customers, item_tags, item_images, items, tags, categories
RESTART IDENTITY CASCADE
`);
// admin_settings is not truncated — it holds the seeded cart_expiry_hours
// default that other suites read. But the email template rows in it are test
// data like any other, and a stored template outliving the suite that wrote
// it silently changes the mail every later suite asserts on. That is not
// hypothetical: a subject of "Gone" written by the template tests reached the
// favorite-alert tests and made five of them fail somewhere else entirely.
await testPool.query(`DELETE FROM admin_settings WHERE key LIKE 'email\_%'`);
}
export async function closeDb(): Promise<void> {
await testPool.end();
}