wowowowowo
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user