create(); $organisation = Organisation::factory()->create([ 'owner_id' => $user->id, ]); $provider = Provider::factory()->create([ 'organisation_id' => $organisation->id, ]); $network = Network::create([ 'name' => 'test-network', 'ip_range' => '10.0.0.0/24', 'external_id' => 'ext-12345', 'organisation_id' => $organisation->id, 'provider_id' => $provider->id, ]); $server = Server::factory()->create([ 'organisation_id' => $organisation->id, 'provider_id' => $provider->id, 'network_id' => $network->id, ]); return [ 'user' => $user, 'organisation' => $organisation, 'provider' => $provider, 'network' => $network, 'server' => $server, ]; } test('create service page is accessible', function () { $setup = setupTestEnvironment(); $this->actingAs($setup['user']); $response = $this->get(route('services.create', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, ])); $response->assertStatus(200); $response->assertInertia( fn (AssertableInertia $page) => $page ->component('services/Create', false) ->has('server') ->has('services') ); }); test('store service with valid data', function () { $setup = setupTestEnvironment(); $this->actingAs($setup['user']); $mockDefaultCredentials = [ 'user' => 'test-user', 'password' => 'test-password', 'db' => 'test-db', ]; $mockDriver = Mockery::mock(Driver::class); $mockDriver->shouldReceive('defaultCredentials')->andReturn($mockDefaultCredentials); // intercept the driver $this->partialMock(Service::class, function ($mock) use ($mockDriver) { $mock->shouldReceive('driver')->andReturn($mockDriver); }); Bus::fake(); $data = [ 'name' => 'test-postgres-database', 'category' => ServiceCategory::DATABASE->value, 'type' => ServiceType::POSTGRES->value, 'version' => '18', ]; $response = $this->post(route('services.store', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, ]), $data); // Since we're not mocking the entire CreateService action, we should get a proper redirect $response->assertRedirect(route('servers.show', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, ])); $response->assertSessionHas('success', 'Service created successfully'); $this->assertDatabaseHas('services', [ 'name' => 'test-postgres-database', 'server_id' => $setup['server']->id, 'category' => ServiceCategory::DATABASE->value, 'type' => ServiceType::POSTGRES->value, 'version' => '18', 'version_track' => '18', 'driver_name' => 'postgres.18', 'deploy_policy' => DeployPolicy::DEPENDENCY_ONLY->value, 'status' => ServiceStatus::NOT_INSTALLED->value, ]); $service = Service::query()->where('name', 'test-postgres-database')->firstOrFail(); expect($service->credentials) ->toHaveKey('user') ->toHaveKey('password') ->toHaveKey('db'); $this->assertDatabaseHas('service_replicas', [ 'service_id' => $service->id, 'server_id' => $setup['server']->id, 'container_name' => "keystone-service-{$service->id}-1", 'internal_host' => "keystone-service-{$service->id}", 'internal_port' => 5432, 'status' => 'pending', 'health_status' => 'unknown', ]); Bus::assertDispatched(DeployService::class); }); test('store service with invalid data', function () { $setup = setupTestEnvironment(); $this->actingAs($setup['user']); $data = [ 'name' => '', // Invalid name 'category' => 'invalid-category', 'type' => 'invalid-type', 'version' => 'invalid-version', ]; $response = $this->post(route('services.store', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, ]), $data); $response->assertSessionHasErrors(['name', 'category', 'type', 'version']); }); test('store service validates version exists in config', function () { $setup = setupTestEnvironment(); $this->actingAs($setup['user']); // Mock the config to simulate the version not existing Config::set('keystone.services.'.ServiceCategory::DATABASE->value.'.'.ServiceType::POSTGRES->value.'.versions', [ '17' => [ 'name' => 'PostgreSQL 17', 'description' => 'PostgreSQL 17', 'image' => 'postgres:17', ], ]); $data = [ 'name' => 'test-postgres-database', 'category' => ServiceCategory::DATABASE->value, 'type' => ServiceType::POSTGRES->value, 'version' => '18', // This version doesn't exist in our mocked config ]; $response = $this->post(route('services.store', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, ]), $data); $response->assertSessionHasErrors(['version']); }); test('store service prevents duplicate gateway on the same server', function () { $setup = setupTestEnvironment(); $this->actingAs($setup['user']); Service::factory()->for($setup['server'])->create([ 'organisation_id' => $setup['organisation']->id, 'name' => 'gateway', 'category' => ServiceCategory::GATEWAY, 'type' => ServiceType::CADDY, 'version' => '2', 'version_track' => '2', 'driver_name' => 'caddy.2', ]); $response = $this->post(route('services.store', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, ]), [ 'name' => 'another-gateway', 'category' => ServiceCategory::GATEWAY->value, 'type' => ServiceType::CADDY->value, 'version' => '2', ]); $response->assertSessionHasErrors(['category' => 'This server already has a gateway service.']); }); test('create service action prevents duplicate gateway on the same server', function () { $setup = setupTestEnvironment(); Service::factory()->for($setup['server'])->create([ 'organisation_id' => $setup['organisation']->id, 'name' => 'gateway', 'category' => ServiceCategory::GATEWAY, 'type' => ServiceType::CADDY, 'version' => '2', 'version_track' => '2', 'driver_name' => 'caddy.2', ]); expect(fn () => app(CreateService::class)->execute( server: $setup['server'], name: 'another-gateway', category: ServiceCategory::GATEWAY, type: ServiceType::CADDY, version: '2', ))->toThrow(RuntimeException::class, 'This server already has a gateway service.'); }); test('store service with non-existent server returns 404', function () { $setup = setupTestEnvironment(); $this->actingAs($setup['user']); $data = [ 'name' => 'test-postgres-database', 'category' => ServiceCategory::DATABASE->value, 'type' => ServiceType::POSTGRES->value, 'version' => '18', ]; $response = $this->post(route('services.store', [ 'organisation' => $setup['organisation']->id, 'server' => 9999, ]), $data); $response->assertStatus(404); }); test('create service page with non-existent server returns 404', function () { $setup = setupTestEnvironment(); $this->actingAs($setup['user']); $response = $this->get(route('services.create', [ 'organisation' => $setup['organisation']->id, 'server' => 9999, ])); $response->assertStatus(404); }); test('store service is properly created and dispatched', function () { $setup = setupTestEnvironment(); $this->actingAs($setup['user']); // Setup mock credentials and driver $mockDriver = Mockery::mock(Driver::class)->shouldReceive('defaultCredentials') ->andReturn([ 'user' => 'test-user', 'password' => 'test-password', 'db' => 'test-db', ]) ->getMock(); // Setup test data $testData = [ 'name' => 'test-postgres-database', 'category' => ServiceCategory::DATABASE->value, 'type' => ServiceType::POSTGRES->value, 'version' => '18', ]; // Mock service class to return our mock driver $this->partialMock(Service::class, function ($mock) use ($mockDriver) { $mock->shouldReceive('driver')->andReturn($mockDriver); }); // Mock CreateService action $this->mock(CreateService::class, function ($mock) use ($setup, $testData) { $service = new Service([ 'id' => 1, 'server_id' => $setup['server']->id, 'name' => $testData['name'], 'category' => ServiceCategory::DATABASE, 'type' => ServiceType::POSTGRES, 'version' => $testData['version'], 'driver_name' => 'postgres.18', 'status' => ServiceStatus::NOT_INSTALLED, ]); $service->setRelation('server', $setup['server']); $mock->shouldReceive('execute') ->once() ->withArgs(function ($server, $name, $category, $type, $version) use ($setup, $testData) { return $server->id === $setup['server']->id && $name === $testData['name'] && $category === ServiceCategory::DATABASE && $type === ServiceType::POSTGRES && $version === $testData['version']; }) ->andReturn($service); }); Bus::fake(); // Execute request $response = $this->post(route('services.store', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, ]), $testData); // Assert response $response->assertRedirect(route('servers.show', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, ])); $response->assertSessionHas('success', 'Service created successfully'); Bus::assertNotDispatched(DeployService::class); }); test('show service page renders inertia view', function () { $setup = setupTestEnvironment(); actingAs($setup['user']); $service = Service::factory()->create([ 'server_id' => $setup['server']->id, 'organisation_id' => $setup['organisation']->id, ]); $response = $this->get(route('services.show', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $service->id, ])); $response->assertOk(); $response->assertInertia(fn (AssertableInertia $page) => $page ->component('services/Show', false)); }); test('edit service page renders inertia view', function () { $setup = setupTestEnvironment(); actingAs($setup['user']); $service = Service::factory()->create([ 'server_id' => $setup['server']->id, 'organisation_id' => $setup['organisation']->id, ]); $response = $this->get(route('services.edit', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $service->id, ])); $response->assertOk(); $response->assertInertia(fn (AssertableInertia $page) => $page ->component('services/Edit', false)); }); test('update service persists changes and redirects', function () { $setup = setupTestEnvironment(); actingAs($setup['user']); $service = Service::factory()->create([ 'server_id' => $setup['server']->id, 'organisation_id' => $setup['organisation']->id, 'name' => 'web', 'desired_replicas' => 1, ]); $response = $this->put(route('services.update', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $service->id, ]), [ 'name' => 'web-renamed', 'desired_replicas' => 4, 'default_cpu_limit' => 2, 'default_memory_limit_mb' => 1024, ]); $response->assertRedirect(route('services.show', [ 'organisation' => $setup['organisation']->id, 'server' => $setup['server']->id, 'service' => $service->id, ])); $response->assertSessionHas('success', 'Service updated.'); $service->refresh(); expect($service->name)->toBe('web-renamed'); expect($service->desired_replicas)->toBe(4); });