mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 23:16:46 +00:00
38f3cb3101
An adversarial review of what had already landed here turned up three that
were wrong, and they are the reason for this commit.
The CSP change removed 'unsafe-eval', which is not leftover: heic-to's
libheif build initialises embind through new Function(), and without it every
iPhone .heic upload fails in the browser. 'wasm-unsafe-eval' alone was tried
once already and was not enough (93b51a0b). Put back, with the reason written
down this time and the test pinning it rather than its absence.
The idempotency in-flight guard never fired. finalize() runs when the
handler's observable completes, but Nest writes the response - and therefore
stores the row - several microtasks later, so the waiting request always woke
up before the row existed and ran the handler a second time. It releases from
the point the row is written now; finalize is the backstop for handlers that
answer another way. The test drove the two in the opposite order from Nest,
which is why it passed either way.
And the trip-scoping check treated an id that resolves to nothing like one
belonging to someone else. accommodation_id has no foreign key, so shortening
a trip's dates leaves a booking pointing at a gap - which update() has always
healed by clearing the link, and which would have become a permanent 400.
The rest: a reply to a soft-deleted chat message is refused and the create
path blanks the quote like the list path does; the ws-token buckets are keyed
by account rather than by IP, where every user behind one NAT shared a
ceiling; the weather and vacay caps read through the stream, since a chunked
response has no content-length to check; the Google host gate takes country
domains again; abandoned response bodies are cancelled instead of left to GC;
the boot failure prints its errno; and a share link that is not yours to read
now says so instead of reporting that the journey has none.
TICKETJSON gets a real repair: the in-place GLOB fix only helps installs that
have not run it yet, so an appended migration puts back the note text the
case-insensitive LIKE cut off, for rows where what it stored is not valid
JSON. A genuine receipt always parses, so it cannot be touched.
Wiki: the audit log records the IP Express resolves after TRUST_PROXY, not
the leftmost X-Forwarded-For entry a caller can write. That is the right way
round, but it means the hop count has to be accurate - said plainly in both
places now.
125 lines
5.0 KiB
TypeScript
125 lines
5.0 KiB
TypeScript
/**
|
|
* Boot migration: move the itemized receipt out of budget_items.note (#1658).
|
|
*
|
|
* The costs UI stored the receipt as a `TICKETJSON:` prefix on the note, so the
|
|
* migration lifts it into its own column and clears the note. The match has to
|
|
* be case-SENSITIVE: LIKE is not, which would swallow a hand-written note that
|
|
* happens to start with the same word.
|
|
*
|
|
* The second half covers the databases that ran the LIKE version before it was
|
|
* repaired — a fixed step never replays, so the damage is undone by a migration
|
|
* of its own at the end of the array.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import Database from 'better-sqlite3';
|
|
import { createTables } from '../../../src/db/schema';
|
|
import { runMigrations } from '../../../src/db/migrations';
|
|
|
|
function makeDbWithNotes(): Database.Database {
|
|
const db = new Database(':memory:');
|
|
db.exec('PRAGMA journal_mode = WAL');
|
|
db.exec('PRAGMA busy_timeout = 5000');
|
|
db.exec('PRAGMA foreign_keys = ON');
|
|
createTables(db);
|
|
// Minimal FK chain: user -> trip -> budget_items rows.
|
|
db.prepare("INSERT INTO users (id, username, email, password_hash) VALUES (1, 'u', 'u@example.test', 'x')").run();
|
|
db.prepare("INSERT INTO trips (id, user_id, title) VALUES (1, 1, 'T')").run();
|
|
db.prepare(
|
|
'INSERT INTO budget_items (id, trip_id, name, note) VALUES (1, 1, ?, ?), (2, 1, ?, ?)',
|
|
).run('Dinner', 'TICKETJSON:{"items":[]}', 'Museum', 'ticketjson: buy at the door');
|
|
return db;
|
|
}
|
|
|
|
describe('budget_items ticket_json migration', () => {
|
|
it('moves a real receipt into ticket_json and clears the note', () => {
|
|
const db = makeDbWithNotes();
|
|
try {
|
|
runMigrations(db);
|
|
const row = db.prepare('SELECT note, ticket_json FROM budget_items WHERE id = 1').get() as {
|
|
note: string | null;
|
|
ticket_json: string | null;
|
|
};
|
|
expect(row).toEqual({ note: null, ticket_json: '{"items":[]}' });
|
|
} finally {
|
|
db.close();
|
|
}
|
|
});
|
|
|
|
it('leaves a lowercase note alone (LIKE would have eaten it)', () => {
|
|
const db = makeDbWithNotes();
|
|
try {
|
|
runMigrations(db);
|
|
const row = db.prepare('SELECT note, ticket_json FROM budget_items WHERE id = 2').get() as {
|
|
note: string | null;
|
|
ticket_json: string | null;
|
|
};
|
|
expect(row).toEqual({ note: 'ticketjson: buy at the door', ticket_json: null });
|
|
} finally {
|
|
db.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('recovering what the case-insensitive match destroyed', () => {
|
|
/**
|
|
* A database that already ran the buggy step: migrated to the tip, then wound
|
|
* back one version so only the recovery step is left to run — which is exactly
|
|
* what an existing install does on the next boot.
|
|
*/
|
|
function makeDamagedDb(): Database.Database {
|
|
const db = new Database(':memory:');
|
|
db.exec('PRAGMA journal_mode = WAL');
|
|
db.exec('PRAGMA busy_timeout = 5000');
|
|
db.exec('PRAGMA foreign_keys = ON');
|
|
createTables(db);
|
|
runMigrations(db);
|
|
db.prepare("INSERT INTO users (id, username, email, password_hash) VALUES (1, 'u', 'u@example.test', 'x')").run();
|
|
db.prepare("INSERT INTO trips (id, user_id, title) VALUES (1, 1, 'T')").run();
|
|
// Row 1: what the LIKE match left behind for "ticketjson: buy at the door".
|
|
// Row 2: an actual receipt. Row 3: a receipt on a row whose owner has since
|
|
// written a note.
|
|
db.prepare('INSERT INTO budget_items (id, trip_id, name, note, ticket_json) VALUES (?, ?, ?, ?, ?)')
|
|
.run(1, 1, 'Museum', null, ' buy at the door');
|
|
db.prepare('INSERT INTO budget_items (id, trip_id, name, note, ticket_json) VALUES (?, ?, ?, ?, ?)')
|
|
.run(2, 1, 'Dinner', null, '{"items":[{"name":"Beer","price":"4.50","parts":[1]}]}');
|
|
db.prepare('INSERT INTO budget_items (id, trip_id, name, note, ticket_json) VALUES (?, ?, ?, ?, ?)')
|
|
.run(3, 1, 'Taxi', 'split at the hotel', '{"items":[]}');
|
|
|
|
const version = (db.prepare('SELECT version FROM schema_version LIMIT 1').get() as { version: number }).version;
|
|
db.prepare('UPDATE schema_version SET version = ?').run(version - 1);
|
|
return db;
|
|
}
|
|
|
|
const read = (db: Database.Database, id: number) =>
|
|
db.prepare('SELECT note, ticket_json FROM budget_items WHERE id = ?').get(id) as {
|
|
note: string | null;
|
|
ticket_json: string | null;
|
|
};
|
|
|
|
it('puts a chopped note back where it came from', () => {
|
|
const db = makeDamagedDb();
|
|
try {
|
|
runMigrations(db);
|
|
expect(read(db, 1)).toEqual({ note: ' buy at the door', ticket_json: null });
|
|
} finally {
|
|
db.close();
|
|
}
|
|
});
|
|
|
|
it('leaves a genuine receipt alone', () => {
|
|
const db = makeDamagedDb();
|
|
try {
|
|
runMigrations(db);
|
|
expect(read(db, 2)).toEqual({
|
|
note: null,
|
|
ticket_json: '{"items":[{"name":"Beer","price":"4.50","parts":[1]}]}',
|
|
});
|
|
// A note written since the damage means the row was not chopped, so it is
|
|
// not a candidate at all.
|
|
expect(read(db, 3)).toEqual({ note: 'split at the hotel', ticket_json: '{"items":[]}' });
|
|
} finally {
|
|
db.close();
|
|
}
|
|
});
|
|
});
|