create(); $organisation = Organisation::factory()->create(['owner_id' => $user->id]); $provider = Provider::factory()->forOrganisation($organisation)->create(); $network = $organisation->networks()->create([ 'provider_id' => $provider->id, 'external_id' => 'network-1', 'network_zone' => 'global', 'name' => 'keystone-global', 'ip_range' => '10.0.0.0/16', ]); $server = Server::factory() ->forOrganisation($organisation->id) ->forProvider((string) $provider->id) ->forNetwork((string) $network->id) ->create(); $service = Service::factory()->for($organisation)->for($server)->create(); return compact('user', 'organisation', 'server', 'service'); } it('shows replica detail and queues lifecycle operations', function () { $setup = serverSetup(); $replica = ServiceReplica::factory() ->for($setup['service']) ->for($setup['server']) ->create(); $this->actingAs($setup['user']) ->get(route('service-replicas.show', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $setup['service']->id, 'replica' => $replica->id, ])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('service-replicas/Show', false) ->where('replica.id', $replica->id)); $this->actingAs($setup['user']) ->post(route('service-replicas.start', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $setup['service']->id, 'replica' => $replica->id, ])) ->assertRedirect(route('service-replicas.show', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $setup['service']->id, 'replica' => $replica->id, ])); $this->actingAs($setup['user']) ->post(route('service-replicas.stop', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $setup['service']->id, 'replica' => $replica->id, ])) ->assertRedirect(route('service-replicas.show', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $setup['service']->id, 'replica' => $replica->id, ])); $this->actingAs($setup['user']) ->post(route('service-replicas.restart', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $setup['service']->id, 'replica' => $replica->id, ])) ->assertRedirect(route('service-replicas.show', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $setup['service']->id, 'replica' => $replica->id, ])); expect($replica->operations()->where('kind', OperationKind::REPLICA_DEPLOY)->count())->toBe(3) ->and($replica->operations()->first()->status)->toBe(OperationStatus::PENDING) ->and($replica->operations()->whereHas('steps', fn ($query) => $query->where('name', 'Start replica')->where('script', "docker start {$replica->container_name}"))->exists())->toBeTrue() ->and($replica->operations()->whereHas('steps', fn ($query) => $query->where('name', 'Stop replica')->where('script', "docker stop {$replica->container_name}"))->exists())->toBeTrue() ->and($replica->operations()->whereHas('steps', fn ($query) => $query->where('name', 'Restart replica')->where('script', "docker restart {$replica->container_name}"))->exists())->toBeTrue(); }); it('creates and shows service slices with independent operations', function () { $setup = serverSetup(); $this->actingAs($setup['user']) ->get(route('service-slices.create', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $setup['service']->id, ])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('service-slices/Create', false)); $this->actingAs($setup['user']) ->post(route('service-slices.store', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $setup['service']->id, ]), [ 'name' => 'readonly', 'type' => 'database_user', 'status' => 'pending', 'config' => '{"access":"readonly"}', ]) ->assertRedirect(); $slice = $setup['service']->slices()->where('name', 'readonly')->firstOrFail(); $slice->operations()->create([ 'kind' => OperationKind::SLICE_PROVISION, 'status' => OperationStatus::PENDING, ]); $this->actingAs($setup['user']) ->get(route('service-slices.index', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $setup['service']->id, ])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('service-slices/Index', false) ->has('slices', 1) ->where('slices.0.id', $slice->id) ->has('slices.0.operations', 1)); $this->actingAs($setup['user']) ->get(route('service-slices.show', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $setup['service']->id, 'slice' => $slice->id, ])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('service-slices/Show', false) ->where('slice.id', $slice->id) ->has('slice.operations', 1)); }); it('updates and detaches environment attachments', function () { $setup = serverSetup(); $application = Application::factory()->for($setup['organisation'])->create(); $environment = Environment::factory()->for($application)->create(); $slice = ServiceSlice::factory()->for($setup['service'])->for($environment)->create(); $attachment = $environment->attachments()->create([ 'service_id' => $setup['service']->id, 'service_slice_id' => $slice->id, 'role' => EnvironmentAttachmentRole::DATABASE, 'env_prefix' => null, 'is_primary' => true, ]); $this->actingAs($setup['user']) ->get(route('environment-attachments.edit', [ 'organisation' => $setup['organisation']->id, 'application' => $application->id, 'environment' => $environment->id, 'attachment' => $attachment->id, ])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('environment-attachments/Edit', false)); $this->actingAs($setup['user']) ->put(route('environment-attachments.update', [ 'organisation' => $setup['organisation']->id, 'application' => $application->id, 'environment' => $environment->id, 'attachment' => $attachment->id, ]), [ 'role' => EnvironmentAttachmentRole::CACHE->value, 'env_prefix' => 'CACHE', 'is_primary' => false, ]) ->assertRedirect(route('environments.show', [$setup['organisation'], $application, $environment])); expect($attachment->refresh()->role)->toBe(EnvironmentAttachmentRole::CACHE) ->and($attachment->env_prefix)->toBe('CACHE') ->and($attachment->is_primary)->toBeFalse(); $this->actingAs($setup['user']) ->delete(route('environment-attachments.destroy', [ 'organisation' => $setup['organisation']->id, 'application' => $application->id, 'environment' => $environment->id, 'attachment' => $attachment->id, ])) ->assertRedirect(route('environments.show', [$setup['organisation'], $application, $environment])); expect($environment->attachments()->exists())->toBeFalse(); }); it('lists and shows build artifacts', function () { $setup = serverSetup(); $application = Application::factory()->for($setup['organisation'])->create(); $environment = Environment::factory()->for($application)->create(); $artifact = BuildArtifact::query()->create([ 'environment_id' => $environment->id, 'commit_sha' => 'abc123', 'image_tag' => 'app:abc123', 'image_digest' => 'sha256:test', 'registry_ref' => 'registry.example.com/app:abc123', 'built_by_service_id' => $setup['service']->id, 'status' => BuildArtifactStatus::AVAILABLE, 'metadata' => ['build_strategy' => 'target_server'], ]); $this->actingAs($setup['user']) ->get(route('build-artifacts.index', [$setup['organisation'], $application, $environment])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('build-artifacts/Index', false) ->has('artifacts.data', 1)); $this->actingAs($setup['user']) ->get(route('build-artifacts.show', [ 'organisation' => $setup['organisation']->id, 'application' => $application->id, 'environment' => $environment->id, 'artifact' => $artifact->id, ])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('build-artifacts/Show', false) ->where('artifact.id', $artifact->id)); });