import 'reflect-metadata'; import { type CanActivate, type ExecutionContext, type INestApplication, ValidationPipe, } from '@nestjs/common'; import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; import { Test } from '@nestjs/testing'; import request from 'supertest'; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { AuthGuard } from '../auth/auth.guard.js'; import { ProjectBootstrapService } from './project-bootstrap.service.js'; import { WorkspaceController } from './workspace.controller.js'; const bootstrapMock = vi.fn(() => Promise.resolve({ projectId: 'project-1', workspacePath: '/opt/mosaic/.workspaces/users/user-1/project-1', }), ); const authGuard: CanActivate = { canActivate(context: ExecutionContext): boolean { const requestContext = context.switchToHttp().getRequest<{ user?: { id: string } }>(); requestContext.user = { id: 'user-1' }; return true; }, }; describe('POST /api/workspaces repoUrl validation', () => { let app: INestApplication; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ controllers: [WorkspaceController], providers: [ { provide: ProjectBootstrapService, useValue: { bootstrap: bootstrapMock }, }, ], }) .overrideGuard(AuthGuard) .useValue(authGuard) .compile(); app = moduleRef.createNestApplication(new FastifyAdapter()); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); await app.init(); await app.getHttpAdapter().getInstance().ready(); }); beforeEach(() => { bootstrapMock.mockClear(); }); afterAll(async () => { await app.close(); }); it.each([ ['a leading-dash value', '--upload-pack=sh -c id'], ['an ext remote helper', 'ext::sh -c id'], ['a file URL', 'file:///tmp/repository'], ['an unparseable value', 'not a url'], ['an SSH shorthand', 'git@example.com:acme/repository.git'], ['a scheme without //', 'https:example.com/acme/repository.git'], ['a hostless git URL', 'git:///tmp/repository'], ])('returns 400 for %s', async (_description, repoUrl) => { const response = await request(app.getHttpServer()) .post('/api/workspaces') .send({ name: 'Example', repoUrl }) .set('Content-Type', 'application/json'); expect(response.status).toBe(400); expect(bootstrapMock).not.toHaveBeenCalled(); }); it.each([ ['a plain HTTPS repository URL', 'https://example.com/acme/repository.git'], ['a git protocol repository URL', 'git://example.com/acme/repository.git'], ])('accepts %s', async (_description, repoUrl) => { const response = await request(app.getHttpServer()) .post('/api/workspaces') .send({ name: 'Example', repoUrl }) .set('Content-Type', 'application/json'); expect(response.status).toBe(201); expect(bootstrapMock).toHaveBeenCalledWith({ name: 'Example', description: undefined, userId: 'user-1', teamId: undefined, repoUrl, }); }); });