wowowowowo
Some checks failed
CI / Lint (push) Failing after 22s
CI / Tests (push) Failing after 33s

This commit is contained in:
2026-05-28 15:15:41 +01:00
parent 8f603122e2
commit 5b977c1f41
129 changed files with 9943 additions and 722 deletions

View File

@@ -1,221 +1,167 @@
<?php
use App\Data\ServerProviders\CreatedServer;
use App\Enums\ProviderType;
use App\Actions\FirewallRules\InstallFirewallRule;
use App\Actions\FirewallRules\UninstallFirewallRule;
use App\Enums\FirewallRuleType;
use App\Enums\OperationKind;
use App\Enums\OperationStatus;
use App\Enums\ServerStatus;
use App\Models\FirewallRule;
use App\Models\Organisation;
use App\Models\Provider;
use App\Models\Server;
use App\Models\User;
use App\Services\ServerProviders\HetznerService;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;
use Mockery\MockInterface;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\mock;
beforeEach(function () {
// If you have database migrations or any setup, include it here
// For example, using Laravel's RefreshDatabase trait
// use Illuminate\Foundation\Testing\RefreshDatabase;
/** @var User $user */
$this->user = User::factory()->create();
actingAs($this->user);
});
test('index route displays servers for an organisation', function () {
$organisation = Organisation::factory()->create();
$provider = Provider::factory()->forOrganisation($organisation->id)->create();
$network = $organisation->networks()->create([
'name' => 'keystone',
'external_id' => 'net-12345',
'provider_id' => $provider->id,
'ip_range' => fake()->ipv4().'/24',
]);
Server::factory()->count(2)->create([
'provider_id' => $provider->id,
'organisation_id' => $organisation->id,
'network_id' => $network->id,
]);
$response = $this->get(route('servers.index', ['organisation' => $organisation->id]));
$response->assertStatus(200);
$response->assertInertia(fn (AssertableInertia $page) => $page
->component('servers/Index', false));
});
test('create route returns inertia view', function () {
$organisation = Organisation::factory()->create();
$response = $this->get(route('servers.create', ['organisation' => $organisation->id]));
$response->assertStatus(200);
$response->assertInertia(fn (AssertableInertia $page) => $page
->component('servers/Create', false));
});
test('store route fails with invalid provider', function () {
$organisation = Organisation::factory()->create();
$response = $this->post(route('servers.store', ['organisation' => $organisation->id]), [
'provider' => 'invalid_provider',
'server_type' => 'cx11',
'location' => 'hel1',
'image' => 'ubuntu-20.04',
]);
$response->assertSessionHasErrors(['provider' => 'The selected provider is invalid.']);
$response->assertStatus(302); // redirect back
});
test('store route creates a server with valid data', function () {
$organisation = Organisation::factory()->create();
// Create a real provider first, then partially mock it
$provider = Provider::factory()->create([
'name' => 'hetzner',
'type' => ProviderType::HETZNER,
'token' => Str::uuid(),
'organisation_id' => $organisation->id,
]);
$network = $organisation->networks()->create([
'name' => 'keystone',
'external_id' => 'net-12345',
'provider_id' => $provider->id,
'ip_range' => fake()->ipv4().'/24',
]);
$this->partialMock(HetznerService::class, function (MockInterface $mock) use ($network) {
$mock->shouldReceive('forProvider')
->andReturnSelf();
$mock->shouldReceive('createServer')
->once()
->andReturn(new CreatedServer(
name: 'test-server-from-mock',
rootPassword: 'password123',
id: 'srv-12345',
status: 'running',
ipv4: '192.0.2.100',
ipv6: '2001:db8::100',
networkId: $network->external_id,
privateIp: '10.0.0.1',
));
$mock->shouldReceive('createNetwork')->never();
});
$response = $this->post(route('servers.store', ['organisation' => $organisation->id]), [
'provider' => $provider->id,
'server_type' => 'cx11',
'location' => 'hel1',
'image' => 'ubuntu-20.04',
]);
$response->assertRedirectContains('/servers/');
$this->assertDatabaseHas('servers', [
'organisation_id' => $organisation->id,
'provider_id' => $provider->id,
'region' => 'hel1',
'os' => 'ubuntu-20.04',
'network_id' => $network->id,
]);
});
test('show route displays a single server', function () {
$organisation = Organisation::factory()->create();
it('lists private network membership on the servers index', function () {
$user = User::factory()->create();
$organisation = Organisation::factory()->create(['owner_id' => $user->id]);
$provider = Provider::factory()->forOrganisation($organisation)->create();
$network = $organisation->networks()->create([
'name' => 'keystone',
'external_id' => 'net-12345',
'provider_id' => $provider->id,
'ip_range' => fake()->ipv4().'/24',
]);
$server = Server::factory()->create([
'organisation_id' => $organisation->id,
'network_id' => $network->id,
'provider_id' => $provider->id,
'external_id' => 'network-1',
'network_zone' => 'eu-central',
'name' => 'keystone-eu-central',
'ip_range' => '10.42.0.0/16',
]);
$server = Server::factory()
->forOrganisation($organisation->id)
->forProvider((string) $provider->id)
->forNetwork((string) $network->id)
->create([
'name' => 'app-1',
'private_ip' => '10.42.0.10',
]);
$response = $this->get(route('servers.show', [
'organisation' => $organisation->id,
'server' => $server->id,
]));
$response->assertStatus(200);
$response->assertInertia(fn (AssertableInertia $page) => $page
->component('servers/Show', false));
$this->actingAs($user)
->get(route('servers.index', $organisation))
->assertOk()
->assertInertia(fn (AssertableInertia $page) => $page
->component('servers/Index', false)
->where('networks.0.name', 'keystone-eu-central')
->where('networks.0.servers.0.id', $server->id)
->where('networks.0.servers.0.private_ip', '10.42.0.10'));
});
test('create route fetches and caches locations, server types and images when provider param is given', function () {
$organisation = Organisation::factory()->create();
$provider = Provider::factory()->forOrganisation($organisation)->create([
'type' => ProviderType::HETZNER,
]);
$this->partialMock(HetznerService::class, function (MockInterface $mock) {
$mock->shouldReceive('forProvider')->andReturnSelf();
$mock->shouldReceive('getLocations')->andReturn(collect());
$mock->shouldReceive('getServerTypes')->andReturn(collect());
$mock->shouldReceive('getImages')->andReturn(collect());
});
$response = $this->get(route('servers.create', [
'organisation' => $organisation->id,
'provider' => $provider->id,
]));
$response->assertOk();
$response->assertInertia(fn (AssertableInertia $page) => $page
->component('servers/Create', false)
->has('locations')
->has('serverTypes')
->has('images'));
});
test('store route creates a network when none exists yet for the provider', function () {
$organisation = Organisation::factory()->create();
$provider = Provider::factory()->forOrganisation($organisation)->create([
'type' => ProviderType::HETZNER,
]);
$this->partialMock(HetznerService::class, function (MockInterface $mock) {
$mock->shouldReceive('forProvider')->andReturnSelf();
$mock->shouldReceive('createNetwork')
->once()
->andReturn(new \App\Data\ServerProviders\Network(
id: 'net-99',
name: 'keystone-global',
ipRange: '10.0.0.0/24',
networkZone: 'global',
));
$mock->shouldReceive('createServer')
->once()
->andReturn(new CreatedServer(
name: 'fresh-server',
rootPassword: 'pw',
id: 'srv-1',
status: 'running',
ipv4: '203.0.113.10',
ipv6: '2001:db8::10',
networkId: 'net-99',
privateIp: '10.0.0.10',
));
});
$response = $this->post(route('servers.store', ['organisation' => $organisation->id]), [
'provider' => $provider->id,
'server_type' => 'cx11',
'location' => 'hel1',
'image' => 'ubuntu-22.04',
]);
$response->assertRedirectContains('/servers/');
$this->assertDatabaseHas('networks', [
it('queues a server heal operation for failed provisioning', function () {
$user = User::factory()->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' => 'net-99',
'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([
'status' => ServerStatus::PROVISIONING_FAILED,
'user' => 'keystone',
]);
$this->actingAs($user)
->get(route('servers.show', [$organisation, $server]))
->assertOk()
->assertInertia(fn (AssertableInertia $page) => $page
->component('servers/Show', false)
->where('server.status', ServerStatus::PROVISIONING_FAILED->value));
$this->actingAs($user)
->post(route('servers.heal', [$organisation, $server]))
->assertRedirect(route('servers.show', [$organisation, $server]));
$operation = $server->operations()->with('steps')->firstOrFail();
expect($operation->kind)->toBe(OperationKind::SERVER_PROVISION)
->and($operation->status)->toBe(OperationStatus::PENDING)
->and($operation->steps)->toHaveCount(3)
->and($operation->steps->pluck('name')->all())->toBe([
'Check server shell',
'Check Docker',
'Check Keystone directories',
]);
});
it('creates and removes server firewall rules from the server page', function () {
mock(InstallFirewallRule::class)->shouldReceive('execute')->andReturnNull();
$user = User::factory()->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(['user' => 'keystone']);
$this->actingAs($user)
->post(route('servers.firewall-rules.store', [$organisation, $server]), [
'type' => FirewallRuleType::ALLOW->value,
'ports' => '443/tcp',
'from' => '10.0.0.0/16',
])
->assertRedirect(route('servers.show', [$organisation, $server]));
$rule = $server->firewallRules()->firstOrFail();
expect($rule->type)->toBe(FirewallRuleType::ALLOW)
->and($rule->ports)->toBe('443/tcp')
->and($rule->from)->toBe('10.0.0.0/16');
$this->actingAs($user)
->get(route('servers.show', [$organisation, $server]))
->assertOk()
->assertInertia(fn (AssertableInertia $page) => $page
->component('servers/Show', false)
->has('server.firewall_rules', 1)
->where('server.firewall_rules.0.ports', '443/tcp'));
mock(UninstallFirewallRule::class)->shouldReceive('execute')->once();
$this->actingAs($user)
->delete(route('servers.firewall-rules.destroy', [$organisation, $server, $rule]))
->assertRedirect(route('servers.show', [$organisation, $server]));
expect(FirewallRule::query()->whereKey($rule->id)->exists())->toBeFalse();
});
it('validates server firewall rules', function () {
$user = User::factory()->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(['user' => 'keystone']);
$this->actingAs($user)
->post(route('servers.firewall-rules.store', [$organisation, $server]), [
'type' => 'open',
'ports' => '443 tcp',
'from' => 'not a source',
])
->assertInvalid(['type', 'ports', 'from']);
expect($server->firewallRules()->exists())->toBeFalse();
});