test: add unit tests for Dockerfile ARG insertion logic

This commit is contained in:
Andras Bacsai 2025-11-06 08:54:40 +01:00
parent dbf7957795
commit 4968e9fa2b

View File

@ -0,0 +1,218 @@
<?php
use App\Jobs\ApplicationDeploymentJob;
/**
* Test the Dockerfile ARG insertion logic
* This tests the fix for GitHub issue #7118
*/
it('finds FROM instructions in simple dockerfile', function () {
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$dockerfile = collect([
'FROM node:16',
'WORKDIR /app',
'COPY . .',
]);
$result = $job->findFromInstructionLines($dockerfile);
expect($result)->toBe([0]);
});
it('finds FROM instructions with comments before', function () {
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$dockerfile = collect([
'# Build stage',
'# Another comment',
'FROM node:16',
'WORKDIR /app',
]);
$result = $job->findFromInstructionLines($dockerfile);
expect($result)->toBe([2]);
});
it('finds multiple FROM instructions in multi-stage dockerfile', function () {
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$dockerfile = collect([
'FROM node:16 AS builder',
'WORKDIR /app',
'RUN npm install',
'',
'FROM nginx:alpine',
'COPY --from=builder /app/dist /usr/share/nginx/html',
]);
$result = $job->findFromInstructionLines($dockerfile);
expect($result)->toBe([0, 4]);
});
it('handles FROM with different cases', function () {
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$dockerfile = collect([
'from node:16',
'From nginx:alpine',
'FROM alpine:latest',
]);
$result = $job->findFromInstructionLines($dockerfile);
expect($result)->toBe([0, 1, 2]);
});
it('returns empty array when no FROM instructions found', function () {
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$dockerfile = collect([
'# Just comments',
'WORKDIR /app',
'RUN npm install',
]);
$result = $job->findFromInstructionLines($dockerfile);
expect($result)->toBe([]);
});
it('inserts ARGs after FROM in simple dockerfile', function () {
$dockerfile = collect([
'FROM node:16',
'WORKDIR /app',
'COPY . .',
]);
$fromLines = [0];
$argsToInsert = collect(['ARG MY_VAR=value', 'ARG ANOTHER_VAR']);
foreach (array_reverse($fromLines) as $fromLineIndex) {
foreach ($argsToInsert->reverse() as $arg) {
$dockerfile->splice($fromLineIndex + 1, 0, [$arg]);
}
}
expect($dockerfile[0])->toBe('FROM node:16');
expect($dockerfile[1])->toBe('ARG MY_VAR=value');
expect($dockerfile[2])->toBe('ARG ANOTHER_VAR');
expect($dockerfile[3])->toBe('WORKDIR /app');
});
it('inserts ARGs after each FROM in multi-stage dockerfile', function () {
$dockerfile = collect([
'FROM node:16 AS builder',
'WORKDIR /app',
'',
'FROM nginx:alpine',
'COPY --from=builder /app/dist /usr/share/nginx/html',
]);
$fromLines = [0, 3];
$argsToInsert = collect(['ARG MY_VAR=value']);
foreach (array_reverse($fromLines) as $fromLineIndex) {
foreach ($argsToInsert->reverse() as $arg) {
$dockerfile->splice($fromLineIndex + 1, 0, [$arg]);
}
}
// First stage
expect($dockerfile[0])->toBe('FROM node:16 AS builder');
expect($dockerfile[1])->toBe('ARG MY_VAR=value');
expect($dockerfile[2])->toBe('WORKDIR /app');
// Second stage (index shifted by +1 due to inserted ARG)
expect($dockerfile[4])->toBe('FROM nginx:alpine');
expect($dockerfile[5])->toBe('ARG MY_VAR=value');
});
it('inserts ARGs after FROM when comments precede FROM', function () {
$dockerfile = collect([
'# Build stage comment',
'FROM node:16',
'WORKDIR /app',
]);
$fromLines = [1];
$argsToInsert = collect(['ARG MY_VAR=value']);
foreach (array_reverse($fromLines) as $fromLineIndex) {
foreach ($argsToInsert->reverse() as $arg) {
$dockerfile->splice($fromLineIndex + 1, 0, [$arg]);
}
}
expect($dockerfile[0])->toBe('# Build stage comment');
expect($dockerfile[1])->toBe('FROM node:16');
expect($dockerfile[2])->toBe('ARG MY_VAR=value');
expect($dockerfile[3])->toBe('WORKDIR /app');
});
it('handles real-world nuxt multi-stage dockerfile with comments', function () {
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$dockerfile = collect([
'# Build Stage 1',
'',
'FROM node:22-alpine AS build',
'WORKDIR /app',
'',
'RUN corepack enable',
'',
'# Copy package.json and your lockfile, here we add pnpm-lock.yaml for illustration',
'COPY package.json pnpm-lock.yaml .npmrc ./',
'',
'# Install dependencies',
'RUN pnpm i',
'',
'# Copy the entire project',
'COPY . ./',
'',
'# Build the project',
'RUN pnpm run build',
'',
'# Build Stage 2',
'',
'FROM node:22-alpine',
'WORKDIR /app',
'',
'# Only `.output` folder is needed from the build stage',
'COPY --from=build /app/.output/ ./',
'',
'# Change the port and host',
'ENV PORT=80',
'ENV HOST=0.0.0.0',
'',
'EXPOSE 80',
'',
'CMD ["node", "/app/server/index.mjs"]',
]);
// Find FROM instructions
$fromLines = $job->findFromInstructionLines($dockerfile);
expect($fromLines)->toBe([2, 21]);
// Simulate ARG insertion
$argsToInsert = collect(['ARG BUILD_VAR=production']);
foreach (array_reverse($fromLines) as $fromLineIndex) {
foreach ($argsToInsert->reverse() as $arg) {
$dockerfile->splice($fromLineIndex + 1, 0, [$arg]);
}
}
// Verify first stage
expect($dockerfile[2])->toBe('FROM node:22-alpine AS build');
expect($dockerfile[3])->toBe('ARG BUILD_VAR=production');
expect($dockerfile[4])->toBe('WORKDIR /app');
// Verify second stage (index shifted by +1 due to first ARG insertion)
expect($dockerfile[22])->toBe('FROM node:22-alpine');
expect($dockerfile[23])->toBe('ARG BUILD_VAR=production');
expect($dockerfile[24])->toBe('WORKDIR /app');
});