import * as path from 'path'; import { BadRequestException } from '@nestjs/common'; import { AppRuntime } from '../common/enums'; import { assertRuntimeMatch, detectDjangoSettingsModule, detectGoBuildTarget, detectRuntimeFromArchive, detectRuntimeFromEntries, listArchiveEntries, normalizeEntryPath, stripCommonRootPrefix, } from './runtime-detector'; const fixturesDir = path.join(__dirname, 'fixtures'); describe('runtime-detector', () => { describe('normalizeEntryPath / stripCommonRootPrefix', () => { it('strips a single root folder prefix', () => { const entries = ['myapp/package.json', 'myapp/src/index.js']; expect(stripCommonRootPrefix(entries)).toEqual(['package.json', 'src/index.js']); }); it('normalizes leading ./ segments', () => { expect(normalizeEntryPath('./package.json')).toBe('package.json'); }); }); describe('detectRuntimeFromEntries', () => { it('detects nodejs in nested layout', () => { const result = detectRuntimeFromEntries(['myapp/package.json']); expect(result).toMatchObject({ runtime: AppRuntime.NODEJS, confidence: 'high' }); }); it('detects go from go.mod', () => { const result = detectRuntimeFromEntries(['go.mod', 'main.go']); expect(result).toMatchObject({ runtime: AppRuntime.GO, confidence: 'high' }); }); it('detects laravel from artisan + composer.json', () => { const result = detectRuntimeFromEntries(['artisan', 'composer.json']); expect(result).toMatchObject({ runtime: AppRuntime.LARAVEL, confidence: 'high' }); }); it('detects php from composer.json without artisan', () => { const result = detectRuntimeFromEntries(['composer.json', 'index.php']); expect(result).toMatchObject({ runtime: AppRuntime.PHP, confidence: 'high' }); }); it('detects django from manage.py', () => { const result = detectRuntimeFromEntries(['manage.py', 'requirements.txt']); expect(result).toMatchObject({ runtime: AppRuntime.DJANGO, confidence: 'high' }); }); it('detects python from requirements.txt', () => { const result = detectRuntimeFromEntries(['requirements.txt', 'app.py']); expect(result).toMatchObject({ runtime: AppRuntime.PYTHON, confidence: 'high' }); }); it('detects dotnet from shallow csproj', () => { const result = detectRuntimeFromEntries(['src/App/App.csproj']); expect(result).toMatchObject({ runtime: AppRuntime.DOTNET, confidence: 'high' }); }); it('detects wordpress wp-content migrate layout', () => { const result = detectRuntimeFromEntries(['themes/twenty/style.css', 'plugins/hello/hello.php']); expect(result).toMatchObject({ runtime: AppRuntime.WORDPRESS, confidence: 'high' }); }); it('returns low confidence when package.json and composer.json coexist', () => { const result = detectRuntimeFromEntries(['package.json', 'composer.json']); expect(result).toMatchObject({ runtime: null, confidence: 'low' }); }); }); describe('assertRuntimeMatch', () => { it('passes when configured runtime matches detection', () => { expect(() => assertRuntimeMatch(AppRuntime.GO, { runtime: AppRuntime.GO, confidence: 'high', signals: ['go.mod'], }), ).not.toThrow(); }); it('throws BadRequestException on high-confidence mismatch', () => { expect(() => assertRuntimeMatch(AppRuntime.NODEJS, { runtime: AppRuntime.GO, confidence: 'high', signals: ['go.mod'], }), ).toThrow(BadRequestException); }); it('allows upload when confidence is low', () => { expect(() => assertRuntimeMatch(AppRuntime.NODEJS, { runtime: null, confidence: 'low', signals: [], }), ).not.toThrow(); }); }); describe('listArchiveEntries + detectRuntimeFromArchive', () => { it.each([ ['nodejs-nested.zip', AppRuntime.NODEJS], ['go-mod.zip', AppRuntime.GO], ['laravel.zip', AppRuntime.LARAVEL], ['php-composer.zip', AppRuntime.PHP], ['django.zip', AppRuntime.DJANGO], ['wordpress-wp-content.zip', AppRuntime.WORDPRESS], ] as const)('reads %s as %s', async (fixture, runtime) => { const zipPath = path.join(fixturesDir, fixture); const entries = await listArchiveEntries(zipPath); expect(entries.length).toBeGreaterThan(0); const detected = await detectRuntimeFromArchive(zipPath); expect(detected.runtime).toBe(runtime); expect(detected.confidence).toBe('high'); }); it('detects go inside a single nested root folder', async () => { const zipPath = path.join(fixturesDir, 'go-nested-root.zip'); const detected = await detectRuntimeFromArchive(zipPath); expect(detected.runtime).toBe(AppRuntime.GO); }); }); describe('dockerfile helpers', () => { it('prefers cmd/*/main.go for go build target', () => { expect(detectGoBuildTarget(['go.mod', 'cmd/server/main.go'])).toBe('./cmd/server'); }); it('infers django settings module from project layout', () => { expect(detectDjangoSettingsModule(['myproject/settings.py'])).toBe('myproject.settings'); }); }); });