import { describe, expect, it } from 'vitest'; import { NODE_TABLES, REL_TYPES } from 'gitnexus-shared'; // --------------------------------------------------------------------------- // Recreate the security guards locally so we can test the exact logic used in // production without exporting private helpers. // // Source locations: // validLabel / validRelType -- gitnexus-web/src/core/llm/tools.ts // isSafeId -- gitnexus-web/src/components/ProcessesPanel.tsx // readOnly guard (regex) -- gitnexus-web/src/core/lbug/lbug-adapter.ts // --------------------------------------------------------------------------- const validLabel = (label: string): boolean => (NODE_TABLES as readonly string[]).includes(label); const validRelType = (t: string): boolean => (REL_TYPES as readonly string[]).includes(t); const isSafeId = (id: string): boolean => /^[a-zA-Z0-9_:.\-/@]+$/.test(id); const isWriteQuery = (cypher: string): boolean => { const stripped = cypher.replace(/'[^']*'|"[^"]*"/g, '').toUpperCase(); return /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|DETACH)\b/.test(stripped); }; // =========================================================================== // validLabel // =========================================================================== describe('validLabel – NODE_TABLES membership', () => { it.each(['Function', 'Class', 'File', 'Process', 'Community'])( 'accepts known label "%s"', (label) => { expect(validLabel(label)).toBe(true); }, ); it.each([ 'Struct', 'Enum', 'Trait', 'Impl', 'Macro', 'Typedef', 'Union', 'Namespace', 'TypeAlias', 'Const', 'Static', 'Property', 'Record', 'Delegate', 'Annotation', 'Constructor', 'Template', 'Module', ])('accepts multi-language label "%s"', (label) => { expect(validLabel(label)).toBe(true); }); it.each([ ['empty string', ''], ['SQL keyword', 'DROP'], ['random word', 'foo'], ['Cypher injection', '})-[:R]->(x)'], ['label with semicolon', 'Function;DELETE'], ['lowercase (case matters)', 'function'], ['lowercase class', 'class'], ['whitespace padded', ' File '], ['numeric', '123'], ])('rejects invalid label: %s', (_desc, label) => { expect(validLabel(label)).toBe(false); }); it('NODE_TABLES contains all expected core labels', () => { const core = [ 'File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement', 'Community', 'Process', ]; for (const label of core) { expect((NODE_TABLES as readonly string[]).includes(label)).toBe(true); } }); }); // =========================================================================== // validRelType // =========================================================================== describe('validRelType – REL_TYPES membership', () => { it.each([...REL_TYPES])('accepts known relation type "%s"', (relType) => { expect(validRelType(relType)).toBe(true); }); it.each([ ['empty string', ''], ['SQL keyword', 'DROP'], ['injection attempt', 'CALLS;DELETE'], ['lowercase', 'calls'], ['nonexistent type', 'FRIEND_OF'], ['padded', ' CALLS '], ])('rejects invalid relation type: %s', (_desc, relType) => { expect(validRelType(relType)).toBe(false); }); it('REL_TYPES has at least the base types', () => { // Guard against accidental removal of relation types expect(REL_TYPES.length).toBeGreaterThanOrEqual(8); }); }); // =========================================================================== // isSafeId // =========================================================================== describe('isSafeId – identifier allowlist regex', () => { it.each([ ['namespaced id', 'Function:myFunc'], ['underscore id', 'proc_5'], ['class id', 'Class:MyClass'], ['dotted name', 'Module:path.to.thing'], ['with hyphen', 'File:my-file.ts'], ['community id', 'comm_5'], ['file path id', 'File:src/index.ts'], ['nested path id', 'Function:src/utils/helpers.ts:doStuff'], ['scoped npm package', 'Module:@scope/pkg'], ['angular-style id', 'Module:@angular/core'], ])('accepts valid ID: %s', (_desc, id) => { expect(isSafeId(id)).toBe(true); }); it.each([['with spaces', 'Process:my process']])( 'rejects ID with unsafe chars: %s', (_desc, id) => { expect(isSafeId(id)).toBe(false); }, ); it('rejects empty string', () => { expect(isSafeId('')).toBe(false); }); it.each([ ['SQL injection', "'; DROP TABLE"], ['command substitution', '$(command)'], ['XSS attempt', '