Files
insforge--insforge/backend/tests/unit/database-fkey-composite.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

331 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { ForeignKeySchema } from '@insforge/shared-schemas';
import { AppError } from '../../src/utils/errors';
// ---------------------------------------------------------------------------
// Mocks shared across both describe blocks
// ---------------------------------------------------------------------------
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,
})),
})),
clearColumnTypeCache: vi.fn(),
},
}));
// ---------------------------------------------------------------------------
// Imports after mocks
// ---------------------------------------------------------------------------
import { AdminRecordService } from '../../src/services/database/admin-record.service';
import { DatabaseTableService } from '../../src/services/database/database-table.service';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function makeService() {
return DatabaseTableService.getInstance();
}
// Typed accessor for the private getFkeyConstraints method under test (no `any`).
type FkeyConstraintsAccessor = {
getFkeyConstraints(schemaName: string, table: string): Promise<ForeignKeySchema[]>;
};
function getFkeyConstraints(
service: DatabaseTableService,
schemaName: string,
table: string
): Promise<ForeignKeySchema[]> {
return (service as unknown as FkeyConstraintsAccessor).getFkeyConstraints(schemaName, table);
}
function makeAdminService() {
return AdminRecordService.getInstance();
}
// ---------------------------------------------------------------------------
// Tests getFkeyConstraints
// ---------------------------------------------------------------------------
describe('DatabaseTableService getFkeyConstraints', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('correctly pairs source and reference columns in a composite FK (positional join)', async () => {
const fkRows = [
{
constraint_name: 'fk_composite_test_ref_composite_tenant_id_item_id',
from_column: 'tenant_id',
foreign_schema: 'public',
foreign_table: 'ref_composite',
foreign_column: 'tenant_id',
ordinal_position: 1,
on_delete: 'NO ACTION',
on_update: 'CASCADE',
},
{
constraint_name: 'fk_composite_test_ref_composite_tenant_id_item_id',
from_column: 'item_id',
foreign_schema: 'public',
foreign_table: 'ref_composite',
foreign_column: 'item_id',
ordinal_position: 2,
on_delete: 'NO ACTION',
on_update: 'CASCADE',
},
];
poolQueryMock.mockResolvedValueOnce({ rows: fkRows });
const service = makeService();
const fks = await getFkeyConstraints(service, 'public', 'composite_test');
// One entity per constraint, carrying all column pairs in ordinal order.
expect(fks).toHaveLength(1);
const entry = fks[0];
expect(entry.constraintName).toBe('fk_composite_test_ref_composite_tenant_id_item_id');
expect(entry.referenceTable).toBe('ref_composite');
expect(entry.referenceColumns).toEqual([
{ sourceColumn: 'tenant_id', referenceColumn: 'tenant_id' },
{ sourceColumn: 'item_id', referenceColumn: 'item_id' },
]);
});
it('does not cross-join columns when reference column values are duplicated across tuples', async () => {
// Simulate two rows in the referenced table that share a common column value:
// (tenant_a, item_1)
// (tenant_a, item_2)
// The positional join must pair tenant_id→tenant_id and item_id→item_id,
// never tenant_id→item_id.
const fkRows = [
{
constraint_name: 'fk_child_parent_tenant_id_item_id',
from_column: 'tenant_id',
foreign_schema: 'public',
foreign_table: 'parent_composite',
foreign_column: 'tenant_id',
ordinal_position: 1,
on_delete: 'RESTRICT',
on_update: 'CASCADE',
},
{
constraint_name: 'fk_child_parent_tenant_id_item_id',
from_column: 'item_id',
foreign_schema: 'public',
foreign_table: 'parent_composite',
foreign_column: 'item_id',
ordinal_position: 2,
on_delete: 'RESTRICT',
on_update: 'CASCADE',
},
];
poolQueryMock.mockResolvedValueOnce({ rows: fkRows });
const service = makeService();
const fks = await getFkeyConstraints(service, 'public', 'child');
// The positional join must pair tenant_id→tenant_id and item_id→item_id.
expect(fks).toHaveLength(1);
expect(fks[0].referenceColumns).toEqual([
{ sourceColumn: 'tenant_id', referenceColumn: 'tenant_id' },
{ sourceColumn: 'item_id', referenceColumn: 'item_id' },
]);
});
it('handles single-column FKs without breaking', async () => {
const fkRows = [
{
constraint_name: 'fk_orders_user_id',
from_column: 'user_id',
foreign_schema: 'public',
foreign_table: 'users',
foreign_column: 'id',
ordinal_position: 1,
on_delete: 'CASCADE',
on_update: 'CASCADE',
},
];
poolQueryMock.mockResolvedValueOnce({ rows: fkRows });
const service = makeService();
const fks = await getFkeyConstraints(service, 'public', 'orders');
expect(fks).toHaveLength(1);
const entry = fks[0];
expect(entry.referenceColumns).toEqual([{ sourceColumn: 'user_id', referenceColumn: 'id' }]);
expect(entry.referenceTable).toBe('users');
expect(entry.onDelete).toBe('CASCADE');
expect(entry.onUpdate).toBe('CASCADE');
});
it('returns an empty list when table has no foreign keys', async () => {
poolQueryMock.mockResolvedValueOnce({ rows: [] });
const service = makeService();
const fks = await getFkeyConstraints(service, 'public', 'no_fk_table');
expect(fks).toEqual([]);
});
});
// ---------------------------------------------------------------------------
// Tests lookupRecord (multi-column)
// ---------------------------------------------------------------------------
describe('AdminRecordService lookupRecord (composite FK)', () => {
beforeEach(() => {
vi.clearAllMocks();
connectMock.mockResolvedValue({
query: clientQueryMock,
release: releaseMock,
});
});
it('builds multi-column WHERE clause and returns the correct record', async () => {
// Simulate metadata query + lookup query
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('SELECT * FROM')) {
return {
rows: [
{
tenant_id: 'tenant_a',
item_id: 'item_2',
label: 'item A2',
},
],
};
}
return { rows: [] };
});
const service = makeAdminService();
const result = await service.lookupRecord(
'public',
'parent_composite',
['tenant_id', 'item_id'],
['tenant_a', 'item_2']
);
// Verify the correct row was returned
expect(result).toEqual({
tenant_id: 'tenant_a',
item_id: 'item_2',
label: 'item A2',
});
// Verify the SQL uses multi-column WHERE
const dataQueryCall = clientQueryMock.mock.calls.find(
([sql]: [string]) => typeof sql === 'string' && sql.includes('SELECT * FROM')
);
expect(dataQueryCall).toBeDefined();
expect(dataQueryCall![0] as string).toMatch(
/WHERE\s+"tenant_id"\s*=\s*\$1\s+AND\s+"item_id"\s*=\s*\$2\s+LIMIT\s+1/i
);
expect(dataQueryCall![1]).toEqual(['tenant_a', 'item_2']);
});
it('returns null when no row matches the composite key', 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('SELECT * FROM')) {
return { rows: [] };
}
return { rows: [] };
});
const service = makeAdminService();
const result = await service.lookupRecord(
'public',
'parent_composite',
['tenant_id', 'item_id'],
['tenant_a', 'nonexistent']
);
expect(result).toBeNull();
});
it('throws when columns and values have mismatched lengths', async () => {
const service = makeAdminService();
await expect(
service.lookupRecord('public', 't', ['tenant_id', 'item_id'], ['tenant_a'])
).rejects.toBeInstanceOf(AppError);
});
it('correctly distinguishes between rows that share a column value', async () => {
// Two rows share the same tenant_id but have different item_ids.
// Lookup by (tenant_a, item_1) must return the first row, not the second.
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('SELECT * FROM')) {
return {
rows: [
{
tenant_id: 'tenant_a',
item_id: 'item_1',
label: 'item A1',
},
],
};
}
return { rows: [] };
});
const service = makeAdminService();
const result = await service.lookupRecord(
'public',
'parent_composite',
['tenant_id', 'item_id'],
['tenant_a', 'item_1']
);
expect(result).toEqual({
tenant_id: 'tenant_a',
item_id: 'item_1',
label: 'item A1',
});
// Confirm the exact params passed to the data query
const dataQueryCall = clientQueryMock.mock.calls.find(
([sql]: [string]) => typeof sql === 'string' && sql.includes('SELECT * FROM')
);
expect(dataQueryCall![1]).toEqual(['tenant_a', 'item_1']);
});
});