Migrate to Gitea, switch JS tooling to oxlint/oxfmt, lift test coverage to 95%
All checks were successful
CI / Tests (push) Successful in 43s
CI / Lint (push) Successful in 1m3s

- 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
This commit is contained in:
2026-05-13 16:51:07 +01:00
parent aa680b25fd
commit 66f0ee9e50
238 changed files with 9243 additions and 1682 deletions

View File

@@ -0,0 +1,113 @@
<?php
use App\Actions\FirewallRules\InstallFirewallRule;
use App\Enums\FirewallRuleStatus;
use App\Enums\FirewallRuleType;
use App\Models\FirewallRule;
use App\Models\Network;
use App\Models\Organisation;
use App\Models\Provider;
use App\Models\Server;
use function Pest\Laravel\mock;
beforeEach(function () {
mock(InstallFirewallRule::class)->shouldReceive('execute')->andReturnNull();
});
function makeFirewallServer(): Server
{
$organisation = Organisation::factory()->create();
$provider = Provider::factory()->forOrganisation($organisation)->create();
$network = Network::create([
'organisation_id' => $organisation->id,
'provider_id' => $provider->id,
'name' => 'test-network',
'ip_range' => '10.0.0.0/24',
]);
return Server::factory()
->forOrganisation($organisation->id)
->forProvider($provider->id)
->forNetwork((string) $network->id)
->create();
}
it('builds a ufw allow command for an allow rule', function () {
$rule = new FirewallRule([
'type' => FirewallRuleType::ALLOW,
'ports' => '22',
]);
expect($rule->command())->toBe('ufw allow 22');
});
it('builds a ufw deny command for a deny rule', function () {
$rule = new FirewallRule([
'type' => FirewallRuleType::DENY,
'ports' => '80',
]);
expect($rule->command())->toBe('ufw deny 80');
});
it('includes the source address when from is set', function () {
$rule = new FirewallRule([
'type' => FirewallRuleType::ALLOW,
'ports' => '5432',
'from' => '10.0.0.0/24',
]);
expect($rule->command())->toBe('ufw allow from 10.0.0.0/24 to any port 5432');
});
it('prefixes the command with delete when requested', function () {
$rule = new FirewallRule([
'type' => FirewallRuleType::ALLOW,
'ports' => '22',
]);
expect($rule->command(delete: true))->toBe('ufw delete allow 22');
});
it('casts status and type to enums', function () {
$server = makeFirewallServer();
$rule = FirewallRule::create([
'server_id' => $server->id,
'type' => 'allow',
'ports' => '22',
'status' => FirewallRuleStatus::NOT_INSTALLED->value,
]);
$rule->refresh();
expect($rule->type)->toBe(FirewallRuleType::ALLOW);
expect($rule->status)->toBe(FirewallRuleStatus::NOT_INSTALLED);
});
it('belongs to a server', function () {
$server = makeFirewallServer();
$rule = FirewallRule::create([
'server_id' => $server->id,
'type' => 'allow',
'ports' => '22',
]);
expect($rule->server)->toBeInstanceOf(Server::class);
expect($rule->server->id)->toBe($server->id);
});
it('invokes the install action when a rule is created', function () {
$action = mock(InstallFirewallRule::class);
$action->shouldReceive('execute')->once();
$server = makeFirewallServer();
FirewallRule::create([
'server_id' => $server->id,
'type' => 'allow',
'ports' => '22',
]);
});