mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-06 08:16:02 +00:00
1. Path traversal via group name — add validateGroupName() with regex [a-zA-Z0-9][a-zA-Z0-9_-]*, called in getGroupDir (defense in depth) 2. gRPC proto regex can't handle nested braces — replace serviceRe with extractServiceBlocks() brace-depth counter (init depth=1, skip malformed protos) 3. Service boundary detector directory exclusions — add EXCLUDED_DIRS set (vendor, target, build, dist, __pycache__, .venv, venv, .tox, .mypy_cache, .gradle, .mvn, out, bin) replacing inline node_modules 4. Double-close of LadybugDB pools — remove blanket closeLbug() from cli/group.ts; sync.ts per-id cleanup is sufficient Tests: 22 new tests across 5 files. Full suite: 4706 passed, 0 failed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
138 lines
4.6 KiB
TypeScript
138 lines
4.6 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as path from 'node:path';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import {
|
|
getGroupDir,
|
|
getGroupsBaseDir,
|
|
writeContractRegistry,
|
|
readContractRegistry,
|
|
listGroups,
|
|
createGroupDir,
|
|
validateGroupName,
|
|
} from '../../../src/core/group/storage.js';
|
|
import type { ContractRegistry } from '../../../src/core/group/types.js';
|
|
|
|
describe('Group storage', () => {
|
|
const tmpDir = path.join(os.tmpdir(), `gitnexus-test-storage-${Date.now()}`);
|
|
|
|
beforeEach(() => {
|
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('getGroupsBaseDir returns ~/.gitnexus/groups/', () => {
|
|
const base = getGroupsBaseDir(tmpDir);
|
|
expect(base).toBe(path.join(tmpDir, 'groups'));
|
|
});
|
|
|
|
it('getGroupDir returns correct path for group name', () => {
|
|
const dir = getGroupDir(tmpDir, 'company');
|
|
expect(dir).toBe(path.join(tmpDir, 'groups', 'company'));
|
|
});
|
|
|
|
it('writeContractRegistry writes atomically and readContractRegistry reads back', async () => {
|
|
const groupDir = path.join(tmpDir, 'groups', 'test-group');
|
|
fs.mkdirSync(groupDir, { recursive: true });
|
|
|
|
const registry: ContractRegistry = {
|
|
version: 1,
|
|
generatedAt: '2026-03-31T10:00:00Z',
|
|
repoSnapshots: {},
|
|
missingRepos: [],
|
|
contracts: [],
|
|
crossLinks: [],
|
|
};
|
|
|
|
await writeContractRegistry(groupDir, registry);
|
|
|
|
const filePath = path.join(groupDir, 'contracts.json');
|
|
expect(fs.existsSync(filePath)).toBe(true);
|
|
|
|
const loaded = await readContractRegistry(groupDir);
|
|
expect(loaded?.version).toBe(1);
|
|
expect(loaded?.generatedAt).toBe('2026-03-31T10:00:00Z');
|
|
});
|
|
|
|
it('readContractRegistry returns null when file does not exist', async () => {
|
|
const groupDir = path.join(tmpDir, 'groups', 'nonexistent');
|
|
fs.mkdirSync(groupDir, { recursive: true });
|
|
const result = await readContractRegistry(groupDir);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('listGroups returns group names', async () => {
|
|
const groupsDir = path.join(tmpDir, 'groups');
|
|
fs.mkdirSync(path.join(groupsDir, 'company'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(groupsDir, 'company', 'group.yaml'),
|
|
'version: 1\nname: company\nrepos:\n a: b',
|
|
);
|
|
fs.mkdirSync(path.join(groupsDir, 'personal'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(groupsDir, 'personal', 'group.yaml'),
|
|
'version: 1\nname: personal\nrepos:\n c: d',
|
|
);
|
|
|
|
const groups = await listGroups(tmpDir);
|
|
expect(groups.sort()).toEqual(['company', 'personal']);
|
|
});
|
|
|
|
describe('validateGroupName', () => {
|
|
it('test_validateGroupName_traversal_path_throws', () => {
|
|
expect(() => validateGroupName('../../evil')).toThrow(/Invalid group name/);
|
|
});
|
|
|
|
it('test_validateGroupName_slash_in_name_throws', () => {
|
|
expect(() => validateGroupName('foo/bar')).toThrow(/Invalid group name/);
|
|
});
|
|
|
|
it('test_validateGroupName_empty_string_throws', () => {
|
|
expect(() => validateGroupName('')).toThrow(/Invalid group name/);
|
|
});
|
|
|
|
it('test_validateGroupName_starts_with_dash_throws', () => {
|
|
expect(() => validateGroupName('-leading-dash')).toThrow(/Invalid group name/);
|
|
});
|
|
|
|
it('test_validateGroupName_starts_with_underscore_throws', () => {
|
|
expect(() => validateGroupName('_leading')).toThrow(/Invalid group name/);
|
|
});
|
|
|
|
it('test_validateGroupName_dots_throws', () => {
|
|
expect(() => validateGroupName('com.example')).toThrow(/Invalid group name/);
|
|
});
|
|
|
|
it('test_validateGroupName_valid_alphanumeric_passes', () => {
|
|
expect(() => validateGroupName('my-group_01')).not.toThrow();
|
|
});
|
|
|
|
it('test_validateGroupName_single_char_passes', () => {
|
|
expect(() => validateGroupName('A')).not.toThrow();
|
|
});
|
|
|
|
it('test_validateGroupName_all_digits_passes', () => {
|
|
expect(() => validateGroupName('123')).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('getGroupDir rejects invalid names', () => {
|
|
it('test_getGroupDir_traversal_throws', () => {
|
|
expect(() => getGroupDir(tmpDir, '../../etc')).toThrow(/Invalid group name/);
|
|
});
|
|
|
|
it('test_getGroupDir_valid_name_returns_path', () => {
|
|
const dir = getGroupDir(tmpDir, 'company');
|
|
expect(dir).toBe(path.join(tmpDir, 'groups', 'company'));
|
|
});
|
|
});
|
|
|
|
describe('createGroupDir rejects invalid names', () => {
|
|
it('test_createGroupDir_traversal_throws', async () => {
|
|
await expect(createGroupDir(tmpDir, '../evil')).rejects.toThrow(/Invalid group name/);
|
|
});
|
|
});
|
|
});
|