withoutVite(); config(['app.maintenance.driver' => 'file']); InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate( ['id' => 0], [ 'id' => 0, 'is_dns_validation_enabled' => false, ] )); $this->team = Team::factory()->create(); $this->user = User::factory()->create(); $this->team->members()->attach($this->user->id, ['role' => 'owner']); $this->actingAs($this->user); session(['currentTeam' => $this->team]); $keyId = DB::table('private_keys')->insertGetId([ 'uuid' => (string) Str::uuid(), 'name' => 'Test Key', 'private_key' => 'test-key', 'team_id' => $this->team->id, 'created_at' => now(), 'updated_at' => now(), ]); $this->server = Server::factory()->create([ 'team_id' => $this->team->id, 'private_key_id' => $keyId, 'ip' => '203.0.113.10', ]); $this->server->settings()->update([ 'is_reachable' => true, 'is_usable' => true, 'generate_exact_labels' => false, ]); StandaloneDocker::withoutEvents(function () { $this->destination = StandaloneDocker::firstOrCreate( ['server_id' => $this->server->id, 'network' => 'coolify'], ['uuid' => (string) Str::uuid(), 'name' => 'test-docker'] ); }); $this->project = Project::factory()->create(['team_id' => $this->team->id]); $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); $this->application = Application::factory()->create([ 'uuid' => (string) Str::uuid(), 'name' => 'Preview Port App', 'environment_id' => $this->environment->id, 'destination_id' => $this->destination->id, 'destination_type' => $this->destination->getMorphClass(), 'fqdn' => null, 'redirect' => 'both', 'build_pack' => 'nixpacks', 'ports_exposes' => '3000,8080', 'is_http_basic_auth_enabled' => false, ]); }); function createPreviewForPortTests(Application $application, int $pullRequestId, array $attributes = []): ApplicationPreview { return ApplicationPreview::create(array_merge([ 'application_id' => $application->id, 'pull_request_id' => $pullRequestId, 'pull_request_html_url' => "https://github.com/coollabsio/coolify/pull/{$pullRequestId}", ], $attributes)); } it('saves preview domain port overrides separately from the public FQDN', function () { $preview = createPreviewForPortTests($this->application, 101); $preview->update([ 'fqdn' => 'https://one-pr-101.example.com:3000,https://two-pr-101.example.com:8080', ]); $preview->refresh(); expect($preview->fqdn) ->toBe('https://one-pr-101.example.com,https://two-pr-101.example.com') ->and($preview->domain_port_overrides) ->toBe([ 'https://one-pr-101.example.com' => 3000, 'https://two-pr-101.example.com' => 8080, ]); }); it('retains an existing preview port override when saving a portless domain', function () { $preview = createPreviewForPortTests($this->application, 102, [ 'fqdn' => 'https://one-pr-102.example.com:3000', ]); $preview->update([ 'fqdn' => 'https://one-pr-102.example.com', ]); expect($preview->fresh()->fqdn) ->toBe('https://one-pr-102.example.com') ->and($preview->fresh()->domain_port_overrides) ->toBe(['https://one-pr-102.example.com' => 3000]); }); it('saves a preview port override from the add-domain form', function () { $preview = createPreviewForPortTests($this->application, 103); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->set('newDomainParts.host', 'preview.example.com') ->set('newDomainParts.port', '8080') ->call('addDomain') ->assertHasNoErrors() ->assertDispatched('success') ->assertSee('Internal port 8080') ->assertDontSee('https://preview.example.com:8080'); $preview->refresh(); expect($preview->fqdn) ->toBe('https://preview.example.com') ->and($preview->domain_port_overrides['https://preview.example.com'] ?? null) ->toBe(8080); }); it('retains different port overrides for two preview domains', function () { $preview = createPreviewForPortTests($this->application, 104); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->set('newDomainParts.host', 'one-preview.example.com') ->set('newDomainParts.port', '3000') ->call('addDomain') ->assertHasNoErrors() ->set('newDomainParts.host', 'two-preview.example.com') ->set('newDomainParts.port', '8080') ->call('addDomain') ->assertHasNoErrors(); $preview->refresh(); expect($preview->domain_port_overrides['https://one-preview.example.com'] ?? null)->toBe(3000) ->and($preview->domain_port_overrides['https://two-preview.example.com'] ?? null)->toBe(8080) ->and(explode(',', (string) $preview->fqdn)) ->toContain('https://one-preview.example.com') ->toContain('https://two-preview.example.com') ->not->toContain('https://one-preview.example.com:3000') ->not->toContain('https://two-preview.example.com:8080'); }); it('reopens preview domain edit with the saved port override', function () { $preview = createPreviewForPortTests($this->application, 105, [ 'fqdn' => 'https://preview.example.com:8080', ]); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->assertSet('domainRows.0.url', 'https://preview.example.com') ->assertSet('domainRows.0.internal_port', 8080) ->assertSet('domainRows.0.has_port_override', true) ->call('startEdit', 0) ->assertSet('editingDomainParts.port', '8080') ->assertSet('editingDomainParts.host', 'preview.example.com'); }); it('does not prefill the default internal port when editing a preview domain without an override', function () { $preview = createPreviewForPortTests($this->application, 106, [ 'fqdn' => 'https://preview.example.com', 'domain_port_overrides' => null, ]); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->assertSet('domainRows.0.internal_port', 3000) ->assertSet('domainRows.0.has_port_override', false) ->call('startEdit', 0) ->assertSet('editingDomainParts.port', ''); }); it('clears a preview domain port override and shows the default internal port', function () { $preview = createPreviewForPortTests($this->application, 107, [ 'fqdn' => 'https://one-preview.example.com:8080,https://two-preview.example.com:9090', ]); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->assertSee('Internal port 8080') ->call('startEdit', 0) ->assertSet('editingDomainParts.port', '8080') ->set('editingDomainParts.port', '') ->call('updateDomain') ->assertHasNoErrors() ->assertSee('Internal port 3000') ->assertSee('Internal port 9090') ->assertDontSee('Internal port 8080'); $preview->refresh(); expect($preview->fqdn) ->toContain('https://one-preview.example.com') ->and($preview->domain_port_overrides) ->not->toHaveKey('https://one-preview.example.com') ->toHaveKey('https://two-preview.example.com', 9090); }); it('prunes a preview domain port override when removing the domain', function () { $preview = createPreviewForPortTests($this->application, 108, [ 'fqdn' => 'https://one-preview.example.com:8080,https://two-preview.example.com:3000', ]); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->call('removeDomain', 0) ->assertDispatched('success'); $preview->refresh(); expect($preview->fqdn)->toBe('https://two-preview.example.com') ->and($preview->domain_port_overrides) ->toBe(['https://two-preview.example.com' => 3000]); }); it('shows an error badge when a preview domain has no internal port and ports exposes is empty', function () { $this->application->update([ 'ports_exposes' => null, ]); $preview = createPreviewForPortTests($this->application, 109, [ 'fqdn' => 'https://preview.example.com', 'domain_port_overrides' => null, ]); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->assertSee('No internal port') ->assertDontSee('Internal port'); }); it('rejects adding a preview domain whose portless URL is already configured', function () { $preview = createPreviewForPortTests($this->application, 110, [ 'fqdn' => 'https://preview.example.com:3000', ]); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->set('newDomainParts.host', 'preview.example.com') ->set('newDomainParts.port', '8080') ->call('addDomain') ->assertHasErrors('newDomainParts.host'); expect($preview->fresh()->fqdn)->toBe('https://preview.example.com') ->and($preview->fresh()->domain_port_overrides) ->toBe(['https://preview.example.com' => 3000]); }); it('saves compose preview domain port overrides per service without putting the port in the public URL', function () { $this->application->update([ 'build_pack' => 'dockercompose', 'docker_compose_raw' => "services:\n web:\n image: nginx:alpine\n expose: [8080]\n api:\n image: nginx:alpine\n expose: [3000]\n", 'docker_compose_domains' => null, ]); $preview = createPreviewForPortTests($this->application, 111); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->set('newDomainService', 'web') ->set('newDomainParts.host', 'web-preview.example.com') ->set('newDomainParts.port', '8080') ->call('addDomain') ->assertHasNoErrors() ->set('newDomainService', 'api') ->set('newDomainParts.host', 'api-preview.example.com') ->set('newDomainParts.port', '3000') ->call('addDomain') ->assertHasNoErrors(); $preview->refresh(); $composeDomains = json_decode($preview->docker_compose_domains, true); expect(data_get($composeDomains, 'web.domain'))->toBe('https://web-preview.example.com') ->and(data_get($composeDomains, 'api.domain'))->toBe('https://api-preview.example.com') ->and($preview->domain_port_overrides) ->toBe([ 'https://web-preview.example.com' => 8080, 'https://api-preview.example.com' => 3000, ]); }); it('copies the parent domain port override onto a generated preview domain', function () { $this->application->update([ 'fqdn' => 'https://app.example.com:8080', ]); $preview = createPreviewForPortTests($this->application, 112); $preview->generate_preview_fqdn(); $preview->refresh(); expect($preview->fqdn) ->toContain('112.') ->not->toContain(':8080') ->and($preview->domain_port_overrides) ->toHaveCount(1) ->and(array_values($preview->domain_port_overrides)) ->toBe([8080]); }); it('saves generated preview domains once in the application parser', function () { $parser = file_get_contents(base_path('bootstrap/helpers/parsers.php')); $previewGeneration = Str::of($parser) ->after('// If the domain is set, we need to generate the FQDNs for the preview') ->before('$defaultLabels = defaultLabels'); expect($previewGeneration->substrCount('$preview->save();'))->toBe(1) ->and((string) $previewGeneration)->toContain('$preview->fqdn = $fqdns->implode(\',\');'); }); it('keeps every generated preview domain port override in the legacy compose parser', function () { $this->application->update([ 'build_pack' => 'dockercompose', 'compose_parsing_version' => '2', 'docker_compose_raw' => <<<'YAML' services: frontend: image: nginx:alpine YAML, 'docker_compose_domains' => json_encode([ 'frontend' => ['domain' => 'https://one.example.com:3000,https://two.example.com:8080'], ]), ]); $preview = createPreviewForPortTests($this->application, 124); parseDockerComposeFile($this->application->fresh(), pull_request_id: 124, preview_id: $preview->id); $preview->refresh(); $previewDomains = explode(',', (string) $preview->fqdn); expect($previewDomains)->toHaveCount(2) ->and(collect($previewDomains) ->filter(fn (string $domain): bool => parse_url($domain, PHP_URL_PORT) !== null)) ->toBeEmpty() ->and($preview->domain_port_overrides)->toHaveCount(2) ->and(array_keys($preview->domain_port_overrides))->toBe($previewDomains) ->and(array_values($preview->domain_port_overrides))->toBe([3000, 8080]); }); it('finds the legacy compose preview by pull request when its id is unavailable', function (?int $previewId) { $this->application->update([ 'build_pack' => 'dockercompose', 'compose_parsing_version' => '2', 'docker_compose_raw' => "services:\n frontend:\n image: nginx:alpine\n", 'docker_compose_domains' => json_encode([ 'frontend' => ['domain' => 'https://app.example.com:3000'], ]), ]); $preview = createPreviewForPortTests($this->application, 125); parseDockerComposeFile($this->application->fresh(), pull_request_id: 125, preview_id: $previewId); expect($preview->fresh()->fqdn)->not->toBeNull(); })->with([ 'missing id' => null, 'stale id' => PHP_INT_MAX, ]); it('throws a controlled exception when the legacy compose preview does not exist', function () { $this->application->update([ 'build_pack' => 'dockercompose', 'compose_parsing_version' => '2', 'docker_compose_raw' => "services:\n frontend:\n image: nginx:alpine\n", 'docker_compose_domains' => json_encode([ 'frontend' => ['domain' => 'https://app.example.com:3000'], ]), ]); expect(fn () => parseDockerComposeFile( $this->application->fresh(), pull_request_id: 126, preview_id: PHP_INT_MAX, ))->toThrow(RuntimeException::class, 'Preview not found.'); }); it('preserves an existing preview port override in the legacy compose parser', function () { $this->application->update([ 'build_pack' => 'dockercompose', 'compose_parsing_version' => '2', 'docker_compose_raw' => <<<'YAML' services: frontend: image: nginx:alpine YAML, 'docker_compose_domains' => json_encode([ 'frontend' => ['domain' => 'https://frontend.example.com'], ]), ]); $preview = createPreviewForPortTests($this->application, 126, [ 'fqdn' => 'https://126.frontend.example.com', 'domain_port_overrides' => [ 'https://126.frontend.example.com' => 8080, ], ]); parseDockerComposeFile($this->application->fresh(), pull_request_id: 126, preview_id: $preview->id); expect($preview->fresh()->domain_port_overrides) ->toBe(['https://126.frontend.example.com' => 8080]); }); it('does not copy production domain port overrides onto preview proxy labels', function () { $this->application->update([ 'fqdn' => 'https://app.example.com', 'domain_port_overrides' => [ 'https://app.example.com' => 9090, ], ]); $preview = createPreviewForPortTests($this->application, 113, [ 'fqdn' => 'https://113.app.example.com', 'domain_port_overrides' => null, ]); $labels = collect(generateLabelsApplication($this->application->fresh(), $preview->fresh())); expect($labels) ->toContain('traefik.http.services.https-0-'.$this->application->uuid.'-pr-113.loadbalancer.server.port=3000') ->not->toContain('loadbalancer.server.port=9090'); }); it('routes portless preview domains to saved preview internal port overrides', function () { $preview = createPreviewForPortTests($this->application, 114, [ 'fqdn' => 'https://one-pr.example.com,https://two-pr.example.com', 'domain_port_overrides' => [ 'https://one-pr.example.com' => 3000, 'https://two-pr.example.com' => 8080, ], ]); $labels = collect(generateLabelsApplication($this->application->fresh(), $preview->fresh())); $uuid = $this->application->uuid.'-pr-114'; expect($labels) ->toContain('traefik.http.routers.https-0-'.$uuid.'.rule=Host(`one-pr.example.com`) && PathPrefix(`/`)') ->toContain('traefik.http.services.https-0-'.$uuid.'.loadbalancer.server.port=3000') ->toContain('traefik.http.routers.https-1-'.$uuid.'.rule=Host(`two-pr.example.com`) && PathPrefix(`/`)') ->toContain('traefik.http.services.https-1-'.$uuid.'.loadbalancer.server.port=8080') ->toContain('caddy_0.handle_path.0_reverse_proxy={{upstreams 3000}}') ->toContain('caddy_1.handle_path.1_reverse_proxy={{upstreams 8080}}') ->not->toContain('Host(`one-pr.example.com:3000`)') ->not->toContain('Host(`two-pr.example.com:8080`)'); }); it('uses the first ports_exposes value for a portless preview domain without an override', function () { $preview = createPreviewForPortTests($this->application, 115, [ 'fqdn' => 'https://plain-pr.example.com', 'domain_port_overrides' => null, ]); $labels = collect(generateLabelsApplication($this->application->fresh(), $preview->fresh())); expect($labels) ->toContain('traefik.http.services.https-0-'.$this->application->uuid.'-pr-115.loadbalancer.server.port=3000') ->toContain('caddy_0.handle_path.0_reverse_proxy={{upstreams 3000}}'); }); it('keeps routing a legacy port-bearing preview FQDN without an override map', function () { $preview = createPreviewForPortTests($this->application, 116, [ 'fqdn' => 'https://legacy-pr.example.com', ]); DB::table('application_previews')->where('id', $preview->id)->update([ 'fqdn' => 'https://legacy-pr.example.com:9090', 'domain_port_overrides' => null, ]); $labels = collect(generateLabelsApplication($this->application->fresh(), $preview->fresh())); expect($labels) ->toContain('traefik.http.routers.https-0-'.$this->application->uuid.'-pr-116.rule=Host(`legacy-pr.example.com`) && PathPrefix(`/`)') ->toContain('traefik.http.services.https-0-'.$this->application->uuid.'-pr-116.loadbalancer.server.port=9090') ->toContain('caddy_0.handle_path.0_reverse_proxy={{upstreams 9090}}'); }); it('passes preview domain port overrides into compose pull-request labels', function () { $this->application->update([ 'build_pack' => 'dockercompose', 'compose_parsing_version' => '3', 'docker_compose_raw' => <<<'YAML' services: frontend: image: myapp/frontend:latest YAML, 'fqdn' => null, 'docker_compose_domains' => json_encode([ 'frontend' => ['domain' => 'https://frontend.example.com'], ]), 'domain_port_overrides' => [ 'https://frontend.example.com' => 80, ], ]); $preview = createPreviewForPortTests($this->application, 117, [ 'docker_compose_domains' => json_encode([ 'frontend' => ['domain' => 'https://117.frontend.example.com'], ]), 'fqdn' => 'https://117.frontend.example.com', 'domain_port_overrides' => [ 'https://117.frontend.example.com' => 8080, ], ]); $parsedCompose = applicationParser($this->application->fresh(), 117, $preview->id); $labels = collect(data_get($parsedCompose, 'services.frontend-pr-117.labels')); expect($labels->contains(fn (string $label): bool => str_ends_with($label, '.loadbalancer.server.port=8080'))) ->toBeTrue() ->and($labels->contains(fn (string $label): bool => str_contains($label, 'reverse_proxy={{upstreams 8080}}'))) ->toBeTrue() ->and($labels->contains(fn (string $label): bool => str_contains($label, 'Host(`117.frontend.example.com`)'))) ->toBeTrue() ->and($labels->contains(fn (string $label): bool => str_ends_with($label, '.loadbalancer.server.port=80'))) ->toBeFalse(); }); it('leaves single-service compose preview ports to proxy discovery when a domain has no override', function () { $this->application->update([ 'build_pack' => 'dockercompose', 'compose_parsing_version' => '3', 'ports_exposes' => '4000,5000', 'docker_compose_raw' => <<<'YAML' services: frontend: image: myapp/frontend:latest YAML, 'fqdn' => null, 'domain_port_overrides' => null, 'docker_compose_domains' => json_encode([ 'frontend' => ['domain' => 'https://frontend.example.com'], ]), ]); $preview = createPreviewForPortTests($this->application, 118, [ 'docker_compose_domains' => json_encode([ 'frontend' => ['domain' => 'https://118.frontend.example.com'], ]), 'fqdn' => 'https://118.frontend.example.com', 'domain_port_overrides' => null, ]); $parsedCompose = applicationParser($this->application->fresh(), 118, $preview->id); $labels = collect(data_get($parsedCompose, 'services.frontend-pr-118.labels')); expect($labels->contains(fn (string $label): bool => str_contains($label, '.loadbalancer.server.port='))) ->toBeFalse() ->and($labels->contains(fn (string $label): bool => str_contains($label, 'reverse_proxy={{upstreams}}'))) ->toBeTrue(); }); it('shows a port warning when a preview domain uses a port that is not exposed or used by the application', function () { $preview = createPreviewForPortTests($this->application, 119); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->set('newDomainParts.host', 'preview.example.com') ->set('newDomainParts.port', '9090') ->call('addDomain') ->assertSet('showPortWarningModal', true) ->assertSet('unrecognizedPort', 9090) ->assertSee('Use a different port?') ->assertSee('9090'); expect($preview->fresh()->fqdn)->toBeNull(); }); it('does not warn when a preview domain port is listed in ports exposes', function () { $preview = createPreviewForPortTests($this->application, 120); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->set('newDomainParts.host', 'preview.example.com') ->set('newDomainParts.port', '8080') ->call('addDomain') ->assertSet('showPortWarningModal', false) ->assertDispatched('success'); }); it('does not warn when a preview domain port is already used by an application domain', function () { $this->application->update([ 'ports_exposes' => '3000', 'fqdn' => 'https://app.example.com:9090', ]); $preview = createPreviewForPortTests($this->application, 121); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->set('newDomainParts.host', 'preview.example.com') ->set('newDomainParts.port', '9090') ->call('addDomain') ->assertSet('showPortWarningModal', false) ->assertDispatched('success'); }); it('saves an unrecognized preview domain port after confirming the warning', function () { $preview = createPreviewForPortTests($this->application, 122); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->set('newDomainParts.host', 'preview.example.com') ->set('newDomainParts.port', '9090') ->call('addDomain') ->assertSet('showPortWarningModal', true) ->call('confirmUseUnknownPort') ->assertSet('showPortWarningModal', false) ->assertDispatched('success'); expect($preview->fresh()->fqdn)->toBe('https://preview.example.com') ->and($preview->fresh()->domain_port_overrides) ->toBe(['https://preview.example.com' => 9090]); }); it('cancels an unrecognized preview domain port without saving', function () { $preview = createPreviewForPortTests($this->application, 123); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->set('newDomainParts.host', 'preview.example.com') ->set('newDomainParts.port', '9090') ->call('addDomain') ->assertSet('showPortWarningModal', true) ->call('cancelUseUnknownPort') ->assertSet('showPortWarningModal', false); expect($preview->fresh()->fqdn)->toBeNull(); }); it('warns when editing a preview domain to an unrecognized port', function () { $preview = createPreviewForPortTests($this->application, 124, [ 'fqdn' => 'https://preview.example.com:3000', ]); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->call('startEdit', 0) ->set('editingDomainParts.port', '5555') ->call('updateDomain') ->assertSet('showPortWarningModal', true) ->assertSet('unrecognizedPort', 5555); expect($preview->fresh()->domain_port_overrides) ->toBe(['https://preview.example.com' => 3000]); }); it('does not warn when re-saving a preview domain with the same custom port', function () { $preview = createPreviewForPortTests($this->application, 125, [ 'fqdn' => 'https://preview.example.com:9090', ]); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->call('startEdit', 0) ->set('editingDomainParts.port', '9090') ->call('updateDomain') ->assertSet('showPortWarningModal', false) ->assertDispatched('success'); }); it('checks compose preview ports against the selected service when saving domains', function (string $action, int $port, bool $warn) { $this->application->update([ 'build_pack' => 'dockercompose', 'ports_exposes' => '3000', 'docker_compose_raw' => <<<'YAML' services: web: image: nginx:alpine expose: - 8080 - 8081 ports: - target: 8082 published: 18082 - "18083:8083" api: image: nginx:alpine expose: - 9090 YAML, 'docker_compose_domains' => json_encode([ 'web' => ['domain' => 'https://existing.example.com'], 'api' => ['domain' => 'https://api.example.com:9090'], ]), ]); $preview = createPreviewForPortTests($this->application, 130, [ 'docker_compose_domains' => json_encode(['web' => ['domain' => 'https://existing.example.com']]), 'fqdn' => 'https://existing.example.com', ]); $component = Livewire::test(PreviewDomains::class, ['preview' => $preview]); if ($action === 'add') { $component->set('newDomainService', 'web') ->set('newDomainParts.host', 'new.example.com') ->set('newDomainParts.port', (string) $port) ->call('addDomain'); $domain = 'https://new.example.com'; } else { $component->call('startEdit', 0) ->set('editingDomainParts.port', (string) $port) ->call('updateDomain'); $domain = 'https://existing.example.com'; } $component->assertHasNoErrors()->assertSet('showPortWarningModal', $warn); if ($warn) { expect($preview->fresh()->domain_port_overrides ?? [])->not->toHaveKey($domain); $component->assertSet('unrecognizedPort', $port) ->call('confirmUseUnknownPort') ->assertSet('showPortWarningModal', false); } $component->assertDispatched('success'); expect($preview->fresh()->domain_port_overrides[$domain] ?? null)->toBe($port); })->with(['add', 'edit'])->with([ 'first exposed port' => [8080, false], 'second exposed port' => [8081, false], 'long syntax target' => [8082, false], 'short syntax target' => [8083, false], 'global port' => [3000, true], 'another service port' => [9090, true], 'published host port' => [18082, true], ]); it('keeps an existing custom compose preview port without another warning', function () { $this->application->update([ 'build_pack' => 'dockercompose', 'ports_exposes' => '3000', 'docker_compose_raw' => "services:\n web:\n image: nginx:alpine\n expose: [8080]\n", 'docker_compose_domains' => json_encode(['web' => ['domain' => 'https://existing.example.com:7070']]), ]); $preview = createPreviewForPortTests($this->application, 131, [ 'docker_compose_domains' => json_encode(['web' => ['domain' => 'https://existing.example.com:7070']]), 'fqdn' => 'https://existing.example.com:7070', ]); Livewire::test(PreviewDomains::class, ['preview' => $preview]) ->call('startEdit', 0) ->assertSet('editingDomainParts.port', '7070') ->call('updateDomain') ->assertHasNoErrors() ->assertSet('showPortWarningModal', false) ->assertDispatched('success'); expect($preview->fresh()->domain_port_overrides['https://existing.example.com'] ?? null)->toBe(7070); });