Files
insforge--insforge/backend/tests/unit/admin-record.service.test.ts
wehub-resource-sync 3a28426bf4
Lint and Format Check / lint-and-format (push) Failing after 0s
Check Migrations / Check for duplicate migration numbers (push) Failing after 1s
CI Pre-merge Check / CI Pre-merge Check (push) Failing after 2m17s
chore: import upstream snapshot with attribution
2026-07-13 12:23:40 +08:00

630 lines
23 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AppError } from '../../src/utils/errors';
const { poolQueryMock, connectMock, clientQueryMock, releaseMock } = vi.hoisted(() => ({
poolQueryMock: vi.fn(),
connectMock: vi.fn(),
clientQueryMock: vi.fn(),
releaseMock: vi.fn(),
}));
vi.mock('../../src/infra/database/database.manager', () => ({
DatabaseManager: {
getInstance: vi.fn(() => ({
getPool: vi.fn(() => ({
query: poolQueryMock,
connect: connectMock,
})),
})),
},
}));
import { AdminRecordService } from '../../src/services/database/admin-record.service';
describe('AdminRecordService', () => {
beforeEach(() => {
vi.clearAllMocks();
connectMock.mockResolvedValue({
query: clientQueryMock,
release: releaseMock,
});
});
it('reads protected-schema records through direct SQL with search and sorting', async () => {
clientQueryMock.mockImplementation(async (sql: string) => {
if (sql.includes('FROM information_schema.columns')) {
return {
rows: [
{ column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' },
{ column_name: 'email', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
],
};
}
if (sql.includes('COUNT(*)::text AS total')) {
return { rows: [{ total: '2' }] };
}
if (sql.includes('SELECT * FROM "auth"."users"')) {
return {
rows: [
{ id: '1', email: 'demo-1@example.com' },
{ id: '2', email: 'demo-2@example.com' },
],
};
}
return { rows: [] };
});
const service = AdminRecordService.getInstance();
const result = await service.listRecords('auth', 'users', {
limit: 10,
offset: 0,
search: 'demo',
sort: [{ columnName: 'email', direction: 'asc' }],
});
expect(result.total).toBe(2);
expect(result.records).toHaveLength(2);
const sqlCalls = clientQueryMock.mock.calls.map(([sql]) => sql as string);
const beginIndex = sqlCalls.indexOf('BEGIN');
const setRoleIndex = sqlCalls.indexOf('SET LOCAL ROLE project_admin');
const dataQueryIndex = sqlCalls.findIndex((sql) =>
sql.includes('ORDER BY "email" ASC LIMIT $2 OFFSET $3')
);
const resetRoleIndex = sqlCalls.indexOf('RESET ROLE');
const commitIndex = sqlCalls.indexOf('COMMIT');
expect(beginIndex).toBe(0);
expect(setRoleIndex).toBeGreaterThan(-1);
expect(dataQueryIndex).toBeGreaterThan(setRoleIndex);
expect(resetRoleIndex).toBeGreaterThan(dataQueryIndex);
expect(commitIndex).toBeGreaterThan(resetRoleIndex);
expect(clientQueryMock.mock.calls[dataQueryIndex]?.[1]).toEqual(['%demo%', 10, 0]);
// Read paths must not pay for the primary-key metadata query.
expect(sqlCalls.some((sql) => sql.includes('information_schema.table_constraints'))).toBe(
false
);
});
it('delegates protected-schema writes to project_admin privileges', async () => {
clientQueryMock
.mockResolvedValueOnce({}) // BEGIN
.mockResolvedValueOnce({}) // SET LOCAL ROLE
.mockResolvedValueOnce({}) // set request.jwt.claims
.mockResolvedValueOnce({
rows: [
{ column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' },
{ column_name: 'email', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
],
}) // metadata
.mockResolvedValueOnce({
rows: [{ id: 'u1', email: 'demo@example.com' }],
}) // INSERT
.mockResolvedValueOnce({}) // RESET ROLE
.mockResolvedValueOnce({}) // reset config
.mockResolvedValueOnce({}); // COMMIT
const service = AdminRecordService.getInstance();
const result = await service.createRecords('auth', 'users', [{ email: 'demo@example.com' }]);
expect(result).toEqual([{ id: 'u1', email: 'demo@example.com' }]);
const sqlCalls = clientQueryMock.mock.calls.map(([sql]) => sql as string);
const setRoleIndex = sqlCalls.indexOf('SET LOCAL ROLE project_admin');
const insertIndex = sqlCalls.findIndex((sql) => sql.includes('INSERT INTO "auth"."users"'));
const commitIndex = sqlCalls.indexOf('COMMIT');
expect(sqlCalls[0]).toBe('BEGIN');
expect(setRoleIndex).toBeGreaterThan(-1);
expect(insertIndex).toBeGreaterThan(setRoleIndex);
expect(commitIndex).toBeGreaterThan(insertIndex);
});
it('updates public records and converts blank nullable uuid values to null', async () => {
clientQueryMock.mockImplementation(async (sql: string) => {
if (sql.includes('FROM information_schema.columns')) {
return {
rows: [
{ column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' },
{ column_name: 'owner_id', data_type: 'uuid', is_nullable: 'YES', udt_name: 'uuid' },
{ column_name: 'name', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
],
};
}
if (sql.includes('information_schema.table_constraints')) {
return { rows: [{ column_name: 'id' }] };
}
if (sql.includes('UPDATE "public"."projects"')) {
return {
rows: [{ id: 'p1', owner_id: null, name: 'Renamed project' }],
};
}
return { rows: [] };
});
const service = AdminRecordService.getInstance();
const record = await service.updateRecord(
'public',
'projects',
{ id: 'p1' },
{
owner_id: '',
name: 'Renamed project',
}
);
expect(record).toEqual({ id: 'p1', owner_id: null, name: 'Renamed project' });
const updateCall = clientQueryMock.mock.calls.find(
([sql]) => typeof sql === 'string' && sql.includes('UPDATE "public"."projects"')
);
expect(updateCall?.[0]).toContain(
'SET "owner_id" = $1, "name" = $2 WHERE "id" = $3 RETURNING *'
);
expect(updateCall?.[1]).toEqual([null, 'Renamed project', 'p1']);
const sqlCalls = clientQueryMock.mock.calls.map(([sql]) => sql as string);
const setRoleIndex = sqlCalls.indexOf('SET LOCAL ROLE project_admin');
const updateIndex = sqlCalls.findIndex((sql) => sql.includes('UPDATE "public"."projects"'));
const commitIndex = sqlCalls.indexOf('COMMIT');
expect(sqlCalls[0]).toBe('BEGIN');
expect(setRoleIndex).toBeGreaterThan(-1);
expect(updateIndex).toBeGreaterThan(setRoleIndex);
expect(commitIndex).toBeGreaterThan(updateIndex);
});
it('preserves empty strings for character varying inserts', async () => {
clientQueryMock
.mockResolvedValueOnce({}) // BEGIN
.mockResolvedValueOnce({}) // SET LOCAL ROLE
.mockResolvedValueOnce({}) // set_config
.mockResolvedValueOnce({
rows: [
{ column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' },
{
column_name: 'name',
data_type: 'character varying',
is_nullable: 'YES',
udt_name: 'varchar',
},
],
}) // metadata
.mockResolvedValueOnce({
rows: [{ id: 'r1', name: '' }],
}) // INSERT
.mockResolvedValueOnce({}) // RESET ROLE
.mockResolvedValueOnce({}) // reset config
.mockResolvedValueOnce({}); // COMMIT
const service = AdminRecordService.getInstance();
const result = await service.createRecords('public', 'projects', [{ name: '' }]);
expect(result).toEqual([{ id: 'r1', name: '' }]);
const insertCall = clientQueryMock.mock.calls.find(
([sql]) => typeof sql === 'string' && sql.includes('INSERT INTO "public"."projects"')
);
expect(insertCall?.[1]).toEqual(['']);
});
it('converts blank updates on nullable non-text columns to null', async () => {
clientQueryMock.mockImplementation(async (sql: string) => {
if (sql.includes('FROM information_schema.columns')) {
return {
rows: [
{ column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' },
{
column_name: 'priority',
data_type: 'integer',
is_nullable: 'YES',
udt_name: 'int4',
},
],
};
}
if (sql.includes('information_schema.table_constraints')) {
return { rows: [{ column_name: 'id' }] };
}
if (sql.includes('UPDATE "public"."projects"')) {
return {
rows: [{ id: 'p1', priority: null }],
};
}
return { rows: [] };
});
const service = AdminRecordService.getInstance();
const record = await service.updateRecord(
'public',
'projects',
{ id: 'p1' },
{
priority: '',
}
);
expect(record).toEqual({ id: 'p1', priority: null });
const updateCall = clientQueryMock.mock.calls.find(
([sql]) => typeof sql === 'string' && sql.includes('UPDATE "public"."projects"')
);
expect(updateCall?.[1]).toEqual([null, 'p1']);
});
it('rejects blank updates on required non-text columns with a 400', async () => {
clientQueryMock.mockImplementation(async (sql: string) => {
if (sql.includes('FROM information_schema.columns')) {
return {
rows: [
{ column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' },
{
column_name: 'priority',
data_type: 'integer',
is_nullable: 'NO',
udt_name: 'int4',
},
],
};
}
return { rows: [] };
});
const service = AdminRecordService.getInstance();
await expect(
service.updateRecord(
'public',
'projects',
{ id: 'p1' },
{
priority: '',
}
)
).rejects.toBeInstanceOf(AppError);
const sqlCalls = clientQueryMock.mock.calls.map(([sql]) => sql as string);
expect(sqlCalls).toContain('ROLLBACK');
});
it('deletes records inside a project_admin transaction', async () => {
clientQueryMock.mockImplementation(async (sql: string) => {
if (sql.includes('FROM information_schema.columns')) {
return {
rows: [
{ column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' },
{ column_name: 'name', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
],
};
}
if (sql.includes('information_schema.table_constraints')) {
return { rows: [{ column_name: 'id' }] };
}
if (sql.includes('DELETE FROM "public"."projects"')) {
return { rowCount: 2, rows: [] };
}
return { rows: [], rowCount: 0 };
});
const service = AdminRecordService.getInstance();
const deletedCount = await service.deleteRecords('public', 'projects', [
{ id: 'p1' },
{ id: 'p2' },
]);
expect(deletedCount).toBe(2);
const deleteCall = clientQueryMock.mock.calls.find(
([sql]) => typeof sql === 'string' && sql.includes('DELETE FROM "public"."projects"')
);
// Each selected single-column key is matched as its own AND-group, OR'd together.
expect(deleteCall?.[0]).toContain('WHERE ("id" = $1) OR ("id" = $2)');
expect(deleteCall?.[1]).toEqual(['p1', 'p2']);
const sqlCalls = clientQueryMock.mock.calls.map(([sql]) => sql as string);
const setRoleIndex = sqlCalls.indexOf('SET LOCAL ROLE project_admin');
const deleteIndex = sqlCalls.findIndex((sql) =>
sql.includes('DELETE FROM "public"."projects"')
);
const commitIndex = sqlCalls.indexOf('COMMIT');
expect(sqlCalls[0]).toBe('BEGIN');
expect(deleteIndex).toBeGreaterThan(setRoleIndex);
expect(commitIndex).toBeGreaterThan(deleteIndex);
});
it('updates a composite-key row using the full primary-key tuple', async () => {
clientQueryMock.mockImplementation(async (sql: string) => {
if (sql.includes('FROM information_schema.columns')) {
return {
rows: [
{ column_name: 'tenant_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
{ column_name: 'item_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
{ column_name: 'label', data_type: 'text', is_nullable: 'YES', udt_name: 'text' },
],
};
}
if (sql.includes('information_schema.table_constraints')) {
return { rows: [{ column_name: 'tenant_id' }, { column_name: 'item_id' }] };
}
if (sql.includes('UPDATE "public"."composite_pk_test"')) {
return {
rows: [{ tenant_id: 'tenant_a', item_id: 'item_2', label: 'second-updated' }],
};
}
return { rows: [] };
});
const service = AdminRecordService.getInstance();
const record = await service.updateRecord(
'public',
'composite_pk_test',
{ tenant_id: 'tenant_a', item_id: 'item_2' },
{ label: 'second-updated' }
);
expect(record).toEqual({ tenant_id: 'tenant_a', item_id: 'item_2', label: 'second-updated' });
const updateCall = clientQueryMock.mock.calls.find(
([sql]) => typeof sql === 'string' && sql.includes('UPDATE "public"."composite_pk_test"')
);
// The WHERE clause must match both key columns, not just the (duplicated) first one.
expect(updateCall?.[0]).toContain(
'SET "label" = $1 WHERE "tenant_id" = $2 AND "item_id" = $3 RETURNING *'
);
expect(updateCall?.[1]).toEqual(['second-updated', 'tenant_a', 'item_2']);
});
it('deletes only the selected composite-key tuples', async () => {
clientQueryMock.mockImplementation(async (sql: string) => {
if (sql.includes('FROM information_schema.columns')) {
return {
rows: [
{ column_name: 'tenant_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
{ column_name: 'item_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
{ column_name: 'label', data_type: 'text', is_nullable: 'YES', udt_name: 'text' },
],
};
}
if (sql.includes('information_schema.table_constraints')) {
return { rows: [{ column_name: 'tenant_id' }, { column_name: 'item_id' }] };
}
if (sql.includes('DELETE FROM "public"."composite_pk_test"')) {
return { rowCount: 1, rows: [] };
}
return { rows: [], rowCount: 0 };
});
const service = AdminRecordService.getInstance();
// tenant_a is duplicated across rows; deleting must target the exact tuple only.
const deletedCount = await service.deleteRecords('public', 'composite_pk_test', [
{ tenant_id: 'tenant_a', item_id: 'item_2' },
]);
expect(deletedCount).toBe(1);
const deleteCall = clientQueryMock.mock.calls.find(
([sql]) => typeof sql === 'string' && sql.includes('DELETE FROM "public"."composite_pk_test"')
);
expect(deleteCall?.[0]).toContain('WHERE ("tenant_id" = $1 AND "item_id" = $2)');
expect(deleteCall?.[1]).toEqual(['tenant_a', 'item_2']);
});
it('matches each selected composite tuple independently when deleting many rows', async () => {
clientQueryMock.mockImplementation(async (sql: string) => {
if (sql.includes('FROM information_schema.columns')) {
return {
rows: [
{ column_name: 'tenant_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
{ column_name: 'item_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
],
};
}
if (sql.includes('information_schema.table_constraints')) {
return { rows: [{ column_name: 'tenant_id' }, { column_name: 'item_id' }] };
}
if (sql.includes('DELETE FROM "public"."composite_pk_test"')) {
return { rowCount: 2, rows: [] };
}
return { rows: [], rowCount: 0 };
});
const service = AdminRecordService.getInstance();
const deletedCount = await service.deleteRecords('public', 'composite_pk_test', [
{ tenant_id: 'tenant_a', item_id: 'item_1' },
{ tenant_id: 'tenant_a', item_id: 'item_2' },
]);
expect(deletedCount).toBe(2);
const deleteCall = clientQueryMock.mock.calls.find(
([sql]) => typeof sql === 'string' && sql.includes('DELETE FROM "public"."composite_pk_test"')
);
expect(deleteCall?.[0]).toContain(
'WHERE ("tenant_id" = $1 AND "item_id" = $2) OR ("tenant_id" = $3 AND "item_id" = $4)'
);
expect(deleteCall?.[1]).toEqual(['tenant_a', 'item_1', 'tenant_a', 'item_2']);
});
it('rejects a partial composite key instead of mutating unintended rows', async () => {
clientQueryMock.mockImplementation(async (sql: string) => {
if (sql.includes('FROM information_schema.columns')) {
return {
rows: [
{ column_name: 'tenant_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
{ column_name: 'item_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
{ column_name: 'label', data_type: 'text', is_nullable: 'YES', udt_name: 'text' },
],
};
}
if (sql.includes('information_schema.table_constraints')) {
return { rows: [{ column_name: 'tenant_id' }, { column_name: 'item_id' }] };
}
return { rows: [], rowCount: 0 };
});
const service = AdminRecordService.getInstance();
// Supplying only the first PK column of a composite key must be rejected, not
// turned into a broad DELETE that removes every row sharing that value.
await expect(
service.deleteRecords('public', 'composite_pk_test', [{ tenant_id: 'tenant_a' }])
).rejects.toBeInstanceOf(AppError);
const sqlCalls = clientQueryMock.mock.calls.map(([sql]) => sql as string);
expect(sqlCalls.some((sql) => sql.includes('DELETE FROM'))).toBe(false);
expect(sqlCalls).toContain('ROLLBACK');
// The same partial key must also be rejected on update.
await expect(
service.updateRecord('public', 'composite_pk_test', { tenant_id: 'tenant_a' }, { label: 'x' })
).rejects.toBeInstanceOf(AppError);
expect(
clientQueryMock.mock.calls.some(
([sql]) => typeof sql === 'string' && sql.includes('UPDATE "public"."composite_pk_test"')
)
).toBe(false);
});
it('falls back to the caller-provided key when the table has no primary key', async () => {
clientQueryMock.mockImplementation(async (sql: string) => {
if (sql.includes('FROM information_schema.columns')) {
return {
rows: [
{ column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' },
{ column_name: 'name', data_type: 'text', is_nullable: 'YES', udt_name: 'text' },
],
};
}
if (sql.includes('information_schema.table_constraints')) {
return { rows: [] }; // table has no primary key
}
if (sql.includes('UPDATE "public"."pkless"')) {
return { rows: [{ id: 'p1', name: 'Renamed' }] };
}
if (sql.includes('DELETE FROM "public"."pkless"')) {
return { rowCount: 1, rows: [] };
}
return { rows: [], rowCount: 0 };
});
const service = AdminRecordService.getInstance();
// With no detectable primary key, the supplied key columns are used as-is.
const updated = await service.updateRecord(
'public',
'pkless',
{ id: 'p1' },
{ name: 'Renamed' }
);
expect(updated).toEqual({ id: 'p1', name: 'Renamed' });
const updateCall = clientQueryMock.mock.calls.find(
([sql]) => typeof sql === 'string' && sql.includes('UPDATE "public"."pkless"')
);
expect(updateCall?.[0]).toContain('WHERE "id" = $2 RETURNING *');
expect(updateCall?.[1]).toEqual(['Renamed', 'p1']);
const deletedCount = await service.deleteRecords('public', 'pkless', [{ id: 'p1' }]);
expect(deletedCount).toBe(1);
const deleteCall = clientQueryMock.mock.calls.find(
([sql]) => typeof sql === 'string' && sql.includes('DELETE FROM "public"."pkless"')
);
expect(deleteCall?.[0]).toContain('WHERE ("id" = $1)');
expect(deleteCall?.[1]).toEqual(['p1']);
});
it('matches a null key column with IS NULL on a keyless table', async () => {
clientQueryMock.mockImplementation(async (sql: string) => {
if (sql.includes('FROM information_schema.columns')) {
return {
rows: [
{ column_name: 'name', data_type: 'text', is_nullable: 'YES', udt_name: 'text' },
{ column_name: 'email', data_type: 'text', is_nullable: 'YES', udt_name: 'text' },
],
};
}
if (sql.includes('information_schema.table_constraints')) {
return { rows: [] }; // table has no primary key
}
if (sql.includes('UPDATE "public"."pkless"')) {
return { rows: [{ name: 'alice', email: 'fixed@x.com' }] };
}
if (sql.includes('DELETE FROM "public"."pkless"')) {
return { rowCount: 1, rows: [] };
}
return { rows: [], rowCount: 0 };
});
const service = AdminRecordService.getInstance();
// A genuinely-null column in the all-columns fallback key must match the row
// with `IS NULL`, not `= ''` (which would silently match nothing).
const updated = await service.updateRecord(
'public',
'pkless',
{ name: 'alice', email: null },
{ email: 'fixed@x.com' }
);
expect(updated).toEqual({ name: 'alice', email: 'fixed@x.com' });
const updateCall = clientQueryMock.mock.calls.find(
([sql]) => typeof sql === 'string' && sql.includes('UPDATE "public"."pkless"')
);
// The null component becomes `IS NULL` and binds no parameter for it.
expect(updateCall?.[0]).toContain('WHERE "name" = $2 AND "email" IS NULL RETURNING *');
expect(updateCall?.[1]).toEqual(['fixed@x.com', 'alice']);
const deletedCount = await service.deleteRecords('public', 'pkless', [
{ name: 'alice', email: null },
]);
expect(deletedCount).toBe(1);
const deleteCall = clientQueryMock.mock.calls.find(
([sql]) => typeof sql === 'string' && sql.includes('DELETE FROM "public"."pkless"')
);
expect(deleteCall?.[0]).toContain('WHERE ("name" = $1 AND "email" IS NULL)');
expect(deleteCall?.[1]).toEqual(['alice']);
});
it('discards the pooled client when admin transaction cleanup fails', async () => {
const resetError = new Error('reset failed');
clientQueryMock.mockImplementation(async (sql: string) => {
if (sql.includes('FROM information_schema.columns')) {
return {
rows: [
{ column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' },
{ column_name: 'name', data_type: 'text', is_nullable: 'NO', udt_name: 'text' },
],
};
}
if (sql.includes('information_schema.table_constraints')) {
return { rows: [{ column_name: 'id' }] };
}
if (sql.includes('UPDATE "public"."projects"')) {
return { rows: [{ id: 'p1', name: 'Renamed project' }] };
}
if (sql === 'RESET ROLE') {
throw resetError;
}
return { rows: [], rowCount: 0 };
});
const service = AdminRecordService.getInstance();
await expect(
service.updateRecord(
'public',
'projects',
{ id: 'p1' },
{
name: 'Renamed project',
}
)
).rejects.toBe(resetError);
expect(clientQueryMock.mock.calls.map(([sql]) => sql as string)).toContain('ROLLBACK');
expect(releaseMock).toHaveBeenCalledWith(resetError);
});
});