mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* 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
135 lines
5.2 KiB
TypeScript
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'");
|
|
});
|
|
});
|