test: fix stale and flaky tests

- Update tests whose expectations no longer matched intended changes
  (GitHub App redirect, wildcard domains, container IDs, nightly
  install folders, localhost metadata, Sentinel restart on save)
- Seed the instance settings the 2FA profile test needs
- Run the global Pest hooks for all test folders and release the
  Laravel application after each test, so tests do not leak state
- Replace a class alias mock that broke later tests
- Generate factory names that always pass name validation; Faker names
  with apostrophes or short words made form tests fail at random
This commit is contained in:
Andras Bacsai
2026-10-06 13:01:49 +02:00
parent aa16cfb420
commit 41ed5b7a69
15 changed files with 88 additions and 33 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ class ApplicationFactory extends Factory
public function definition(): array
{
return [
'name' => fake()->unique()->name(),
'name' => fake()->unique()->words(2, true),
'destination_id' => 1,
'git_repository' => fake()->url(),
'git_branch' => fake()->word(),
+1 -1
View File
@@ -9,7 +9,7 @@ class EnvironmentFactory extends Factory
public function definition(): array
{
return [
'name' => fake()->unique()->word(),
'name' => fake()->unique()->words(2, true),
'project_id' => 1,
];
}
+1 -1
View File
@@ -9,7 +9,7 @@ class ProjectFactory extends Factory
public function definition(): array
{
return [
'name' => fake()->unique()->company(),
'name' => fake()->unique()->words(2, true),
'team_id' => 1,
];
}
+3 -2
View File
@@ -2,6 +2,7 @@
namespace Database\Factories;
use App\Models\StandaloneDocker;
use Illuminate\Database\Eloquent\Factories\Factory;
class ServiceFactory extends Factory
@@ -9,8 +10,8 @@ class ServiceFactory extends Factory
public function definition(): array
{
return [
'name' => fake()->unique()->word(),
'destination_type' => \App\Models\StandaloneDocker::class,
'name' => fake()->unique()->words(2, true),
'destination_type' => StandaloneDocker::class,
'destination_id' => 1,
'environment_id' => 1,
'docker_compose_raw' => 'version: "3"',
@@ -119,6 +119,10 @@ describe('GitHub Source Create Component', function () {
$githubApp = GithubApp::where('name', 'redirect-test')->first();
$component->assertRedirect(route('source.github.show', ['github_app_uuid' => $githubApp->uuid]));
$component->assertRedirect(route('source.github.show', [
'github_app_uuid' => $githubApp->uuid,
'previews' => 1,
'runners' => 0,
]));
});
});
+1 -1
View File
@@ -2316,7 +2316,7 @@ it('regenerates an application domain as a draft while preserving its url settin
->assertSet('editingDomainParts.path', '/api');
expect($component->get('editingDomainParts')['host'])
->toEndWith('.sslip.io')
->toEndWith('.wildcard.example.net')
->not->toBe('old.example.com')
->and($this->application->fresh()->fqdn)->toBe('http://old.example.com/api')
->and($this->application->fresh()->domain_port_overrides)->toMatchArray([
@@ -4,9 +4,17 @@ use App\Models\Server;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
uses(RefreshDatabase::class);
beforeEach(function () {
Server::flushIdentityMap();
// Changing is_metrics_enabled queues a Sentinel restart (ServerSetting::booted()); these tests
// only check the enabled state, so keep the sync queue from starting Sentinel.
Queue::fake();
});
it('treats Sentinel as enabled for regular servers even when the legacy flag and metrics are disabled', function () {
DB::table('instance_settings')->insert(['id' => 0]);
$user = User::factory()->create();
+8 -6
View File
@@ -164,12 +164,14 @@ it('collects localhost server details after a successful connection check', func
'connection_timeout' => 10,
]);
Process::fake([
'*' => Process::sequence([
Process::result(output: 'bin', exitCode: 0),
Process::result(output: localhostMetadataProcessOutput(), exitCode: 0),
]),
]);
// The connection check also runs SSH multiplexing commands, so answer by command, not by order.
Process::fake(function ($process) {
$command = is_array($process->command) ? implode(' ', $process->command) : $process->command;
return str_contains($command, '---PRETTY_NAME---')
? Process::result(output: localhostMetadataProcessOutput(), exitCode: 0)
: Process::result(output: 'bin', exitCode: 0);
});
Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
->call('checkLocalhostConnection');
@@ -1,6 +1,7 @@
<?php
use App\Livewire\Profile\Index as ProfileIndex;
use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider;
@@ -8,6 +9,10 @@ use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::query()->forceCreate(['id' => 0]);
});
/**
* @return array{input: string, copied: string}
*/
@@ -99,6 +99,25 @@ it('caches parsed local service templates by bundle mtime', function () {
});
it('keeps service template keys for service selection and docs links', function () {
$path = base_path('templates/'.config('constants.services.file_name'));
File::partialMock()
->shouldReceive('exists')
->with($path)
->andReturn(true)
->shouldReceive('get')
->with($path)
->andReturn(json_encode([
'denoKV' => [
'documentation' => 'https://coolify.io/docs',
'slogan' => 'Deno KV.',
'compose' => '',
'tags' => null,
'category' => 'database',
'logo' => 'images/default.webp',
'minversion' => '0.0.0',
],
]));
$services = collect((new Select)->loadServices()['services']);
$denoKv = $services->firstWhere('id', 'denoKV');
@@ -56,7 +56,7 @@ function fakeTerminalContainer(string $state, bool $hasShell = true, array $runn
if (str_contains($command, 'docker ps -a')) {
return Process::result(output: collect($runningContainers)
->map(fn (string $name) => json_encode(['Names' => $name, 'State' => 'running', 'Labels' => '']))
->map(fn (string $name) => json_encode(['ID' => md5($name), 'Names' => $name, 'State' => 'running', 'Labels' => '']))
->implode("\n"));
}
+7 -3
View File
@@ -70,19 +70,23 @@ function loadLazy(Testable $component): Testable
| Test Hooks
|--------------------------------------------------------------------------
|
| Global hooks that run before/after each test.
| Global hooks that run before/after each test. A bare beforeEach() in this file
| only applies to tests in this file, so register them with uses()->in().
|
*/
beforeEach(function () {
uses()->beforeEach(function () {
// Flush the Once memoization cache to ensure tests get fresh data
Once::flush();
// Flush the Server identity map cache to ensure tests get fresh data
Server::flushIdentityMap();
})->in('Feature', 'Unit', 'v4');
uses()->beforeEach(function () {
// Browser Livewire actions often dispatch events; disable broadcasting for host-side runs.
config(['broadcasting.default' => 'null']);
});
})->in('v4/Browser');
function loginAndSkipBoarding(string $email = 'test@example.com', string $password = 'password'): mixed
{
+16
View File
@@ -2,9 +2,25 @@
namespace Tests;
use Illuminate\Container\Container;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Facade;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
/**
* Laravel flushes the application on tear down but leaves it set as the facade
* application and container instance. Plain unit tests that mock facades or call
* helpers afterwards would then resolve bindings from that empty application.
*/
protected function tearDown(): void
{
parent::tearDown();
Facade::clearResolvedInstances();
Facade::setFacadeApplication(null);
Container::setInstance(null);
}
}
@@ -40,7 +40,7 @@ function installScriptFileMode(string $path): string
$installScripts = [
'stable install' => ['scripts/install.sh', ['source', 'ssh', 'images']],
'nightly install' => ['other/nightly/install.sh', ['source', 'ssh']],
'nightly install' => ['other/nightly/install.sh', ['source', 'ssh', 'images']],
];
$resourceDirectories = ['applications', 'databases', 'backups', 'services', 'proxy', 'sentinel'];
+11 -15
View File
@@ -2,24 +2,23 @@
use App\Events\RestoreJobFinished;
use App\Events\S3RestoreJobFinished;
use App\Models\Server;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Process;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
/**
* Tests for RestoreJobFinished and S3RestoreJobFinished events to ensure they handle
* null server scenarios gracefully (when server is deleted during operation).
*/
describe('RestoreJobFinished null server handling', function () {
afterEach(function () {
Mockery::close();
beforeEach(function () {
Process::fake();
});
it('handles null server gracefully in RestoreJobFinished event', function () {
// Mock Server::find to return null (server was deleted)
$mockServer = Mockery::mock('alias:'.Server::class);
$mockServer->shouldReceive('find')
->with(999)
->andReturn(null);
// Server 999 does not exist (it was deleted during the restore)
$data = [
'scriptPath' => '/tmp/script.sh',
'tmpPath' => '/tmp/backup.sql',
@@ -29,15 +28,11 @@ describe('RestoreJobFinished null server handling', function () {
// Should not throw an error when server is null
expect(fn () => new RestoreJobFinished($data))->not->toThrow(\Throwable::class);
Process::assertNothingRan();
});
it('handles null server gracefully in S3RestoreJobFinished event', function () {
// Mock Server::find to return null (server was deleted)
$mockServer = Mockery::mock('alias:'.Server::class);
$mockServer->shouldReceive('find')
->with(999)
->andReturn(null);
// Server 999 does not exist (it was deleted during the restore)
$data = [
'containerName' => 'helper-container',
'serverTmpPath' => '/tmp/downloaded.sql',
@@ -49,6 +44,7 @@ describe('RestoreJobFinished null server handling', function () {
// Should not throw an error when server is null
expect(fn () => new S3RestoreJobFinished($data))->not->toThrow(\Throwable::class);
Process::assertNothingRan();
});
it('handles empty serverId in RestoreJobFinished event', function () {