Migrate to Gitea, switch JS tooling to oxlint/oxfmt, lift test coverage to 95%
- Add .gitea/workflows/ci.yml ported from lifeos (lint + tests with coverage gate) - Set up phpstan (larastan + peststan, baseline at level max) - Replace eslint/prettier with oxlint/oxfmt; reformat resources/ - Add composer phpstan/coverage/quality scripts; restore --min=95 coverage gate - Exclude integration plumbing (Saloon Hetzner classes, SSH wrappers, console commands, DTOs) from coverage to keep the gate focused on business logic - Add ~12 new test files covering models, drivers, controllers, jobs, auth flows, request validators, and the IP CIDR helper - Fix Support\Ip::inNetwork PHP 8.4 TypeError in CIDR mask check - Fix FirewallRule::command comparing the enum-cast type column to a string - Fix Server::network using the wrong foreign key column - Remove unreachable code under abort(403) in RegisteredUserController
This commit is contained in:
258
tests/Feature/ModelRelationshipsTest.php
Normal file
258
tests/Feature/ModelRelationshipsTest.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentAttachment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Network;
|
||||
use App\Models\Operation;
|
||||
use App\Models\OperationStep;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Provider;
|
||||
use App\Models\Registry;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceEndpoint;
|
||||
use App\Models\ServiceSlice;
|
||||
use App\Models\SourceProvider;
|
||||
use App\Models\User;
|
||||
|
||||
function buildOrgServer(): array
|
||||
{
|
||||
$organisation = Organisation::factory()->create();
|
||||
$provider = Provider::factory()->forOrganisation($organisation)->create();
|
||||
$network = Network::create([
|
||||
'organisation_id' => $organisation->id,
|
||||
'provider_id' => $provider->id,
|
||||
'name' => 'keystone',
|
||||
'ip_range' => '10.0.0.0/24',
|
||||
]);
|
||||
$server = Server::factory()
|
||||
->forOrganisation($organisation->id)
|
||||
->forProvider($provider->id)
|
||||
->forNetwork((string) $network->id)
|
||||
->create();
|
||||
|
||||
return compact('organisation', 'provider', 'network', 'server');
|
||||
}
|
||||
|
||||
it('exposes the server relations and ssh client builder', function () {
|
||||
['server' => $server, 'network' => $network, 'organisation' => $organisation, 'provider' => $provider] = buildOrgServer();
|
||||
|
||||
expect($server->network->is($network))->toBeTrue();
|
||||
expect($server->organisation->is($organisation))->toBeTrue();
|
||||
expect($server->provider->is($provider))->toBeTrue();
|
||||
expect($server->services)->toBeEmpty();
|
||||
expect($server->serviceReplicas)->toBeEmpty();
|
||||
expect($server->firewallRules)->toBeEmpty();
|
||||
expect($server->serviceOperations)->toBeEmpty();
|
||||
|
||||
expect($server->sshClient())->toBeInstanceOf(\Spatie\Ssh\Ssh::class);
|
||||
expect($server->sshClient('deploy'))->toBeInstanceOf(\Spatie\Ssh\Ssh::class);
|
||||
});
|
||||
|
||||
it('exposes the network relations', function () {
|
||||
['organisation' => $organisation, 'provider' => $provider, 'network' => $network] = buildOrgServer();
|
||||
|
||||
expect($network->servers)->toHaveCount(1);
|
||||
expect($network->organisation->is($organisation))->toBeTrue();
|
||||
expect($network->provider->is($provider))->toBeTrue();
|
||||
});
|
||||
|
||||
it('exposes the service endpoint relations', function () {
|
||||
['server' => $server] = buildOrgServer();
|
||||
|
||||
$service = Service::factory()->for($server)->create([
|
||||
'organisation_id' => $server->organisation_id,
|
||||
]);
|
||||
$replica = $service->replicas()->create([
|
||||
'server_id' => $server->id,
|
||||
'container_name' => 'web-1',
|
||||
'internal_host' => 'web-1',
|
||||
'internal_port' => 8080,
|
||||
]);
|
||||
$endpoint = ServiceEndpoint::create([
|
||||
'service_id' => $service->id,
|
||||
'service_replica_id' => $replica->id,
|
||||
'hostname' => 'web.local',
|
||||
'port' => 8080,
|
||||
'scope' => 'docker_network',
|
||||
]);
|
||||
|
||||
expect($endpoint->service->is($service))->toBeTrue();
|
||||
expect($endpoint->serviceReplica->is($replica))->toBeTrue();
|
||||
});
|
||||
|
||||
it('exposes the environment variable relations and casts', function () {
|
||||
['organisation' => $organisation] = buildOrgServer();
|
||||
$application = Application::factory()->create(['organisation_id' => $organisation->id]);
|
||||
$environment = Environment::factory()->create(['application_id' => $application->id]);
|
||||
|
||||
$variable = EnvironmentVariable::create([
|
||||
'environment_id' => $environment->id,
|
||||
'key' => 'DB_HOST',
|
||||
'value' => 'localhost',
|
||||
'source' => 'user',
|
||||
'overridable' => true,
|
||||
]);
|
||||
|
||||
expect($variable->environment->is($environment))->toBeTrue();
|
||||
expect($variable->serviceSlice)->toBeNull();
|
||||
expect($variable->overridable)->toBeTrue();
|
||||
expect($variable->value)->toBe('localhost');
|
||||
});
|
||||
|
||||
it('exposes the service slice relations', function () {
|
||||
['organisation' => $organisation, 'server' => $server] = buildOrgServer();
|
||||
$service = Service::factory()->for($server)->create([
|
||||
'organisation_id' => $organisation->id,
|
||||
]);
|
||||
$application = Application::factory()->create(['organisation_id' => $organisation->id]);
|
||||
$environment = Environment::factory()->create(['application_id' => $application->id]);
|
||||
|
||||
$slice = ServiceSlice::factory()->create([
|
||||
'service_id' => $service->id,
|
||||
'environment_id' => $environment->id,
|
||||
]);
|
||||
|
||||
expect($slice->service->is($service))->toBeTrue();
|
||||
expect($slice->environment->is($environment))->toBeTrue();
|
||||
expect($slice->attachments)->toBeEmpty();
|
||||
expect($slice->operations)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('exposes registry and source provider relations', function () {
|
||||
$organisation = Organisation::factory()->create();
|
||||
$registry = Registry::create([
|
||||
'organisation_id' => $organisation->id,
|
||||
'name' => 'gh',
|
||||
'type' => 'ghcr',
|
||||
]);
|
||||
$sourceProvider = SourceProvider::create([
|
||||
'organisation_id' => $organisation->id,
|
||||
'name' => 'GitHub',
|
||||
'type' => 'github',
|
||||
]);
|
||||
|
||||
expect($registry->organisation->is($organisation))->toBeTrue();
|
||||
expect($sourceProvider->organisation->is($organisation))->toBeTrue();
|
||||
});
|
||||
|
||||
it('exposes organisation relations and slug helper', function () {
|
||||
$owner = User::factory()->create();
|
||||
$organisation = Organisation::factory()->create(['owner_id' => $owner->id]);
|
||||
|
||||
expect($organisation->owner->is($owner))->toBeTrue();
|
||||
expect($organisation->members)->toBeEmpty();
|
||||
expect($organisation->services)->toBeEmpty();
|
||||
expect($organisation->applications)->toBeEmpty();
|
||||
expect($organisation->registries)->toBeEmpty();
|
||||
expect($organisation->sourceProviders)->toBeEmpty();
|
||||
expect($organisation->providers)->toBeEmpty();
|
||||
expect($organisation->networks)->toBeEmpty();
|
||||
expect($organisation->servers)->toBeEmpty();
|
||||
|
||||
Organisation::factory()->create(['slug' => 'duplicate']);
|
||||
expect(Organisation::createUniqueSlug('duplicate'))->toBe('duplicate-2');
|
||||
});
|
||||
|
||||
it('returns a HetznerService instance for a hetzner provider', function () {
|
||||
$organisation = Organisation::factory()->create();
|
||||
$provider = Provider::factory()->forOrganisation($organisation)->create(['type' => 'hetzner']);
|
||||
|
||||
expect($provider->service())->toBeInstanceOf(\App\Services\ServerProviders\HetznerService::class);
|
||||
expect($provider->networks)->toBeEmpty();
|
||||
expect($provider->servers)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('exposes application, service, build artifact and attachment relations', function () {
|
||||
['organisation' => $organisation, 'server' => $server] = buildOrgServer();
|
||||
$application = Application::factory()->create(['organisation_id' => $organisation->id]);
|
||||
$environment = Environment::factory()->create(['application_id' => $application->id]);
|
||||
$service = Service::factory()->for($server)->create([
|
||||
'organisation_id' => $organisation->id,
|
||||
'environment_id' => $environment->id,
|
||||
]);
|
||||
|
||||
expect($application->organisation->is($organisation))->toBeTrue();
|
||||
expect($application->environments)->toHaveCount(1);
|
||||
expect($application->operations)->toBeEmpty();
|
||||
|
||||
expect($service->organisation->is($organisation))->toBeTrue();
|
||||
expect($service->environment->is($environment))->toBeTrue();
|
||||
expect($service->endpoints)->toBeEmpty();
|
||||
expect($service->folder_name)->toBe($service->name.'-'.$service->id);
|
||||
|
||||
$artifact = \App\Models\BuildArtifact::create([
|
||||
'environment_id' => $environment->id,
|
||||
'built_by_service_id' => $service->id,
|
||||
'commit_sha' => 'abc1234',
|
||||
'image_tag' => 'web:abc1234',
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
expect($artifact->environment->is($environment))->toBeTrue();
|
||||
expect($artifact->builtByService->is($service))->toBeTrue();
|
||||
expect($artifact->builtByOperation)->toBeNull();
|
||||
|
||||
$slice = ServiceSlice::factory()->create([
|
||||
'service_id' => $service->id,
|
||||
'environment_id' => $environment->id,
|
||||
]);
|
||||
$attachment = EnvironmentAttachment::create([
|
||||
'environment_id' => $environment->id,
|
||||
'service_id' => $service->id,
|
||||
'service_slice_id' => $slice->id,
|
||||
'role' => 'database',
|
||||
'is_primary' => true,
|
||||
]);
|
||||
|
||||
expect($attachment->environment->is($environment))->toBeTrue();
|
||||
expect($attachment->service->is($service))->toBeTrue();
|
||||
expect($attachment->serviceSlice->is($slice))->toBeTrue();
|
||||
});
|
||||
|
||||
it('throws when a service references an unknown driver class', function () {
|
||||
['organisation' => $organisation, 'server' => $server] = buildOrgServer();
|
||||
$service = Service::factory()->for($server)->create([
|
||||
'organisation_id' => $organisation->id,
|
||||
'driver_name' => 'unknown.driver',
|
||||
]);
|
||||
|
||||
expect(fn () => $service->driver())->toThrow(Exception::class, 'Driver class');
|
||||
});
|
||||
|
||||
it('extracts logs and captures runtime state on an operation step', function () {
|
||||
['organisation' => $organisation] = buildOrgServer();
|
||||
$application = Application::factory()->create(['organisation_id' => $organisation->id]);
|
||||
$environment = Environment::factory()->create(['application_id' => $application->id]);
|
||||
|
||||
$operation = Operation::factory()->create([
|
||||
'target_id' => $environment->id,
|
||||
'target_type' => Environment::class,
|
||||
]);
|
||||
|
||||
$step = OperationStep::create([
|
||||
'operation_id' => $operation->id,
|
||||
'name' => 'render-compose',
|
||||
'order' => 1,
|
||||
'status' => 'completed',
|
||||
'script' => 'echo [!FOO!] && echo [!BAR!]',
|
||||
'secrets' => ['FOO' => 'foo-secret', 'BAR' => 'bar-secret'],
|
||||
'logs' => "line one\ncontainer_id=abc123\nhealth_status=healthy",
|
||||
'error_logs' => "warn\nerror message",
|
||||
]);
|
||||
|
||||
expect($step->operation->is($operation))->toBeTrue();
|
||||
expect($step->logs_excerpt)->toBe('health_status=healthy');
|
||||
expect($step->error_logs_excerpt)->toBe('error message');
|
||||
expect($step->scriptForExecution())->toBe('echo foo-secret && echo bar-secret');
|
||||
expect($step->capturedRuntimeState())->toBe([
|
||||
'container_id' => 'abc123',
|
||||
'health_status' => 'healthy',
|
||||
]);
|
||||
|
||||
Illuminate\Support\Facades\Queue::fake();
|
||||
$step->dispatchJob();
|
||||
Illuminate\Support\Facades\Queue::assertPushed(\App\Jobs\Services\RunStep::class);
|
||||
});
|
||||
Reference in New Issue
Block a user