arrayable enums, only use base ubuntu images, server controller tests, server frontend page fixes
This commit is contained in:
14
app/Enums/Concerns/Arrayable.php
Normal file
14
app/Enums/Concerns/Arrayable.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Concerns;
|
||||
|
||||
trait Arrayable
|
||||
{
|
||||
public static function toArray(): array
|
||||
{
|
||||
$names = array_column(self::cases(), 'name');
|
||||
$values = array_column(self::cases(), 'value');
|
||||
|
||||
return array_combine($names, $values);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\Arrayable;
|
||||
|
||||
enum DeploymentStatus: string
|
||||
{
|
||||
use Arrayable;
|
||||
|
||||
case PENDING = 'pending';
|
||||
case IN_PROGRESS = 'in-progress';
|
||||
case COMPLETED = 'completed';
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\Arrayable;
|
||||
|
||||
enum FirewallRuleStatus: string
|
||||
{
|
||||
use Arrayable;
|
||||
|
||||
case NOT_APPLIED = 'not-applied';
|
||||
case APPLIED = 'applied';
|
||||
case FAILED = 'failed';
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\Arrayable;
|
||||
|
||||
enum OrganisationRole: string
|
||||
{
|
||||
use Arrayable;
|
||||
|
||||
case ADMIN = 'admin';
|
||||
case MEMBER = 'member';
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\Arrayable;
|
||||
|
||||
enum RepositoryType: string
|
||||
{
|
||||
use Arrayable;
|
||||
|
||||
case GIT = 'git';
|
||||
}
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\Arrayable;
|
||||
|
||||
enum ServerProvider: string
|
||||
{
|
||||
use Arrayable;
|
||||
|
||||
case HETZNER = 'hetzner';
|
||||
case DIGITAL_OCEAN = 'digital-ocean';
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\Arrayable;
|
||||
|
||||
enum ServerStatus: string
|
||||
{
|
||||
use Arrayable;
|
||||
|
||||
case WAITING_FOR_PROVIDER = 'waiting-for-provider';
|
||||
case PROVIDER_TIMEOUT = 'provider-timeout';
|
||||
case UNPROVISIONED = 'unprovisioned';
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\Arrayable;
|
||||
|
||||
enum ServiceCategory: string
|
||||
{
|
||||
use Arrayable;
|
||||
|
||||
case DATABASE = 'database';
|
||||
case APPLICATION = 'application';
|
||||
case GATEWAY = 'gateway';
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\Arrayable;
|
||||
|
||||
enum ServiceStatus: string
|
||||
{
|
||||
use Arrayable;
|
||||
|
||||
case NOT_INSTALLED = 'not-installed';
|
||||
case INSTALLING = 'installing';
|
||||
case RUNNING = 'running';
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\Arrayable;
|
||||
|
||||
enum ServiceType: string {
|
||||
use Arrayable;
|
||||
|
||||
case FRANKENPHP = 'frankenphp';
|
||||
case PHP_FPM = 'php-fpm';
|
||||
case POSTGRES = 'postgres';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
@@ -10,6 +11,9 @@ use Illuminate\Support\Str;
|
||||
|
||||
class Organisation extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function owner(): BelongsTo
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use App\Enums\ServerProvider;
|
||||
use App\Enums\ServerStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
@@ -11,6 +12,9 @@ use Spatie\Ssh\Ssh;
|
||||
|
||||
class Server extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
|
||||
@@ -104,6 +104,6 @@ class HetznerService extends ServerProviderService
|
||||
osFlavor: $image['os_flavor'],
|
||||
osVersion: $image['os_version'],
|
||||
);
|
||||
})->values();
|
||||
})->where('osVersion', '!=', 'unknown')->values();
|
||||
}
|
||||
}
|
||||
|
||||
29
database/factories/OrganisationFactory.php
Normal file
29
database/factories/OrganisationFactory.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Organisation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Organisation>
|
||||
*/
|
||||
class OrganisationFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$name = $this->faker->company();
|
||||
$owner = User::inRandomOrder()->first() ?: User::factory()->create();
|
||||
return [
|
||||
'name' => $this->faker->company(),
|
||||
'slug' => Organisation::createUniqueSlug($name),
|
||||
'owner_id' => $owner->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
44
database/factories/ServerFactory.php
Normal file
44
database/factories/ServerFactory.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\ServerProvider;
|
||||
use App\Enums\ServerStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Server>
|
||||
*/
|
||||
class ServerFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->word(),
|
||||
'provider' => ServerProvider::HETZNER,
|
||||
'provider_id' => $this->faker->uuid(),
|
||||
'ipv4' => $this->faker->ipv4(),
|
||||
'ipv6' => $this->faker->ipv6(),
|
||||
'provider_status' => '',
|
||||
'status' => $this->faker->randomElement(ServerStatus::toArray()),
|
||||
'region' => '28',
|
||||
'os' => 'ubuntu',
|
||||
'plan' => '26',
|
||||
'user' => 'keystone',
|
||||
];
|
||||
}
|
||||
|
||||
public function forOrganisation(int $organisationId): static
|
||||
{
|
||||
return $this->state(function (array $attributes) use ($organisationId) {
|
||||
return [
|
||||
'organisation_id' => $organisationId,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace Database\Seeders;
|
||||
use App\Enums\OrganisationRole;
|
||||
use App\Enums\RepositoryType;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
@@ -31,6 +32,10 @@ class DatabaseSeeder extends Seeder
|
||||
'owner_id' => 1,
|
||||
]);
|
||||
|
||||
$servers = Server::factory(40)->forOrganisation($organisation->id)->create();
|
||||
|
||||
$organisation->servers()->saveMany($servers);
|
||||
|
||||
$organisation->members()->attach($user, ['role' => OrganisationRole::ADMIN]);
|
||||
|
||||
$application = $organisation->applications()->create([
|
||||
|
||||
@@ -33,10 +33,21 @@ const serverProviders = [
|
||||
watch(
|
||||
() => form.provider,
|
||||
(provider) => {
|
||||
console.log(provider);
|
||||
loadLocations();
|
||||
},
|
||||
);
|
||||
|
||||
watch (
|
||||
() => form.location,
|
||||
(location) => {
|
||||
loadServerTypes();
|
||||
}
|
||||
)
|
||||
|
||||
loadLocations();
|
||||
loadServerTypes();
|
||||
|
||||
function loadLocations() {
|
||||
if (form.provider && !props.locations) {
|
||||
router.reload({
|
||||
only: ['locations'],
|
||||
@@ -46,12 +57,38 @@ if (form.provider && !props.locations) {
|
||||
async: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function loadServerTypes() {
|
||||
if (form.location && !props.serverTypes) {
|
||||
router.reload({
|
||||
only: ['serverTypes', 'images'],
|
||||
data: {
|
||||
provider: form.provider,
|
||||
location: form.location,
|
||||
},
|
||||
async: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Create Server" />
|
||||
|
||||
<AppLayout>
|
||||
<AppLayout
|
||||
:breadcrumbs="[
|
||||
{
|
||||
title: 'Servers',
|
||||
href: route('servers.index', {
|
||||
organisation: $page.props.organisation.id,
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: 'Create',
|
||||
}
|
||||
]"
|
||||
>
|
||||
<div class="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<RadioButton
|
||||
@@ -65,17 +102,11 @@ if (form.provider && !props.locations) {
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div v-if="form.provider" class="flex flex-wrap gap-2">
|
||||
<RadioButton
|
||||
v-for="location in locations"
|
||||
v-model="form.location"
|
||||
:value="location.id"
|
||||
:disabled="location.disabled"
|
||||
name="location"
|
||||
>
|
||||
<RadioButton v-for="location in locations" v-model="form.location" :value="location.id" :disabled="location.disabled" name="location">
|
||||
{{ location.city }}
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div v-if="form.location" class="grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
|
||||
<div v-if="form.location" class="grid gap-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<RadioButton
|
||||
v-for="serverType in serverTypes?.sort((a, b) => a.cores - b.cores) ?? []"
|
||||
v-model="form.server_type"
|
||||
@@ -84,21 +115,17 @@ if (form.provider && !props.locations) {
|
||||
name="server-type"
|
||||
>
|
||||
<h5 class="text-lg font-semibold uppercase tracking-tight">{{ serverType.name }}</h5>
|
||||
<p class="text-sm opacity-60">{{ serverType.cores }} cores • {{ serverType.memory }} GB RAM • {{ serverType.disk }} GB disk</p>
|
||||
<p class="text-sm opacity-60">
|
||||
{{ serverType.cores }} cores • {{ serverType.memory }} GB RAM • {{ serverType.disk }} GB disk
|
||||
</p>
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div v-if="form.server_type" class="flex gap-2 flex-wrap">
|
||||
<RadioButton
|
||||
v-for="image in images"
|
||||
v-model="form.image"
|
||||
:value="image.id"
|
||||
:disabled="image.disabled"
|
||||
name="image"
|
||||
>
|
||||
<div v-if="form.server_type" class="flex flex-wrap gap-2">
|
||||
<RadioButton v-for="image in images" v-model="form.image" :value="image.id" :disabled="image.disabled" name="image">
|
||||
<h5 class="text-lg font-semibold tracking-tight">{{ image.name }}</h5>
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div class="flex justify-end items-center">
|
||||
<div class="flex items-center justify-end">
|
||||
<Button @click="form.post(route('servers.store', { organisation: $page.props.organisation.id }))">Submit</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup>
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
servers: {
|
||||
type: [Array, null],
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
@@ -14,27 +16,42 @@ const props = defineProps({
|
||||
<template>
|
||||
<Head title="Dashboard" />
|
||||
|
||||
<AppLayout :breadcrumbs="[
|
||||
<AppLayout
|
||||
:breadcrumbs="[
|
||||
{
|
||||
title: 'Servers',
|
||||
href: route('servers.index', {
|
||||
organisation: $page.props.organisation.id,
|
||||
}),
|
||||
},
|
||||
]">
|
||||
]"
|
||||
>
|
||||
<div class="flex justify-between items-center gap-3 p-4">
|
||||
<h2 class="text-3xl font-bold tracking-tight">Servers</h2>
|
||||
<div>
|
||||
<Button :as="Link" :href="route('servers.create', {
|
||||
organisation: $page.props.organisation.id,
|
||||
})">Create</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 rounded-xl p-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="server in servers.data" :key="`server{$servers.id}`" class="w-full relative">
|
||||
<Card v-for="server in servers.data" :key="`server{$servers.id}`" class="relative w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ server.name }}</CardTitle>
|
||||
<CardDescription
|
||||
><span class="inline-block rounded-md bg-green-200 px-2 text-xs uppercase text-green-800">{{ server.status }}</span> •
|
||||
<CardDescription>
|
||||
<Badge :variant="server.status === 'active' ? 'success' : 'secondary'">{{ server.status.replace('-', ' ') }}</Badge> •
|
||||
{{ server.ipv4 || server.ipv6 }}</CardDescription
|
||||
>
|
||||
</CardHeader>
|
||||
<Link :href="route('servers.show', {
|
||||
<Link
|
||||
:href="
|
||||
route('servers.show', {
|
||||
organisation: $page.props.organisation.id,
|
||||
server: server.id,
|
||||
})" class="absolute inset-0"></Link>
|
||||
})
|
||||
"
|
||||
class="absolute inset-0"
|
||||
></Link>
|
||||
</Card>
|
||||
|
||||
<div>@todo pagination</div>
|
||||
|
||||
@@ -3,7 +3,9 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { DatabaseIcon, Layers2Icon } from 'lucide-vue-next';
|
||||
import { useCycleList, useInterval } from '@vueuse/core';
|
||||
import { DatabaseIcon, Layers2Icon, LoaderCircleIcon } from 'lucide-vue-next';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
server: {
|
||||
@@ -11,6 +13,20 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { state: provisionMessage, next } = useCycleList([
|
||||
'Provisioning your server...',
|
||||
'Updating dependencies...',
|
||||
'Tightening security...',
|
||||
'Installing packages...',
|
||||
'Configuring ssh...',
|
||||
'Installing docker...',
|
||||
]);
|
||||
const { counter, reset, pause, resume } = useInterval(5000, { controls: true });
|
||||
|
||||
watch(counter, () => {
|
||||
next();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -42,15 +58,18 @@ const props = defineProps({
|
||||
<div class="leading-none opacity-40">{{ server.ipv4 }} • {{ server.ipv6 }}</div>
|
||||
</div>
|
||||
|
||||
<template v-if="server.status === 'active'">
|
||||
<div>
|
||||
<h3 class="text-2xl font-semibold tracking-tight mb-3">Services</h3>
|
||||
<h3 class="mb-3 text-2xl font-semibold tracking-tight">Services</h3>
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="service in server.services" :key="service.id">
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<DatabaseIcon v-if="service.category === 'database'" class="size-5 opacity-50" />
|
||||
<CardTitle>{{ service.name }}</CardTitle>
|
||||
<Badge :variant="service.status === 'active' ? 'success' : 'secondary'">{{ service.status.replace('-', ' ') }}</Badge>
|
||||
<Badge :variant="service.status === 'active' ? 'success' : 'secondary'">{{
|
||||
service.status.replace('-', ' ')
|
||||
}}</Badge>
|
||||
</div>
|
||||
<CardDescription>
|
||||
<span class="capitalize">{{ service.type }}</span> {{ service.version }} •
|
||||
@@ -60,6 +79,27 @@ const props = defineProps({
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="server.status === 'provisioning'">
|
||||
<div class="flex items-center gap-4 py-6">
|
||||
<div class="flex-0 flex-shrink">
|
||||
<LoaderCircleIcon class="size-8 animate-spin" />
|
||||
</div>
|
||||
<div class="relative flex-grow">
|
||||
<Transition
|
||||
enter-active-class="transition duration-500 ease-in-out"
|
||||
enter-from-class="opacity-0 -translate-x-4"
|
||||
enter-to-class="opacity-100 translate-x-0"
|
||||
leave-active-class="transition absolute left-0 duration-500 ease-in-out"
|
||||
leave-from-class="opacity-100 translate-x-0"
|
||||
leave-to-class="opacity-0 translate-x-4"
|
||||
>
|
||||
<div :key="provisionMessage">{{ provisionMessage }}</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template> Something else </template>
|
||||
|
||||
<div v-if="$page.props.flash?.server_credentials" class="p-5">
|
||||
<div class="mb-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||
|
||||
104
tests/Feature/ServerControllerTest.php
Normal file
104
tests/Feature/ServerControllerTest.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\GetProviderService;
|
||||
use App\Data\ServerProviders\CreatedServer;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Testing\AssertableInertia;
|
||||
|
||||
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;
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
actingAs($this->user);
|
||||
});
|
||||
|
||||
test('index route displays servers for an organisation', function () {
|
||||
$organisation = Organisation::factory()->create();
|
||||
Server::factory()->count(2)->create(['organisation_id' => $organisation->id]);
|
||||
|
||||
$response = $this->get(route('servers.index', ['organisation' => $organisation->id]));
|
||||
$response->assertStatus(200);
|
||||
$response->assertInertia(fn(AssertableInertia $page) => $page
|
||||
->component('servers/Index'));
|
||||
});
|
||||
|
||||
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'));
|
||||
});
|
||||
|
||||
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->assertSessionHas('error', 'Invalid provider');
|
||||
$response->assertStatus(302); // redirect back
|
||||
});
|
||||
|
||||
test('store route creates a server with valid data', function () {
|
||||
$organisation = Organisation::factory()->create();
|
||||
|
||||
$this->mock(GetProviderService::class, function ($mock) {
|
||||
$providerMock = \Mockery::mock(\App\Services\ServerProviders\ServerProviderService::class);
|
||||
|
||||
$providerMock->shouldReceive('createServer')->andReturn(
|
||||
new CreatedServer(
|
||||
name: 'test-server',
|
||||
id: 123,
|
||||
ipv4: '127.0.0.1',
|
||||
ipv6: '::1',
|
||||
status: 'running',
|
||||
rootPassword: Str::random(16),
|
||||
)
|
||||
);
|
||||
|
||||
$mock->shouldReceive('execute')->andReturn($providerMock);
|
||||
});
|
||||
|
||||
$response = $this->post(route('servers.store', ['organisation' => $organisation->id]), [
|
||||
'provider' => 'hetzner',
|
||||
'server_type' => 'cx11',
|
||||
'location' => 'hel1',
|
||||
'image' => 'ubuntu-20.04',
|
||||
]);
|
||||
|
||||
$response->assertRedirectContains('/servers/');
|
||||
$this->assertDatabaseHas('servers', [
|
||||
'organisation_id' => $organisation->id,
|
||||
'provider' => 'hetzner',
|
||||
'region' => 'hel1',
|
||||
'os' => 'ubuntu-20.04',
|
||||
]);
|
||||
});
|
||||
|
||||
test('show route displays a single server', function () {
|
||||
$organisation = Organisation::factory()->create();
|
||||
$server = Server::factory()->create(['organisation_id' => $organisation->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'));
|
||||
});
|
||||
Reference in New Issue
Block a user