diff --git a/database/factories/ApplicationFactory.php b/database/factories/ApplicationFactory.php index 188d32954b..2ffc9f07ce 100644 --- a/database/factories/ApplicationFactory.php +++ b/database/factories/ApplicationFactory.php @@ -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(), diff --git a/database/factories/EnvironmentFactory.php b/database/factories/EnvironmentFactory.php index 98959197d9..aa22c82517 100644 --- a/database/factories/EnvironmentFactory.php +++ b/database/factories/EnvironmentFactory.php @@ -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, ]; } diff --git a/database/factories/ProjectFactory.php b/database/factories/ProjectFactory.php index 0b2b72b8a2..535225f953 100644 --- a/database/factories/ProjectFactory.php +++ b/database/factories/ProjectFactory.php @@ -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, ]; } diff --git a/database/factories/ServiceFactory.php b/database/factories/ServiceFactory.php index 62c5f7cda5..391f94e8d2 100644 --- a/database/factories/ServiceFactory.php +++ b/database/factories/ServiceFactory.php @@ -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"', diff --git a/tests/Feature/Application/GithubSourceCreateTest.php b/tests/Feature/Application/GithubSourceCreateTest.php index a3eb13d547..cd566411e9 100644 --- a/tests/Feature/Application/GithubSourceCreateTest.php +++ b/tests/Feature/Application/GithubSourceCreateTest.php @@ -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, + ])); }); }); diff --git a/tests/Feature/ApplicationDomainsTest.php b/tests/Feature/ApplicationDomainsTest.php index ca1e115f24..5ffea4298f 100644 --- a/tests/Feature/ApplicationDomainsTest.php +++ b/tests/Feature/ApplicationDomainsTest.php @@ -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([ diff --git a/tests/Feature/Jobs/CheckAndStartSentinelJobTest.php b/tests/Feature/Jobs/CheckAndStartSentinelJobTest.php index 931b0b316a..56b1bfd541 100644 --- a/tests/Feature/Jobs/CheckAndStartSentinelJobTest.php +++ b/tests/Feature/Jobs/CheckAndStartSentinelJobTest.php @@ -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(); diff --git a/tests/Feature/LocalhostServerDetailsTest.php b/tests/Feature/LocalhostServerDetailsTest.php index f2fe735ec7..c106830184 100644 --- a/tests/Feature/LocalhostServerDetailsTest.php +++ b/tests/Feature/LocalhostServerDetailsTest.php @@ -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'); diff --git a/tests/Feature/ProfileTwoFactorSetupTest.php b/tests/Feature/ProfileTwoFactorSetupTest.php index f6a084d62a..0ae9c89ada 100644 --- a/tests/Feature/ProfileTwoFactorSetupTest.php +++ b/tests/Feature/ProfileTwoFactorSetupTest.php @@ -1,6 +1,7 @@ forceCreate(['id' => 0]); +}); + /** * @return array{input: string, copied: string} */ diff --git a/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php b/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php index 325b0caaff..7619b81e19 100644 --- a/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php +++ b/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php @@ -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'); diff --git a/tests/Feature/TerminalSessionStartFailureTest.php b/tests/Feature/TerminalSessionStartFailureTest.php index 0da9a68efb..80990fed1f 100644 --- a/tests/Feature/TerminalSessionStartFailureTest.php +++ b/tests/Feature/TerminalSessionStartFailureTest.php @@ -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")); } diff --git a/tests/Pest.php b/tests/Pest.php index 9707fad3ba..07cfd25a16 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -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 { diff --git a/tests/TestCase.php b/tests/TestCase.php index 2932d4a69d..fd9ea65e98 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -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); + } } diff --git a/tests/Unit/InstallScriptDirectoryPermissionsTest.php b/tests/Unit/InstallScriptDirectoryPermissionsTest.php index d9e079f365..812af5bde0 100644 --- a/tests/Unit/InstallScriptDirectoryPermissionsTest.php +++ b/tests/Unit/InstallScriptDirectoryPermissionsTest.php @@ -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']; diff --git a/tests/Unit/RestoreJobFinishedNullServerTest.php b/tests/Unit/RestoreJobFinishedNullServerTest.php index d3dfb2f9ad..b0d3d00d58 100644 --- a/tests/Unit/RestoreJobFinishedNullServerTest.php +++ b/tests/Unit/RestoreJobFinishedNullServerTest.php @@ -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 () {