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); });