Files
redefined-designs/backend/tests/integration/setup/testDb.ts
T
bermudalambandClaude Opus 5 0b6cc85c4f
Linting / lint (pull_request) Successful in 2m55s
SonarQube Analysis / sonarqube (pull_request) Successful in 30m43s
fix(lint): bring the backend test suites into scope (#298)
The backend lint script covered src and scripts; the frontend's has always covered src and tests. So roughly sixty backend test files had never been linted at all.

That was a documented deferral rather than an oversight — the config said so in as many words, because tsconfig.json includes only src and type-aware rules had no program to resolve the test files against. tsconfig.test.json is that program, exactly as frontend/tsconfig.test.json was for the same problem in #137. It is separate from tsconfig.json rather than a widening of it, because that one drives the build and emits to dist, and pulling the suite in would ship the tests. The files were already type-checked at run time by ts-jest; this adds nothing to that, only to what the linter can see.

Pointing it at tests produced 77 warnings and no errors. Sixty of those were rules that cannot be true in a test, so they are switched off here rather than left to accumulate — #60's argument, that a gate nobody reads is not a gate, and that a rule which cannot be true is noise hiding the rules that can. Forty-one alone were hardcoded passwords, which are the entire point of a test and which this project's own rule says must live only in test paths, which is here. The rest were a stub server on http to a socket the test opened itself, an RFC 5737 documentation IP, os.tmpdir, Math.random for a run id, and sorting two arrays to compare them.

What was left was signal, and it found a real one on the first run. testDb.ts cleaned up settings with LIKE 'email\_%', and in a JavaScript string that backslash does nothing: the pattern is 'email_%', and an underscore in SQL LIKE matches any single character. It meant "email plus any one character" rather than "email_". It deleted the right rows only because no other key begins with those letters followed by something else — a setting called emailing_enabled would have been swept away between suites, silently, in a file that never mentions it. It now uses an explicit ESCAPE clause.

It also found five dead `const before: string[] = []` declarations in uploadValidation, left over from #228's redesign of that suite. The tests assert properly through filesSettlingTo; the variables did nothing.

Seven warnings remain, all in routesAreWrapped and workflowGate, and all judgement calls about guard-test complexity rather than defects. Leaving them visible is the point of having lint here at all.

Closes #298

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 12:41:58 -05:00

141 lines
5.5 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
});
}
/**
* The tables `resetDb` truncates. Also, therefore, exactly the set whose absence
* makes this suite meaningless.
*/
const REQUIRED_TABLES = [
'admin_settings',
'cart_items',
'carts',
'categories',
'checkout_items',
'checkouts',
'customer_sessions',
'customer_tokens',
'customers',
'favorites',
'item_drafts',
'item_images',
'item_tags',
'items',
'orders',
'shipping_addresses',
'tags',
'upload_links'
] as const;
/** Which of them the database does not currently have. */
async function missingTables(): Promise<string[]> {
const { rows } = await testPool.query<{ table_name: string }>(
`SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'`
);
const present = new Set(rows.map((row) => row.table_name));
return REQUIRED_TABLES.filter((name) => !present.has(name));
}
/**
* Says plainly when the schema has gone, rather than letting the suite report
* it as unrelated logic failures.
*
* In #154 the integration run lost its schema partway through — a Postgres
* service container recreated mid-run comes back with an empty data directory —
* and it presented as 36 assertion errors about categories, price filters and
* favourite notifications. The real message, `relation "items" does not exist`,
* was further down the same log. Hours went into chasing the assertions.
*
* The cause is still open and needs runner-side evidence. This is the half that
* can be fixed from here: whatever the cause, the next occurrence should read as
* "the database lost its schema" on the first line.
*/
export async function assertSchemaPresent(context: string): Promise<void> {
const missing = await missingTables();
if (missing.length === 0) return;
throw new Error(
`The test database has no schema (${context}).
` +
`Missing ${missing.length} of ${REQUIRED_TABLES.length} tables: ${missing.join(', ')}.
` +
`Migrations ran at the start of this run, so the schema existed and has since gone. ` +
`Nothing in this suite drops tables — TRUNCATE does not — so the database itself was ` +
`replaced or restarted underneath the run. On CI the likeliest cause is the Postgres ` +
`service container being recreated, which comes back with an empty data directory. ` +
`See #154.`
);
}
export async function resetDb(): Promise<void> {
// The schema check runs only when the truncate fails, not on every reset.
// resetDb runs in a beforeEach several hundred times a suite, and an extra
// round trip each time to guard against something that has happened once
// would be paying continuously for a rare event. A failure here is where the
// information is worth having.
try {
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
`);
} catch (err) {
await assertSchemaPresent('while resetting between tests');
// The schema is there, so this is not #154 — let the original speak.
throw err;
}
// 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.
//
// Same reasoning, same failure, for the intake settings (#227): a ceiling of
// 1 left behind by the ceiling suite makes every later submission refuse with
// a 503, in files that never mention a ceiling.
//
// ESCAPE, and not a backslash, because the backslash that used to be here did
// nothing. In a JavaScript string 'email\_%' is 'email_%', and `_` in SQL LIKE
// matches any single character — so the pattern meant "email plus any one
// character", not "email_". It happened to delete the right rows only because
// no other key begins with those letters followed by something else; a
// setting called emailing_enabled would have been swept away silently. Found
// by no-useless-escape the first time lint was pointed at tests/ (#298).
await testPool.query(
`DELETE FROM admin_settings WHERE key LIKE 'email!_%' ESCAPE '!' OR key LIKE 'intake!_%' ESCAPE '!'`
);
}
export async function closeDb(): Promise<void> {
await testPool.end();
}