build logs
This commit is contained in:
128
tests/Feature/DockerImageBuilderTest.php
Normal file
128
tests/Feature/DockerImageBuilderTest.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\RunDockerImageBuild;
|
||||
use App\Models\DockerImageBuild;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('guests are redirected to the login page', function () {
|
||||
$this->get(route('docker-builder.index'))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('authenticated users can visit the docker image builder page', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('docker-builder.index'))
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('docker/ImageBuilder')
|
||||
->where('build', null),
|
||||
);
|
||||
});
|
||||
|
||||
test('authenticated users can queue a docker image build from editor input', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->post(route('docker-builder.store'), [
|
||||
'dockerfile' => implode("\n", [
|
||||
'FROM alpine:3.20',
|
||||
'RUN echo "building"',
|
||||
]),
|
||||
'image_name' => 'ghcr.io/acme/example',
|
||||
'image_tag' => 'latest',
|
||||
'environment' => [
|
||||
['key' => 'APP_ENV', 'value' => 'production'],
|
||||
['key' => 'APP_NAME', 'value' => 'Docker Builder'],
|
||||
],
|
||||
])
|
||||
->assertSessionHasNoErrors();
|
||||
|
||||
$dockerImageBuild = DockerImageBuild::query()->sole();
|
||||
|
||||
$response->assertRedirect(route('docker-builder.index', [
|
||||
'build' => $dockerImageBuild->id,
|
||||
]));
|
||||
|
||||
expect($dockerImageBuild->user_id)->toBe($user->id)
|
||||
->and($dockerImageBuild->image_name)->toBe('ghcr.io/acme/example')
|
||||
->and($dockerImageBuild->image_tag)->toBe('latest')
|
||||
->and($dockerImageBuild->dockerfile_content)->toContain('RUN echo "building"')
|
||||
->and($dockerImageBuild->environment)->toBe([
|
||||
['key' => 'APP_ENV', 'value' => 'production'],
|
||||
['key' => 'APP_NAME', 'value' => 'Docker Builder'],
|
||||
])
|
||||
->and($dockerImageBuild->status)->toBe(DockerImageBuild::STATUS_QUEUED)
|
||||
->and($dockerImageBuild->successful)->toBeNull()
|
||||
->and($dockerImageBuild->queued_at)->not->toBeNull();
|
||||
|
||||
Queue::assertPushed(RunDockerImageBuild::class, function (RunDockerImageBuild $job) use ($dockerImageBuild): bool {
|
||||
return $job->dockerImageBuildId === $dockerImageBuild->id;
|
||||
});
|
||||
});
|
||||
|
||||
test('authenticated users can view their docker build history', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$olderBuild = DockerImageBuild::factory()->for($user)->create([
|
||||
'status' => DockerImageBuild::STATUS_FAILED,
|
||||
'build_output' => 'failed to solve',
|
||||
]);
|
||||
|
||||
$newerBuild = DockerImageBuild::factory()->for($user)->create([
|
||||
'status' => DockerImageBuild::STATUS_SUCCEEDED,
|
||||
'successful' => true,
|
||||
'exit_code' => 0,
|
||||
'build_output' => 'Successfully built image',
|
||||
'started_at' => now()->subMinute(),
|
||||
'finished_at' => now(),
|
||||
]);
|
||||
|
||||
DockerImageBuild::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('docker-builder.history'))
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('docker/BuildHistory')
|
||||
->has('builds', 2)
|
||||
->where('builds.0.id', $newerBuild->id)
|
||||
->where('builds.1.id', $olderBuild->id),
|
||||
);
|
||||
});
|
||||
|
||||
test('authenticated users can fetch their saved build status', function () {
|
||||
$user = User::factory()->create();
|
||||
$dockerImageBuild = DockerImageBuild::factory()->for($user)->create([
|
||||
'image_name' => 'ghcr.io/acme/example',
|
||||
'image_tag' => 'stable',
|
||||
'status' => DockerImageBuild::STATUS_RUNNING,
|
||||
'build_output' => 'Step 1/3 : FROM alpine:3.20',
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('docker-builder.builds.show', $dockerImageBuild))
|
||||
->assertOk()
|
||||
->assertJsonPath('build.id', $dockerImageBuild->id)
|
||||
->assertJsonPath('build.image', 'ghcr.io/acme/example:stable')
|
||||
->assertJsonPath('build.status', DockerImageBuild::STATUS_RUNNING)
|
||||
->assertJsonPath('build.build_output', 'Step 1/3 : FROM alpine:3.20');
|
||||
});
|
||||
|
||||
test('authenticated users cannot fetch builds that belong to someone else', function () {
|
||||
$user = User::factory()->create();
|
||||
$dockerImageBuild = DockerImageBuild::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('docker-builder.builds.show', $dockerImageBuild))
|
||||
->assertNotFound();
|
||||
});
|
||||
99
tests/Feature/RunDockerImageBuildJobTest.php
Normal file
99
tests/Feature/RunDockerImageBuildJobTest.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\RunDockerImageBuild;
|
||||
use App\Models\DockerImageBuild;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Process\PendingProcess;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('run docker image build job marks successful builds as succeeded', function () {
|
||||
$dockerImageBuild = DockerImageBuild::factory()->create([
|
||||
'image_name' => 'ghcr.io/acme/example',
|
||||
'image_tag' => 'latest',
|
||||
'dockerfile_content' => implode("\n", [
|
||||
'FROM alpine:3.20',
|
||||
'RUN echo "building"',
|
||||
]),
|
||||
'environment' => [
|
||||
['key' => 'APP_ENV', 'value' => 'production'],
|
||||
['key' => 'APP_NAME', 'value' => 'Docker Builder'],
|
||||
],
|
||||
]);
|
||||
|
||||
Process::fake(function (PendingProcess $process) {
|
||||
expect($process->path)->not->toBeNull();
|
||||
expect($process->command)->toBe([
|
||||
'docker',
|
||||
'build',
|
||||
'--tag',
|
||||
'ghcr.io/acme/example:latest',
|
||||
'.',
|
||||
]);
|
||||
|
||||
$dockerfile = file_get_contents($process->path.'/Dockerfile');
|
||||
|
||||
expect($dockerfile)
|
||||
->toContain('FROM alpine:3.20')
|
||||
->toContain('RUN echo "building"')
|
||||
->toContain('ENV APP_ENV="production"')
|
||||
->toContain('ENV APP_NAME="Docker Builder"');
|
||||
|
||||
return Process::result(implode("\n", [
|
||||
'Step 1/2 : FROM alpine:3.20',
|
||||
'Successfully built ghcr.io/acme/example:latest',
|
||||
]));
|
||||
});
|
||||
|
||||
(new RunDockerImageBuild($dockerImageBuild->id))->handle();
|
||||
|
||||
$dockerImageBuild->refresh();
|
||||
|
||||
expect($dockerImageBuild->status)->toBe(DockerImageBuild::STATUS_SUCCEEDED)
|
||||
->and($dockerImageBuild->successful)->toBeTrue()
|
||||
->and($dockerImageBuild->exit_code)->toBe(0)
|
||||
->and($dockerImageBuild->build_output)->toContain('Successfully built ghcr.io/acme/example:latest')
|
||||
->and($dockerImageBuild->started_at)->not->toBeNull()
|
||||
->and($dockerImageBuild->finished_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('run docker image build job marks non-zero docker exits as failed', function () {
|
||||
$dockerImageBuild = DockerImageBuild::factory()->create([
|
||||
'image_name' => 'ghcr.io/acme/example',
|
||||
'image_tag' => 'broken',
|
||||
]);
|
||||
|
||||
Process::fake(fn (PendingProcess $process) => Process::result(
|
||||
'Step 1/1 : FROM missing:latest',
|
||||
'failed to solve: missing image',
|
||||
1,
|
||||
));
|
||||
|
||||
(new RunDockerImageBuild($dockerImageBuild->id))->handle();
|
||||
|
||||
$dockerImageBuild->refresh();
|
||||
|
||||
expect($dockerImageBuild->status)->toBe(DockerImageBuild::STATUS_FAILED)
|
||||
->and($dockerImageBuild->successful)->toBeFalse()
|
||||
->and($dockerImageBuild->exit_code)->toBe(1)
|
||||
->and($dockerImageBuild->build_output)->toContain('failed to solve: missing image')
|
||||
->and($dockerImageBuild->finished_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('run docker image build job records unexpected process exceptions as failed', function () {
|
||||
$dockerImageBuild = DockerImageBuild::factory()->create();
|
||||
|
||||
Process::fake(fn (PendingProcess $process) => throw new \RuntimeException('Docker is unavailable'));
|
||||
|
||||
expect(fn () => (new RunDockerImageBuild($dockerImageBuild->id))->handle())
|
||||
->toThrow(\RuntimeException::class, 'Docker is unavailable');
|
||||
|
||||
$dockerImageBuild->refresh();
|
||||
|
||||
expect($dockerImageBuild->status)->toBe(DockerImageBuild::STATUS_FAILED)
|
||||
->and($dockerImageBuild->successful)->toBeFalse()
|
||||
->and($dockerImageBuild->exit_code)->toBe(1)
|
||||
->and($dockerImageBuild->build_output)->toContain('Docker is unavailable')
|
||||
->and($dockerImageBuild->finished_at)->not->toBeNull();
|
||||
});
|
||||
Reference in New Issue
Block a user