import { describe, it, expect } from 'vitest' import { classifyColumns, resolvePrincipal, buildPrincipalIndex, tableBaseName, sortFindings, type DbColumn, type DbFk, type DbTable, } from '../classify.js' const tables: DbTable[] = [ { schema: 'core', table: 'tenant_Tenants' }, { schema: 'core', table: 'auth_Users' }, { schema: 'core', table: 'ref_Departments' }, { schema: 'extensions', table: 'crm_Clients' }, { schema: 'extensions', table: 'crm_Orders' }, { schema: 'extensions', table: 'crm_Stages' }, ] const col = (column: string): DbColumn => ({ schema: 'extensions', table: 'crm_Orders', column, dataType: 'uniqueidentifier' }) function byColumn(findings: ReturnType): Record { return Object.fromEntries(findings.map((f) => [f.column, f.classification])) } describe('classify — principal resolution helpers', () => { it('tableBaseName strips the table prefix', () => { expect(tableBaseName('tenant_Tenants')).toBe('Tenants') expect(tableBaseName('crm_Clients')).toBe('Clients') expect(tableBaseName('NoPrefix')).toBe('NoPrefix') }) it('resolvePrincipal maps a *Id column to its table (singular + plural)', () => { const idx = buildPrincipalIndex(tables) expect(resolvePrincipal('TenantId', idx)).toBe('core.tenant_Tenants') expect(resolvePrincipal('ClientId', idx)).toBe('extensions.crm_Clients') expect(resolvePrincipal('DepartmentId', idx)).toBe('core.ref_Departments') expect(resolvePrincipal('CorrelationId', idx)).toBeNull() expect(resolvePrincipal('Id', idx)).toBeNull() }) }) describe('classify — column classification (SmartStack.app aligned)', () => { it('TenantId without a FK is CRITICAL — it is NOT exempt', () => { const findings = classifyColumns([col('TenantId')], [], tables) expect(findings[0].classification).toBe('critical') expect(findings[0].candidatePrincipal).toBe('core.tenant_Tenants') }) it('a business reference (ClientId) without a FK is CRITICAL', () => { const findings = classifyColumns([col('ClientId')], [], tables) expect(findings[0].classification).toBe('critical') expect(findings[0].candidatePrincipal).toBe('extensions.crm_Clients') }) it('identity/audit columns are EXEMPT (CreatedByUserId, UserId)', () => { const findings = classifyColumns([col('CreatedByUserId'), col('UserId'), col('ChangedByUserId')], [], tables) const map = byColumn(findings) expect(map.CreatedByUserId).toBe('exempt') expect(map.UserId).toBe('exempt') expect(map.ChangedByUserId).toBe('exempt') }) it('an unresolvable *Id (CorrelationId) is REVIEW, not critical', () => { const findings = classifyColumns([col('CorrelationId')], [], tables) expect(findings[0].classification).toBe('review') }) it('a column already covered by a FK is OK', () => { const fks: DbFk[] = [{ schema: 'extensions', table: 'crm_Orders', column: 'StageId' }] const findings = classifyColumns([col('StageId')], fks, tables) expect(findings[0].classification).toBe('ok') expect(findings[0].hasFk).toBe(true) }) it('FK match is case-insensitive on schema/table/column', () => { const fks: DbFk[] = [{ schema: 'EXTENSIONS', table: 'CRM_ORDERS', column: 'CLIENTID' }] const findings = classifyColumns([col('ClientId')], fks, tables) expect(findings[0].classification).toBe('ok') }) it('a custom allowlist entry downgrades a would-be CRITICAL/REVIEW to EXEMPT', () => { // WidgetId has no principal table → normally REVIEW; custom allowlist makes it EXEMPT. const base = classifyColumns([col('WidgetId')], [], tables) expect(base[0].classification).toBe('review') const custom = classifyColumns([col('WidgetId')], [], tables, [/^WidgetId$/i]) expect(custom[0].classification).toBe('exempt') }) }) describe('classify — sortFindings', () => { it('orders critical first, then review, then exempt', () => { const findings = classifyColumns([col('CreatedByUserId'), col('CorrelationId'), col('TenantId')], [], tables) const sorted = sortFindings(findings).map((f) => f.classification) expect(sorted).toEqual(['critical', 'review', 'exempt']) }) })