Files
TREK/server/tests/unit/db/instance-api-key-backfill.test.ts
Maurice 724c88c201 style: format server and shared with prettier
A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
2026-10-10 21:23:37 +02:00

166 lines
6.4 KiB
TypeScript

/**
* Unit test for the instance API-key backfill migration (#1939).
*
* The Places/Unsplash credential moved from "whichever admin row the resolver
* happened to find" to an instance-wide app_settings row. An install that
* upgrades must keep searching with the key it already had, so the migration
* copies the value the old resolver would have handed out — the lowest-id admin
* who has one — and leaves the users columns alone. It only does so where that
* value was the whole install's key: as soon as a second row holds one, the
* promotion is skipped, because the instance row outranks everybody's own
* column and nobody may be moved onto a stranger's key by an upgrade.
*
* Ported off the legacy runner (Task 0 triage: PORT) onto the real
* `Migration20200101031100_1939`: migrate to the step immediately before it,
* seed rows with raw SQL, apply just that one migration, assert.
*/
import { createMigrationOrm, migrateTo, pendingNames, rawExec, rawQuery } from '../../helpers/migration-step';
import type { MikroORM } from '@mikro-orm/sqlite';
import { describe, it, expect } from 'vitest';
const TARGET = 'Migration20200101031100_1939';
async function ormBeforeTarget(): Promise<MikroORM> {
const orm = await createMigrationOrm();
const names = await pendingNames(orm);
const idx = names.indexOf(TARGET);
expect(idx).toBeGreaterThan(0);
await migrateTo(orm, names[idx - 1]);
return orm;
}
async function seedUser(
orm: MikroORM,
id: number,
role: 'admin' | 'user',
keys: { maps?: string; unsplash?: string } = {},
): Promise<void> {
await rawExec(
orm,
`INSERT INTO users (id, username, email, password_hash, role, maps_api_key, unsplash_api_key)
VALUES (?, ?, ?, 'x', ?, ?, ?)`,
[id, `u${id}`, `u${id}@test.local`, role, keys.maps ?? null, keys.unsplash ?? null],
);
}
async function setting(orm: MikroORM, key: string): Promise<string | undefined> {
const rows = await rawQuery<{ value: string }>(orm, 'SELECT value FROM app_settings WHERE key = ?', [key]);
return rows[0]?.value;
}
describe('instance API-key backfill migration', () => {
it('KEYFILL-001: copies the one key the install was searching with and keeps the column', async () => {
const orm = await ormBeforeTarget();
try {
await seedUser(orm, 1, 'admin', { maps: 'the-admin-google', unsplash: 'the-admin-unsplash' });
await seedUser(orm, 2, 'user');
await migrateTo(orm, TARGET);
expect(await setting(orm, 'maps_api_key')).toBe('the-admin-google');
expect(await setting(orm, 'unsplash_api_key')).toBe('the-admin-unsplash');
// Nothing is taken away: the columns are still the per-user fallback.
const rows = await rawQuery<{ maps_api_key: string }>(orm, 'SELECT maps_api_key FROM users WHERE id = 1');
expect(rows[0].maps_api_key).toBe('the-admin-google');
} finally {
await orm.close(true);
}
}, 30000);
it('KEYFILL-002: skips an admin whose column is empty and takes the next one', async () => {
const orm = await ormBeforeTarget();
try {
await seedUser(orm, 1, 'admin', { maps: '' });
await seedUser(orm, 2, 'admin', { maps: 'the-only-real-key' });
await migrateTo(orm, TARGET);
expect(await setting(orm, 'maps_api_key')).toBe('the-only-real-key');
} finally {
await orm.close(true);
}
}, 30000);
it('KEYFILL-003: never overwrites a value the admin has already saved instance-wide', async () => {
const orm = await ormBeforeTarget();
try {
await seedUser(orm, 1, 'admin', { maps: 'old-column-key' });
await rawExec(orm, "INSERT INTO app_settings (key, value) VALUES ('maps_api_key', 'already-instance-wide')");
await migrateTo(orm, TARGET);
expect(await setting(orm, 'maps_api_key')).toBe('already-instance-wide');
} finally {
await orm.close(true);
}
}, 30000);
it('KEYFILL-004: ignores a non-admin key and writes no row at all', async () => {
const orm = await ormBeforeTarget();
try {
await seedUser(orm, 1, 'user', { maps: 'members-own-key' });
await migrateTo(orm, TARGET);
// A member's key stays theirs — the resolver still finds it for them, and
// promoting it would hand their billing to the whole instance.
expect(await setting(orm, 'maps_api_key')).toBeUndefined();
} finally {
await orm.close(true);
}
}, 30000);
it('KEYFILL-005: writes nothing on an install that never had a key', async () => {
const orm = await ormBeforeTarget();
try {
await seedUser(orm, 1, 'admin');
await migrateTo(orm, TARGET);
expect(await setting(orm, 'maps_api_key')).toBeUndefined();
expect(await setting(orm, 'unsplash_api_key')).toBeUndefined();
} finally {
await orm.close(true);
}
}, 30000);
it('KEYFILL-006: leaves a column alone once a member holds a key of their own', async () => {
const orm = await ormBeforeTarget();
try {
await seedUser(orm, 1, 'admin', { maps: 'admins-own-google', unsplash: 'admins-own-unsplash' });
await seedUser(orm, 7, 'user', { maps: 'members-own-google' });
await migrateTo(orm, TARGET);
// Member 7 pays for their own Google key. Promoting the admin's would put
// every one of their searches on his key and his bill, because the instance
// row is resolved before their own column.
expect(await setting(orm, 'maps_api_key')).toBeUndefined();
// Decided per column: nobody else has an Unsplash key, so that one really
// was the whole install's and stays it.
expect(await setting(orm, 'unsplash_api_key')).toBe('admins-own-unsplash');
const rows = await rawQuery<{ maps_api_key: string }>(orm, 'SELECT maps_api_key FROM users WHERE id = 7');
expect(rows[0].maps_api_key).toBe('members-own-google');
} finally {
await orm.close(true);
}
}, 30000);
it('KEYFILL-007: two admins with their own keys both keep them', async () => {
const orm = await ormBeforeTarget();
try {
await seedUser(orm, 1, 'admin', { maps: 'first-admin-google' });
await seedUser(orm, 2, 'admin', { maps: 'second-admin-google' });
await migrateTo(orm, TARGET);
expect(await setting(orm, 'maps_api_key')).toBeUndefined();
const rows = await rawQuery<{ maps_api_key: string }>(orm, 'SELECT maps_api_key FROM users ORDER BY id');
expect(rows.map((r) => r.maps_api_key)).toEqual(['first-admin-google', 'second-admin-google']);
} finally {
await orm.close(true);
}
}, 30000);
});