GitNexus/gitnexus/test/unit/repo-manager.test.ts
Gergő Magyar acf6fbdd39
feat: configure eslint with unused import removal (#564)
* feat: configure eslint with unused import removal

Add ESLint v9 (flat config) for code quality:
- eslint-plugin-unused-imports for auto-removing dead imports
- @typescript-eslint for TypeScript-aware linting
- eslint-plugin-react-hooks for React hooks rules
- eslint-config-prettier to avoid formatting conflicts
- lint-staged runs eslint --fix before prettier on .ts/.tsx
- CI lint job added to ci-quality.yml

* refactor: remove unused imports via eslint --fix

Auto-fixed by eslint-plugin-unused-imports. No logic changes.

* chore: add eslint fix commit to .git-blame-ignore-revs
2026-03-28 15:28:09 +00:00

135 lines
5.2 KiB
TypeScript

/**
* P1 Unit Tests: Repository Manager
*
* Tests: getStoragePath, getStoragePaths, readRegistry, registerRepo, unregisterRepo
* Covers hardening fixes #29 (API key file permissions) and #30 (case-insensitive paths on Windows)
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import path from 'path';
import os from 'os';
import fs from 'fs/promises';
import {
getStoragePath,
getStoragePaths,
readRegistry,
loadCLIConfig,
} from '../../src/storage/repo-manager.js';
import { createTempDir } from '../helpers/test-db.js';
// ─── getStoragePath ──────────────────────────────────────────────────
describe('getStoragePath', () => {
it('appends .gitnexus to resolved repo path', () => {
const result = getStoragePath('/home/user/project');
expect(result).toContain('.gitnexus');
expect(path.basename(result)).toBe('.gitnexus');
});
it('resolves relative paths', () => {
const result = getStoragePath('.');
// Should be an absolute path
expect(path.isAbsolute(result)).toBe(true);
});
});
// ─── getStoragePaths ─────────────────────────────────────────────────
describe('getStoragePaths', () => {
it('returns storagePath, lbugPath, metaPath', () => {
const paths = getStoragePaths('/home/user/project');
expect(paths.storagePath).toContain('.gitnexus');
expect(paths.lbugPath).toContain('lbug');
expect(paths.metaPath).toContain('meta.json');
});
it('all paths are under storagePath', () => {
const paths = getStoragePaths('/home/user/project');
expect(paths.lbugPath.startsWith(paths.storagePath)).toBe(true);
expect(paths.metaPath.startsWith(paths.storagePath)).toBe(true);
});
});
// ─── readRegistry ────────────────────────────────────────────────────
describe('readRegistry', () => {
it('returns empty array when registry does not exist', async () => {
// readRegistry reads from ~/.gitnexus/registry.json
// If the file doesn't exist, it should return []
// This test exercises the catch path
const result = await readRegistry();
// Result is an array (may or may not be empty depending on user's system)
expect(Array.isArray(result)).toBe(true);
});
});
// ─── CLI Config (file permissions) ───────────────────────────────────
describe('saveCLIConfig / loadCLIConfig', () => {
let tmpHandle: Awaited<ReturnType<typeof createTempDir>>;
let originalHomedir: typeof os.homedir;
beforeEach(async () => {
tmpHandle = await createTempDir('gitnexus-config-test-');
originalHomedir = os.homedir;
// Mock os.homedir to point to our temp dir
// Note: This won't fully work because repo-manager uses its own import of os
// We'll test what we can.
});
afterEach(async () => {
os.homedir = originalHomedir;
await tmpHandle.cleanup();
});
it('loadCLIConfig returns empty object when config does not exist', async () => {
const config = await loadCLIConfig();
// Returns {} or existing config
expect(typeof config).toBe('object');
});
});
// ─── Case-insensitive path comparison (Windows hardening #30) ────────
describe('case-insensitive path comparison', () => {
it('registerRepo uses case-insensitive compare on Windows', () => {
// The fix is in registerRepo: process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase()
// We verify the logic inline since we can't easily mock process.platform
const compareWindows = (a: string, b: string): boolean => {
return a.toLowerCase() === b.toLowerCase();
};
// On Windows, these should match
expect(compareWindows('D:\\Projects\\MyApp', 'd:\\projects\\myapp')).toBe(true);
expect(compareWindows('C:\\Users\\USER\\project', 'c:\\users\\user\\project')).toBe(true);
// Different paths should not match
expect(compareWindows('D:\\Projects\\App1', 'D:\\Projects\\App2')).toBe(false);
});
it('case-sensitive compare for non-Windows', () => {
const compareUnix = (a: string, b: string): boolean => {
return a === b;
};
// On Unix, case matters
expect(compareUnix('/home/user/Project', '/home/user/project')).toBe(false);
expect(compareUnix('/home/user/project', '/home/user/project')).toBe(true);
});
});
// ─── API key file permissions (hardening #29) ────────────────────────
describe('API key file permissions', () => {
it('saveCLIConfig calls chmod 0o600 on non-Windows', async () => {
// We verify that the saveCLIConfig code has the chmod call
// by reading the source and checking statically.
// The actual chmod behavior is platform-dependent.
const source = await fs.readFile(
path.join(process.cwd(), 'src', 'storage', 'repo-manager.ts'),
'utf-8',
);
expect(source).toContain('chmod(configPath, 0o600)');
expect(source).toContain("process.platform !== 'win32'");
});
});