8d1855b89c
Reject zip uploads when the selected runtime does not match archive contents, and re-validate before Kaniko builds to fail fast instead of producing the wrong Dockerfile. Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
5.2 KiB
TypeScript
142 lines
5.2 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|