- 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
222 lines
7.5 KiB
PHP
222 lines
7.5 KiB
PHP
<?php
|
|
|
|
use App\Data\ServerProviders\CreatedServer;
|
|
use App\Enums\ProviderType;
|
|
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;
|
|
|
|
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();
|
|
$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,
|
|
]);
|
|
|
|
$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));
|
|
});
|
|
|
|
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', [
|
|
'provider_id' => $provider->id,
|
|
'external_id' => 'net-99',
|
|
'name' => 'keystone-global',
|
|
]);
|
|
});
|