gadget/gadget-code/tests/project-api.test.ts
2026-05-15 19:34:08 -04:00

210 lines
8.4 KiB
TypeScript

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();
});
});
});