Skip to content

Commit 54fe7d6

Browse files
committed
test(db/postgres): cover withTransaction, lowercased backfill and updateRepo case handling
1 parent ea3a312 commit 54fe7d6

4 files changed

Lines changed: 74 additions & 2 deletions

File tree

test/db/postgres/helper.test.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,14 @@ vi.mock('../../../src/config', () => ({
6363
}));
6464

6565
describe('PostgreSQL - helper', async () => {
66-
const { connect, query, resetConnection, getSessionStore, ensureSessionStoreReady } =
67-
await import('../../../src/db/postgres/helper');
66+
const {
67+
connect,
68+
query,
69+
resetConnection,
70+
getSessionStore,
71+
ensureSessionStoreReady,
72+
withTransaction,
73+
} = await import('../../../src/db/postgres/helper');
6874

6975
beforeEach(async () => {
7076
vi.clearAllMocks();
@@ -288,6 +294,47 @@ describe('PostgreSQL - helper', async () => {
288294
});
289295
});
290296

297+
describe('withTransaction', () => {
298+
it('wraps the callback in BEGIN/COMMIT and releases the client', async () => {
299+
getDatabaseMock.mockReturnValue({
300+
type: 'postgres',
301+
enabled: true,
302+
connectionString: 'postgresql://localhost/x',
303+
});
304+
305+
const result = await withTransaction(async (client) => {
306+
await client.query('SELECT 1');
307+
return 'ok';
308+
});
309+
310+
expect(result).toBe('ok');
311+
const statements = mockClientQuery.mock.calls.map(([sql]) => sql);
312+
expect(statements[0]).toBe('BEGIN');
313+
expect(statements).toContain('SELECT 1');
314+
expect(statements[statements.length - 1]).toBe('COMMIT');
315+
expect(mockClientRelease).toHaveBeenCalledTimes(1);
316+
});
317+
318+
it('rolls back and rethrows when the callback fails', async () => {
319+
getDatabaseMock.mockReturnValue({
320+
type: 'postgres',
321+
enabled: true,
322+
connectionString: 'postgresql://localhost/x',
323+
});
324+
325+
await expect(
326+
withTransaction(async () => {
327+
throw new Error('boom');
328+
}),
329+
).rejects.toThrow('boom');
330+
331+
const statements = mockClientQuery.mock.calls.map(([sql]) => sql);
332+
expect(statements[0]).toBe('BEGIN');
333+
expect(statements[statements.length - 1]).toBe('ROLLBACK');
334+
expect(mockClientRelease).toHaveBeenCalledTimes(1);
335+
});
336+
});
337+
291338
describe('pool error handling', () => {
292339
it('registers an idle-client error listener that logs without crashing', async () => {
293340
getDatabaseMock.mockReturnValue({

test/db/postgres/repo.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,4 +383,15 @@ describe('PostgreSQL - Repo', async () => {
383383
expect(repos[0].lastModified).toBe('2026-01-02T00:00:00.000Z');
384384
});
385385
});
386+
387+
it('lowercases usernames when replacing permissions through updateRepo', async () => {
388+
mockQuery.mockResolvedValue({ rowCount: 1, rows: [] });
389+
390+
await updateRepo({ _id: 'r1', users: { canPush: ['Alice'], canAuthorise: ['BOB'] } });
391+
392+
const inserted = mockQuery.mock.calls
393+
.filter(([sql]) => /INSERT INTO repo_users/.test(String(sql)))
394+
.map(([, params]) => params?.[1]);
395+
expect(inserted).toEqual(['alice', 'bob']);
396+
});
386397
});

test/db/postgres/schemaMigrations.integration.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ describe.runIf(shouldRunPostgresTests)('PostgreSQL Schema Migration Integration
131131
const single = await seed('single', { canPush: ['alice'], canAuthorise: ['bob'] });
132132
const multi = await seed('multi', { canPush: ['amy', 'cara'], canAuthorise: ['dan'] });
133133
const both = await seed('both', { canPush: ['eve'], canAuthorise: ['eve'] });
134+
// Legacy entries can be mixed case; the runtime writers lowercase, so the
135+
// backfill must too or these users become unretrievable.
136+
const mixed = await seed('mixed', { canPush: ['Alice'], canAuthorise: ['ALICE', 'alice'] });
134137
const empty = await seed('empty', { canPush: [], canAuthorise: [] });
135138

136139
// Apply the remaining migrations: v3 creates repo_users + backfills, v4
@@ -171,6 +174,12 @@ describe.runIf(shouldRunPostgresTests)('PostgreSQL Schema Migration Integration
171174
{ username: 'eve', role: 'canAuthorise' },
172175
{ username: 'eve', role: 'canPush' },
173176
]);
177+
178+
// Mixed-case entries are lowercased and case-only duplicates collapse.
179+
expect(await permsOf(mixed)).toEqual([
180+
{ username: 'alice', role: 'canAuthorise' },
181+
{ username: 'alice', role: 'canPush' },
182+
]);
174183
// A repo with no permissions backfills nothing.
175184
expect(await permsOf(empty)).toEqual([]);
176185
} finally {

test/db/postgres/schemaMigrations.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,9 @@ describe('PostgreSQL - migrations', () => {
117117
await expect(runMigrations(pool as never)).rejects.toThrow('migration boom');
118118
expect(release).toHaveBeenCalledTimes(1);
119119
});
120+
121+
it('lowercases usernames in the repo_users backfill to match the runtime writers', () => {
122+
const v4 = MIGRATIONS.find((m) => m.version === 4);
123+
expect(v4?.sql).toContain('lower(elem.username)');
124+
});
120125
});

0 commit comments

Comments
 (0)