import { describe, it, expect, beforeAll } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; const ROOT_DIR = path.resolve(__dirname, '..'); describe('Project API Endpoints', () => { describe('Project API Controller', () => { it('should have project controller registered', () => { const apiV1Path = path.join(ROOT_DIR, 'src', 'controllers', 'api', 'v1', 'project.ts'); expect(fs.existsSync(apiV1Path)).toBe(true); }); it('should have GET projects route', () => { const controllerPath = path.join(ROOT_DIR, 'src', 'controllers', 'api', 'v1', 'project.ts'); const content = fs.readFileSync(controllerPath, 'utf-8'); expect(content).toContain('getProjects'); }); it('should have POST projects route', () => { const controllerPath = path.join(ROOT_DIR, 'src', 'controllers', 'api', 'v1', 'project.ts'); const content = fs.readFileSync(controllerPath, 'utf-8'); expect(content).toContain('createProject'); }); it('should have POST /pull route', () => { const controllerPath = path.join(ROOT_DIR, 'src', 'controllers', 'api', 'v1', 'project.ts'); const content = fs.readFileSync(controllerPath, 'utf-8'); expect(content).toContain('pullProject'); expect(content).toContain('/pull'); }); it('should register /pull route before /:projectId', () => { const controllerPath = path.join(ROOT_DIR, 'src', 'controllers', 'api', 'v1', 'project.ts'); const content = fs.readFileSync(controllerPath, 'utf-8'); const pullIndex = content.indexOf('"/pull"'); const paramIndex = content.indexOf('"/:projectId"'); expect(pullIndex).toBeGreaterThan(0); expect(paramIndex).toBeGreaterThan(0); expect(pullIndex).toBeLessThan(paramIndex); }); it('should use requireUser middleware', () => { const controllerPath = path.join(ROOT_DIR, 'src', 'controllers', 'api', 'v1', 'project.ts'); const content = fs.readFileSync(controllerPath, 'utf-8'); expect(content).toContain('requireUser()'); }); it('should not expose password fields in responses', () => { const controllerPath = path.join(ROOT_DIR, 'src', 'controllers', 'api', 'v1', 'project.ts'); const content = fs.readFileSync(controllerPath, 'utf-8'); expect(content).not.toMatch(/passwordSalt/); }); }); describe('Project Service', () => { it('should have findById method', () => { const servicePath = path.join(ROOT_DIR, 'src', 'services', 'project.ts'); const content = fs.readFileSync(servicePath, 'utf-8'); expect(content).toContain('findById'); }); it('should have delete method', () => { const servicePath = path.join(ROOT_DIR, 'src', 'services', 'project.ts'); const content = fs.readFileSync(servicePath, 'utf-8'); expect(content).toContain('async delete'); }); it('should have getForUser method', () => { const servicePath = path.join(ROOT_DIR, 'src', 'services', 'project.ts'); const content = fs.readFileSync(servicePath, 'utf-8'); expect(content).toContain('getForUser'); }); it('should have createFromGitUrl method', () => { const servicePath = path.join(ROOT_DIR, 'src', 'services', 'project.ts'); const content = fs.readFileSync(servicePath, 'utf-8'); expect(content).toContain('createFromGitUrl'); }); it('should have parseGitUrl method', () => { const servicePath = path.join(ROOT_DIR, 'src', 'services', 'project.ts'); const content = fs.readFileSync(servicePath, 'utf-8'); expect(content).toContain('parseGitUrl'); }); it('should populate user with password excluded', () => { const servicePath = path.join(ROOT_DIR, 'src', 'services', 'project.ts'); const content = fs.readFileSync(servicePath, 'utf-8'); expect(content).toContain('-passwordSalt'); }); }); describe('Git URL Parsing', () => { // We import the service to test parseGitUrl directly let projectService: any; beforeAll(async () => { const mod = await import(path.join(ROOT_DIR, 'src', 'services', 'project.ts')); projectService = mod.default; }); it('should parse HTTPS URL with .git suffix', () => { const result = projectService.parseGitUrl('https://github.com/user/my-repo.git'); expect(result.name).toBe('my-repo'); expect(result.slug).toBe('my-repo'); }); it('should parse HTTPS URL without .git suffix', () => { const result = projectService.parseGitUrl('https://github.com/user/my-repo'); expect(result.name).toBe('my-repo'); expect(result.slug).toBe('my-repo'); }); it('should parse SSH format URL', () => { const result = projectService.parseGitUrl('git@github.com:user/my-repo.git'); expect(result.name).toBe('my-repo'); expect(result.slug).toBe('my-repo'); }); it('should parse SSH URL with ssh:// prefix', () => { const result = projectService.parseGitUrl('ssh://git@github.com/user/my-repo.git'); expect(result.name).toBe('my-repo'); expect(result.slug).toBe('my-repo'); }); it('should handle repo names with special characters', () => { const result = projectService.parseGitUrl('https://github.com/user/my_cool_repo.git'); expect(result.name).toBe('my_cool_repo'); // slug() removes underscores entirely expect(result.slug).toBe('mycoolrepo'); }); it('should handle repo names with mixed case', () => { const result = projectService.parseGitUrl('https://github.com/user/MyRepo.git'); expect(result.name).toBe('MyRepo'); // slug() lowercases expect(result.slug).toBe('myrepo'); }); it('should trim whitespace from URL', () => { const result = projectService.parseGitUrl(' https://github.com/user/my-repo.git '); expect(result.name).toBe('my-repo'); expect(result.slug).toBe('my-repo'); }); }); describe('Frontend API Client', () => { it('should add Authorization header with token', () => { const apiPath = path.join(ROOT_DIR, 'frontend', 'src', 'lib', 'api.ts'); const content = fs.readFileSync(apiPath, 'utf-8'); expect(content).toContain('Authorization'); expect(content).toContain('Bearer'); }); it('should store token in localStorage', () => { const apiPath = path.join(ROOT_DIR, 'frontend', 'src', 'lib', 'api.ts'); const content = fs.readFileSync(apiPath, 'utf-8'); expect(content).toContain('dtp_auth_token'); }); it('should have getAll method', () => { const apiPath = path.join(ROOT_DIR, 'frontend', 'src', 'lib', 'api.ts'); const content = fs.readFileSync(apiPath, 'utf-8'); expect(content).toContain('getAll'); }); it('should have pull method for project API', () => { const apiPath = path.join(ROOT_DIR, 'frontend', 'src', 'lib', 'api.ts'); const content = fs.readFileSync(apiPath, 'utf-8'); expect(content).toContain('pull:'); expect(content).toContain('/projects/pull'); }); }); describe('Pull Project UI', () => { it('should have PullProjectForm component', () => { const uiPath = path.join(ROOT_DIR, 'frontend', 'src', 'pages', 'ProjectManager.tsx'); const content = fs.readFileSync(uiPath, 'utf-8'); expect(content).toContain('PullProjectForm'); }); it('should have Pull Project button in sidebar', () => { const uiPath = path.join(ROOT_DIR, 'frontend', 'src', 'pages', 'ProjectManager.tsx'); const content = fs.readFileSync(uiPath, 'utf-8'); expect(content).toContain('Pull Project'); }); it('should have showPullForm state', () => { const uiPath = path.join(ROOT_DIR, 'frontend', 'src', 'pages', 'ProjectManager.tsx'); const content = fs.readFileSync(uiPath, 'utf-8'); expect(content).toContain('showPullForm'); }); it('should have handleProjectPulled handler', () => { const uiPath = path.join(ROOT_DIR, 'frontend', 'src', 'pages', 'ProjectManager.tsx'); const content = fs.readFileSync(uiPath, 'utf-8'); expect(content).toContain('handleProjectPulled'); }); }); describe('User Interface', () => { it('should not include password fields in User type', () => { const apiPath = path.join(ROOT_DIR, 'frontend', 'src', 'lib', 'api.ts'); const content = fs.readFileSync(apiPath, 'utf-8'); const userInterfaceMatch = content.match(/export interface User/); expect(userInterfaceMatch).toBeTruthy(); }); }); });