Drop restart-limit fields and enforcement from database resources, clear cached Traefik version data when proxies change, and show missing service environment variables from disabled deploy actions.
261 lines
11 KiB
PHP
261 lines
11 KiB
PHP
<?php
|
|
|
|
use App\Actions\Docker\GetContainersStatus;
|
|
use App\Actions\Service\StopServiceApplication;
|
|
use App\Jobs\PushServerUpdateJob;
|
|
use App\Models\ApplicationPreview;
|
|
use App\Models\ServiceApplication;
|
|
use App\Models\ServiceDatabase;
|
|
use App\Models\StandaloneClickhouse;
|
|
use App\Models\StandaloneDragonfly;
|
|
use App\Models\StandaloneKeydb;
|
|
use App\Models\StandaloneMariadb;
|
|
use App\Models\StandaloneMongodb;
|
|
use App\Models\StandaloneMysql;
|
|
use App\Models\StandalonePostgresql;
|
|
use App\Models\StandaloneRedis;
|
|
use App\Traits\HasRestartLimit;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Support\Facades\Blade;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
it('gives independently runnable application resources restart limit state', function (string $modelClass) {
|
|
expect(class_uses_recursive($modelClass))->toContain(HasRestartLimit::class);
|
|
|
|
$resource = new $modelClass;
|
|
|
|
expect($resource->getFillable())->toContain(
|
|
'restart_count',
|
|
'max_restart_count',
|
|
'restart_limit_reached',
|
|
'last_restart_at',
|
|
'last_restart_type',
|
|
)->and($resource->getCasts())->toMatchArray([
|
|
'restart_count' => 'integer',
|
|
'max_restart_count' => 'integer',
|
|
'restart_limit_reached' => 'boolean',
|
|
'last_restart_at' => 'datetime',
|
|
]);
|
|
})->with([
|
|
[ApplicationPreview::class],
|
|
[ServiceApplication::class],
|
|
]);
|
|
|
|
it('collects restart counts for preview and service containers from both status sources', function () {
|
|
$dockerStatus = file_get_contents(app_path('Actions/Docker/GetContainersStatus.php'));
|
|
$sentinelStatus = file_get_contents(app_path('Jobs/PushServerUpdateJob.php'));
|
|
|
|
expect($dockerStatus)
|
|
->toContain('previewContainerRestartCounts')
|
|
->toContain('serviceContainerRestartCounts')
|
|
->and($sentinelStatus)
|
|
->toContain('previewContainerRestartCounts')
|
|
->toContain('serviceContainerRestartCounts');
|
|
});
|
|
|
|
it('ignores docker compose one-off job containers in both status sources', function () {
|
|
$dockerStatus = file_get_contents(app_path('Actions/Docker/GetContainersStatus.php'));
|
|
$sentinelStatus = file_get_contents(app_path('Jobs/PushServerUpdateJob.php'));
|
|
|
|
expect($dockerStatus)
|
|
->toContain("filter_var(data_get(\$labels, 'com.docker.compose.oneoff'), FILTER_VALIDATE_BOOLEAN)")
|
|
->and($sentinelStatus)
|
|
->toContain("filter_var(\$labels->get('com.docker.compose.oneoff'), FILTER_VALIDATE_BOOLEAN)");
|
|
});
|
|
|
|
it('shows restart limit warnings for every resource family', function () {
|
|
$previews = file_get_contents(resource_path('views/livewire/project/application/previews.blade.php'));
|
|
$serviceCard = file_get_contents(resource_path('views/livewire/project/service/resource-card.blade.php'));
|
|
$applicationStatus = file_get_contents(resource_path('views/livewire/project/application/status.blade.php'));
|
|
$databaseStatus = file_get_contents(resource_path('views/livewire/project/database/status.blade.php'));
|
|
|
|
expect($previews)->toContain('<x-application.restart-limit-warning :application="$preview" />')
|
|
->and($serviceCard)->toContain('<x-application.restart-limit-warning :application="$resource" />')
|
|
->and($applicationStatus)->toContain('<x-application.restart-limit-warning :application="$application" />')
|
|
->and($databaseStatus)->toContain('<x-application.restart-limit-warning :application="$database" />');
|
|
|
|
$serviceStatus = file_get_contents(resource_path('views/livewire/project/service/status.blade.php'));
|
|
$serviceHeading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
|
|
expect($serviceStatus)->toContain('<x-application.restart-limit-warning :application="$selectedResource" />')
|
|
->and($serviceStatus)->toContain('$selectedResource?->status ?? $service->status')
|
|
->and($serviceHeading)->toContain('<x-application.restart-limit-warning :application="$selectedResource" />')
|
|
->and($serviceHeading)->toContain('$selectedResource?->status ?? $service->status');
|
|
});
|
|
|
|
it('matches application restart badge layout on mobile resource headings', function () {
|
|
$applicationHeading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
|
$databaseHeading = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php'));
|
|
$serviceHeading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
|
|
|
|
foreach ([$applicationHeading, $databaseHeading, $serviceHeading] as $heading) {
|
|
expect($heading)
|
|
->toContain('class="relative flex w-full min-w-0 items-center gap-2"')
|
|
->toContain('class="flex w-full flex-wrap gap-1"');
|
|
}
|
|
});
|
|
|
|
it('shows database restart limits in shared resource listings', function () {
|
|
$resourceIndex = file_get_contents(app_path('Livewire/Project/Resource/Index.php'));
|
|
$serverResources = file_get_contents(resource_path('views/livewire/server/resources.blade.php'));
|
|
$destination = file_get_contents(resource_path('views/livewire/project/shared/destination.blade.php'));
|
|
|
|
expect($resourceIndex)
|
|
->toContain("method_exists(\$item, 'stoppedAfterRestartLimit')")
|
|
->not->toContain("\$type === 'application' && \$item->stoppedAfterRestartLimit()")
|
|
->and($serverResources)
|
|
->toContain('<x-application.restart-limit-warning :application="$resource" />')
|
|
->and($destination)
|
|
->toContain('<x-application.restart-limit-warning :application="$resource" />');
|
|
});
|
|
|
|
it('keeps the service resource table readable with horizontal scrolling on mobile', function () {
|
|
$configuration = file_get_contents(resource_path('views/livewire/project/service/configuration.blade.php'));
|
|
$resourceCard = file_get_contents(resource_path('views/livewire/project/service/resource-card.blade.php'));
|
|
|
|
expect($configuration)
|
|
->toContain("'overflow-x-auto rounded-xl")
|
|
->toContain('min-w-[48rem]')
|
|
->and($resourceCard)
|
|
->toContain('min-w-[48rem]')
|
|
->not->toContain('<div class="hidden truncate font-mono');
|
|
});
|
|
|
|
it('opens service resource settings when a table row is clicked', function () {
|
|
$resourceCard = file_get_contents(resource_path('views/livewire/project/service/resource-card.blade.php'));
|
|
|
|
expect($resourceCard)
|
|
->toContain('x-on:click="openSettings($event)"')
|
|
->toContain('x-on:keydown.enter="openSettings($event)"')
|
|
->toContain("closest('a, button')")
|
|
->toContain('role="link"')
|
|
->toContain('tabindex="0"');
|
|
});
|
|
|
|
it('uses selected service resource actions instead of parent complex status actions', function () {
|
|
$heading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
|
|
$headingClass = file_get_contents(app_path('Livewire/Project/Service/Heading.php'));
|
|
|
|
expect(substr_count($heading, "\$selectedResource && \$selectedResource->container_present !== false && \$selectedResourceStatus->startsWith('exited')"))->toBe(2)
|
|
->and($heading)
|
|
->toContain('Remove container')
|
|
->toContain('removeSelectedResourceContainer')
|
|
->and($headingClass)
|
|
->toContain('public function removeSelectedResourceContainer(): void');
|
|
});
|
|
|
|
it('imports the application model used when claiming a restart limit', function () {
|
|
$statusAction = file_get_contents(app_path('Actions/Docker/GetContainersStatus.php'));
|
|
|
|
expect($statusAction)
|
|
->toContain('use App\\Models\\Application;')
|
|
->toContain('Application::query()');
|
|
});
|
|
|
|
it('limits restarts only for applications', function () {
|
|
$migrations = collect(glob(database_path('migrations/*.php')))
|
|
->map(fn (string $path): string => file_get_contents($path))
|
|
->implode("\n");
|
|
|
|
expect($migrations)
|
|
->toContain("'application_previews'")
|
|
->toContain("'service_applications'")
|
|
->toContain("'max_restart_count'")
|
|
->toContain("'restart_limit_reached'")
|
|
->toContain("dropColumn(['max_restart_count', 'restart_limit_reached'])");
|
|
|
|
$databaseModels = [
|
|
ServiceDatabase::class,
|
|
StandalonePostgresql::class,
|
|
StandaloneRedis::class,
|
|
StandaloneMongodb::class,
|
|
StandaloneMysql::class,
|
|
StandaloneMariadb::class,
|
|
StandaloneKeydb::class,
|
|
StandaloneDragonfly::class,
|
|
StandaloneClickhouse::class,
|
|
];
|
|
|
|
foreach ($databaseModels as $databaseModel) {
|
|
expect(class_uses_recursive($databaseModel))->not->toContain(HasRestartLimit::class);
|
|
}
|
|
|
|
foreach ([
|
|
'service_databases',
|
|
'standalone_postgresqls',
|
|
'standalone_redis',
|
|
'standalone_mongodbs',
|
|
'standalone_mysqls',
|
|
'standalone_mariadbs',
|
|
'standalone_keydbs',
|
|
'standalone_dragonflies',
|
|
'standalone_clickhouses',
|
|
] as $databaseTable) {
|
|
expect(Schema::hasColumn($databaseTable, 'max_restart_count'))->toBeFalse()
|
|
->and(Schema::hasColumn($databaseTable, 'restart_limit_reached'))->toBeFalse();
|
|
}
|
|
|
|
foreach ([GetContainersStatus::class, PushServerUpdateJob::class] as $statusUpdater) {
|
|
$source = file_get_contents((new ReflectionClass($statusUpdater))->getFileName());
|
|
|
|
expect($source)
|
|
->not->toContain('$database->trackRestartCount')
|
|
->not->toContain('$database->stoppedAfterRestartLimit()');
|
|
}
|
|
|
|
$stopServiceResource = file_get_contents((new ReflectionClass(StopServiceApplication::class))->getFileName());
|
|
|
|
expect($stopServiceResource)
|
|
->toContain('$resetRestartCount && $serviceApplication instanceof ServiceApplication');
|
|
});
|
|
|
|
it('does not render restart limit warnings for service databases', function () {
|
|
$html = Blade::render(
|
|
'<x-application.restart-limit-warning :application="$database" />',
|
|
['database' => new ServiceDatabase],
|
|
);
|
|
|
|
expect(trim($html))->toBeEmpty();
|
|
});
|
|
|
|
it('atomically claims a resource restart limit once and can reset it', function () {
|
|
Schema::create('restart_limit_test_resources', function (Blueprint $table): void {
|
|
$table->id();
|
|
$table->string('status')->default('running');
|
|
$table->integer('restart_count')->default(0);
|
|
$table->integer('max_restart_count')->default(2);
|
|
$table->boolean('restart_limit_reached')->default(false);
|
|
$table->timestamp('last_restart_at')->nullable();
|
|
$table->string('last_restart_type')->nullable();
|
|
$table->timestamps();
|
|
});
|
|
|
|
$resource = new class extends Model
|
|
{
|
|
use HasRestartLimit;
|
|
|
|
protected $table = 'restart_limit_test_resources';
|
|
};
|
|
$resource->save();
|
|
$resource->refresh();
|
|
|
|
expect($resource->trackRestartCount(2))->toBeTrue()
|
|
->and($resource->fresh()->restart_limit_reached)->toBeTrue()
|
|
->and($resource->trackRestartCount(2))->toBeFalse();
|
|
|
|
$resource->resetRestartLimit();
|
|
|
|
expect($resource->fresh()->restart_count)->toBe(0)
|
|
->and($resource->restart_limit_reached)->toBeFalse();
|
|
|
|
$resourceWithExistingRestarts = $resource->newInstance();
|
|
$resourceWithExistingRestarts->max_restart_count = 0;
|
|
$resourceWithExistingRestarts->save();
|
|
expect($resourceWithExistingRestarts->trackRestartCount(17))->toBeFalse();
|
|
|
|
$resourceWithExistingRestarts->update(['max_restart_count' => 10]);
|
|
expect($resourceWithExistingRestarts->trackRestartCount(17))->toBeTrue()
|
|
->and($resourceWithExistingRestarts->fresh()->restart_limit_reached)->toBeTrue();
|
|
|
|
Schema::drop('restart_limit_test_resources');
|
|
});
|