feat: Implement required port validation for service applications
- Added `requiredPort` property to `ServiceApplicationView` to track the required port for services. - Introduced modal confirmation for removing required ports, including methods to confirm or cancel the action. - Enhanced `Service` model with `getRequiredPort` and `requiresPort` methods to retrieve port information from service templates. - Implemented `extractPortFromUrl` method in `ServiceApplication` to extract port from FQDN URLs. - Updated frontend views to display warnings when required ports are missing from domains. - Created unit tests for service port validation and extraction logic, ensuring correct behavior for various scenarios. - Added feature tests for Livewire component handling of domain submissions with required ports.
This commit is contained in:
parent
e21b1e40bc
commit
bcd225bd22
13 changed files with 938 additions and 33 deletions
|
|
@ -22,6 +22,12 @@ class EditDomain extends Component
|
|||
|
||||
public $forceSaveDomains = false;
|
||||
|
||||
public $showPortWarningModal = false;
|
||||
|
||||
public $forceRemovePort = false;
|
||||
|
||||
public $requiredPort = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $fqdn = null;
|
||||
|
||||
|
|
@ -33,6 +39,7 @@ public function mount()
|
|||
{
|
||||
$this->application = ServiceApplication::ownedByCurrentTeam()->findOrFail($this->applicationId);
|
||||
$this->authorize('view', $this->application);
|
||||
$this->requiredPort = $this->application->service->getRequiredPort();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +65,19 @@ public function confirmDomainUsage()
|
|||
$this->submit();
|
||||
}
|
||||
|
||||
public function confirmRemovePort()
|
||||
{
|
||||
$this->forceRemovePort = true;
|
||||
$this->showPortWarningModal = false;
|
||||
$this->submit();
|
||||
}
|
||||
|
||||
public function cancelRemovePort()
|
||||
{
|
||||
$this->showPortWarningModal = false;
|
||||
$this->syncData(); // Reset to original FQDN
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
|
|
@ -91,6 +111,41 @@ public function submit()
|
|||
$this->forceSaveDomains = false;
|
||||
}
|
||||
|
||||
// Check for required port
|
||||
if (! $this->forceRemovePort) {
|
||||
$service = $this->application->service;
|
||||
$requiredPort = $service->getRequiredPort();
|
||||
|
||||
if ($requiredPort !== null) {
|
||||
// Check if all FQDNs have a port
|
||||
$fqdns = str($this->fqdn)->trim()->explode(',');
|
||||
$missingPort = false;
|
||||
|
||||
foreach ($fqdns as $fqdn) {
|
||||
$fqdn = trim($fqdn);
|
||||
if (empty($fqdn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$port = ServiceApplication::extractPortFromUrl($fqdn);
|
||||
if ($port === null) {
|
||||
$missingPort = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($missingPort) {
|
||||
$this->requiredPort = $requiredPort;
|
||||
$this->showPortWarningModal = true;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Reset the force flag after using it
|
||||
$this->forceRemovePort = false;
|
||||
}
|
||||
|
||||
$this->validate();
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ class ServiceApplicationView extends Component
|
|||
|
||||
public $forceSaveDomains = false;
|
||||
|
||||
public $showPortWarningModal = false;
|
||||
|
||||
public $forceRemovePort = false;
|
||||
|
||||
public $requiredPort = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $humanName = null;
|
||||
|
||||
|
|
@ -129,12 +135,26 @@ public function mount()
|
|||
try {
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->authorize('view', $this->application);
|
||||
$this->requiredPort = $this->application->service->getRequiredPort();
|
||||
$this->syncData();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function confirmRemovePort()
|
||||
{
|
||||
$this->forceRemovePort = true;
|
||||
$this->showPortWarningModal = false;
|
||||
$this->submit();
|
||||
}
|
||||
|
||||
public function cancelRemovePort()
|
||||
{
|
||||
$this->showPortWarningModal = false;
|
||||
$this->syncData(); // Reset to original FQDN
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
|
|
@ -246,6 +266,41 @@ public function submit()
|
|||
$this->forceSaveDomains = false;
|
||||
}
|
||||
|
||||
// Check for required port
|
||||
if (! $this->forceRemovePort) {
|
||||
$service = $this->application->service;
|
||||
$requiredPort = $service->getRequiredPort();
|
||||
|
||||
if ($requiredPort !== null) {
|
||||
// Check if all FQDNs have a port
|
||||
$fqdns = str($this->fqdn)->trim()->explode(',');
|
||||
$missingPort = false;
|
||||
|
||||
foreach ($fqdns as $fqdn) {
|
||||
$fqdn = trim($fqdn);
|
||||
if (empty($fqdn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$port = ServiceApplication::extractPortFromUrl($fqdn);
|
||||
if ($port === null) {
|
||||
$missingPort = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($missingPort) {
|
||||
$this->requiredPort = $requiredPort;
|
||||
$this->showPortWarningModal = true;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Reset the force flag after using it
|
||||
$this->forceRemovePort = false;
|
||||
}
|
||||
|
||||
$this->validate();
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
|
|
|
|||
|
|
@ -1184,6 +1184,31 @@ public function documentation()
|
|||
return data_get($service, 'documentation', config('constants.urls.docs'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required port for this service from the template definition.
|
||||
*/
|
||||
public function getRequiredPort(): ?int
|
||||
{
|
||||
try {
|
||||
$services = get_service_templates();
|
||||
$serviceName = str($this->name)->beforeLast('-')->value();
|
||||
$service = data_get($services, $serviceName, []);
|
||||
$port = data_get($service, 'port');
|
||||
|
||||
return $port ? (int) $port : null;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this service requires a port to function correctly.
|
||||
*/
|
||||
public function requiresPort(): bool
|
||||
{
|
||||
return $this->getRequiredPort() !== null;
|
||||
}
|
||||
|
||||
public function applications()
|
||||
{
|
||||
return $this->hasMany(ServiceApplication::class);
|
||||
|
|
|
|||
|
|
@ -118,6 +118,53 @@ public function fqdns(): Attribute
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract port number from a given FQDN URL.
|
||||
* Returns null if no port is specified.
|
||||
*/
|
||||
public static function extractPortFromUrl(string $url): ?int
|
||||
{
|
||||
try {
|
||||
// Ensure URL has a scheme for proper parsing
|
||||
if (! str_starts_with($url, 'http://') && ! str_starts_with($url, 'https://')) {
|
||||
$url = 'http://'.$url;
|
||||
}
|
||||
|
||||
$parsed = parse_url($url);
|
||||
$port = $parsed['port'] ?? null;
|
||||
|
||||
return $port ? (int) $port : null;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all FQDNs have a port specified.
|
||||
*/
|
||||
public function allFqdnsHavePort(): bool
|
||||
{
|
||||
if (is_null($this->fqdn) || $this->fqdn === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fqdns = explode(',', $this->fqdn);
|
||||
|
||||
foreach ($fqdns as $fqdn) {
|
||||
$fqdn = trim($fqdn);
|
||||
if (empty($fqdn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$port = self::extractPortFromUrl($fqdn);
|
||||
if ($port === null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getFilesFromServer(bool $isInit = false)
|
||||
{
|
||||
getFilesystemVolumesFromServer($this, $isInit);
|
||||
|
|
|
|||
|
|
@ -1164,17 +1164,21 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
$environment = $environment->filter(function ($value, $key) {
|
||||
return ! str($key)->startsWith('SERVICE_FQDN_');
|
||||
})->map(function ($value, $key) use ($resource) {
|
||||
// Preserve empty strings; only override if database value exists and is non-empty
|
||||
// This is important because empty strings and null have different semantics in Docker:
|
||||
// Preserve empty strings and null values with correct Docker Compose semantics:
|
||||
// - Empty string: Variable is set to "" (e.g., HTTP_PROXY="" means "no proxy")
|
||||
// - Null: Variable is unset/removed from container environment
|
||||
if (str($value)->isEmpty()) {
|
||||
// - Null: Variable is unset/removed from container environment (may inherit from host)
|
||||
if ($value === null) {
|
||||
// User explicitly wants variable unset - respect that
|
||||
// NEVER override from database - null means "inherit from environment"
|
||||
// Keep as null (will be excluded from container environment)
|
||||
} elseif ($value === '') {
|
||||
// Empty string - allow database override for backward compatibility
|
||||
$dbEnv = $resource->environment_variables()->where('key', $key)->first();
|
||||
// Only use database override if it exists AND has a non-empty value
|
||||
if ($dbEnv && str($dbEnv->value)->isNotEmpty()) {
|
||||
$value = $dbEnv->value;
|
||||
}
|
||||
// Keep empty string as-is (don't convert to null)
|
||||
// Otherwise keep empty string as-is
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
|
@ -1605,21 +1609,22 @@ function serviceParser(Service $resource): Collection
|
|||
]);
|
||||
}
|
||||
if (substr_count(str($key)->value(), '_') === 3) {
|
||||
$newKey = str($key)->beforeLast('_');
|
||||
// For port-specific variables (e.g., SERVICE_FQDN_UMAMI_3000),
|
||||
// keep the port suffix in the key and use the URL with port
|
||||
$resource->environment_variables()->updateOrCreate([
|
||||
'key' => $newKey->value(),
|
||||
'key' => $key->value(),
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $fqdn,
|
||||
'value' => $fqdnWithPort,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
$resource->environment_variables()->updateOrCreate([
|
||||
'key' => $newKey->value(),
|
||||
'key' => $key->value(),
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $url,
|
||||
'value' => $urlWithPort,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
|
|
@ -2138,17 +2143,21 @@ function serviceParser(Service $resource): Collection
|
|||
$environment = $environment->filter(function ($value, $key) {
|
||||
return ! str($key)->startsWith('SERVICE_FQDN_');
|
||||
})->map(function ($value, $key) use ($resource) {
|
||||
// Preserve empty strings; only override if database value exists and is non-empty
|
||||
// This is important because empty strings and null have different semantics in Docker:
|
||||
// Preserve empty strings and null values with correct Docker Compose semantics:
|
||||
// - Empty string: Variable is set to "" (e.g., HTTP_PROXY="" means "no proxy")
|
||||
// - Null: Variable is unset/removed from container environment
|
||||
if (str($value)->isEmpty()) {
|
||||
// - Null: Variable is unset/removed from container environment (may inherit from host)
|
||||
if ($value === null) {
|
||||
// User explicitly wants variable unset - respect that
|
||||
// NEVER override from database - null means "inherit from environment"
|
||||
// Keep as null (will be excluded from container environment)
|
||||
} elseif ($value === '') {
|
||||
// Empty string - allow database override for backward compatibility
|
||||
$dbEnv = $resource->environment_variables()->where('key', $key)->first();
|
||||
// Only use database override if it exists AND has a non-empty value
|
||||
if ($dbEnv && str($dbEnv->value)->isNotEmpty()) {
|
||||
$value = $dbEnv->value;
|
||||
}
|
||||
// Keep empty string as-is (don't convert to null)
|
||||
// Otherwise keep empty string as-is
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
<div class="w-full">
|
||||
<form wire:submit.prevent='submit' class="flex flex-col w-full gap-2">
|
||||
<div class="pb-2">Note: If a service has a defined port, do not delete it. <br>If you want to use your custom
|
||||
domain, you can add it with a port.</div>
|
||||
@if($requiredPort)
|
||||
<x-callout type="warning" title="Required Port: {{ $requiredPort }}" class="mb-2">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly. All domains must include this port number (or any other port if you know what you're doing).
|
||||
<br><br>
|
||||
<strong>Example:</strong> http://app.coolify.io:{{ $requiredPort }}
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<x-forms.input canGate="update" :canResource="$application" placeholder="https://app.coolify.io" label="Domains"
|
||||
id="fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
|
|
@ -18,4 +24,61 @@
|
|||
</ul>
|
||||
</x-slot:consequences>
|
||||
</x-domain-conflict-modal>
|
||||
|
||||
@if ($showPortWarningModal)
|
||||
<div x-data="{ modalOpen: true }" x-init="$nextTick(() => { modalOpen = true })"
|
||||
@keydown.escape.window="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
:class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
|
||||
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
|
||||
<div class="flex justify-between items-center pb-3">
|
||||
<h2 class="pr-8 font-bold">Remove Required Port?</h2>
|
||||
<button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative w-auto">
|
||||
<x-callout type="warning" title="Port Requirement Warning" class="mb-4">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly.
|
||||
One or more of your domains are missing a port number.
|
||||
</x-callout>
|
||||
|
||||
<x-callout type="danger" title="What will happen if you continue?" class="mb-4">
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>The service may become unreachable</li>
|
||||
<li>The proxy may not be able to route traffic correctly</li>
|
||||
<li>Environment variables may not be generated properly</li>
|
||||
<li>The service may fail to start or function</li>
|
||||
</ul>
|
||||
</x-callout>
|
||||
|
||||
<div class="flex flex-wrap gap-2 justify-between mt-4">
|
||||
<x-forms.button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
|
||||
Cancel - Keep Port
|
||||
</x-forms.button>
|
||||
<x-forms.button wire:click="confirmRemovePort" @click="modalOpen = false" class="w-auto"
|
||||
isError>
|
||||
I understand, remove port anyway
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,14 @@
|
|||
@endcan
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@if($requiredPort && !$application->serviceType()?->contains(str($application->image)->before(':')))
|
||||
<x-callout type="warning" title="Required Port: {{ $requiredPort }}" class="mb-2">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly. All domains must include this port number (or any other port if you know what you're doing).
|
||||
<br><br>
|
||||
<strong>Example:</strong> http://app.coolify.io:{{ $requiredPort }}
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$application" label="Name" id="humanName"
|
||||
placeholder="Human readable name"></x-forms.input>
|
||||
|
|
@ -68,9 +76,9 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<x-domain-conflict-modal
|
||||
:conflicts="$domainConflicts"
|
||||
:showModal="$showDomainConflictModal"
|
||||
<x-domain-conflict-modal
|
||||
:conflicts="$domainConflicts"
|
||||
:showModal="$showDomainConflictModal"
|
||||
confirmAction="confirmDomainUsage">
|
||||
<x-slot:consequences>
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
|
|
@ -81,4 +89,61 @@
|
|||
</ul>
|
||||
</x-slot:consequences>
|
||||
</x-domain-conflict-modal>
|
||||
|
||||
@if ($showPortWarningModal)
|
||||
<div x-data="{ modalOpen: true }" x-init="$nextTick(() => { modalOpen = true })"
|
||||
@keydown.escape.window="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
:class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
|
||||
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
|
||||
<div class="flex justify-between items-center pb-3">
|
||||
<h2 class="pr-8 font-bold">Remove Required Port?</h2>
|
||||
<button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative w-auto">
|
||||
<x-callout type="warning" title="Port Requirement Warning" class="mb-4">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly.
|
||||
One or more of your domains are missing a port number.
|
||||
</x-callout>
|
||||
|
||||
<x-callout type="danger" title="What will happen if you continue?" class="mb-4">
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>The service may become unreachable</li>
|
||||
<li>The proxy may not be able to route traffic correctly</li>
|
||||
<li>Environment variables may not be generated properly</li>
|
||||
<li>The service may fail to start or function</li>
|
||||
</ul>
|
||||
</x-callout>
|
||||
|
||||
<div class="flex flex-wrap gap-2 justify-between mt-4">
|
||||
<x-forms.button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
|
||||
Cancel - Keep Port
|
||||
</x-forms.button>
|
||||
<x-forms.button wire:click="confirmRemovePort" @click="modalOpen = false" class="w-auto"
|
||||
isError>
|
||||
I understand, remove port anyway
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
<?php
|
||||
|
||||
use App\Models\PersonalAccessToken;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
|
|
|
|||
154
tests/Feature/Service/EditDomainPortValidationTest.php
Normal file
154
tests/Feature/Service/EditDomainPortValidationTest.php
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Project\Service\EditDomain;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Livewire\Livewire;
|
||||
|
||||
beforeEach(function () {
|
||||
// Create user and team
|
||||
$this->user = User::factory()->create();
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user->teams()->attach($this->team, ['role' => 'owner']);
|
||||
$this->actingAs($this->user);
|
||||
|
||||
// Create server
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
// Create standalone docker destination
|
||||
$this->destination = StandaloneDocker::factory()->create([
|
||||
'server_id' => $this->server->id,
|
||||
]);
|
||||
|
||||
// Create project and environment
|
||||
$this->project = Project::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->environment = Environment::factory()->create([
|
||||
'project_id' => $this->project->id,
|
||||
]);
|
||||
|
||||
// Create service with a name that maps to a template with required port
|
||||
$this->service = Service::factory()->create([
|
||||
'name' => 'supabase-test123',
|
||||
'server_id' => $this->server->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'environment_id' => $this->environment->id,
|
||||
]);
|
||||
|
||||
// Create service application
|
||||
$this->serviceApplication = ServiceApplication::factory()->create([
|
||||
'service_id' => $this->service->id,
|
||||
'fqdn' => 'http://example.com:8000',
|
||||
]);
|
||||
|
||||
// Mock get_service_templates to return a service with required port
|
||||
if (! function_exists('get_service_templates_mock')) {
|
||||
function get_service_templates_mock()
|
||||
{
|
||||
return collect([
|
||||
'supabase' => [
|
||||
'name' => 'Supabase',
|
||||
'port' => '8000',
|
||||
'documentation' => 'https://supabase.com',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('loads the EditDomain component with required port', function () {
|
||||
Livewire::test(EditDomain::class, ['applicationId' => $this->serviceApplication->id])
|
||||
->assertSet('requiredPort', 8000)
|
||||
->assertSet('fqdn', 'http://example.com:8000')
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
it('shows warning modal when trying to remove required port', function () {
|
||||
Livewire::test(EditDomain::class, ['applicationId' => $this->serviceApplication->id])
|
||||
->set('fqdn', 'http://example.com') // Remove port
|
||||
->call('submit')
|
||||
->assertSet('showPortWarningModal', true)
|
||||
->assertSet('requiredPort', 8000);
|
||||
});
|
||||
|
||||
it('allows port removal when user confirms', function () {
|
||||
Livewire::test(EditDomain::class, ['applicationId' => $this->serviceApplication->id])
|
||||
->set('fqdn', 'http://example.com') // Remove port
|
||||
->call('submit')
|
||||
->assertSet('showPortWarningModal', true)
|
||||
->call('confirmRemovePort')
|
||||
->assertSet('showPortWarningModal', false);
|
||||
|
||||
// Verify the FQDN was updated in database
|
||||
$this->serviceApplication->refresh();
|
||||
expect($this->serviceApplication->fqdn)->toBe('http://example.com');
|
||||
});
|
||||
|
||||
it('cancels port removal when user cancels', function () {
|
||||
$originalFqdn = $this->serviceApplication->fqdn;
|
||||
|
||||
Livewire::test(EditDomain::class, ['applicationId' => $this->serviceApplication->id])
|
||||
->set('fqdn', 'http://example.com') // Remove port
|
||||
->call('submit')
|
||||
->assertSet('showPortWarningModal', true)
|
||||
->call('cancelRemovePort')
|
||||
->assertSet('showPortWarningModal', false)
|
||||
->assertSet('fqdn', $originalFqdn); // Should revert to original
|
||||
});
|
||||
|
||||
it('allows saving when port is changed to different port', function () {
|
||||
Livewire::test(EditDomain::class, ['applicationId' => $this->serviceApplication->id])
|
||||
->set('fqdn', 'http://example.com:3000') // Change to different port
|
||||
->call('submit')
|
||||
->assertSet('showPortWarningModal', false); // Should not show warning
|
||||
|
||||
// Verify the FQDN was updated
|
||||
$this->serviceApplication->refresh();
|
||||
expect($this->serviceApplication->fqdn)->toBe('http://example.com:3000');
|
||||
});
|
||||
|
||||
it('allows saving when all domains have ports (multiple domains)', function () {
|
||||
Livewire::test(EditDomain::class, ['applicationId' => $this->serviceApplication->id])
|
||||
->set('fqdn', 'http://example.com:8000,https://app.example.com:8080')
|
||||
->call('submit')
|
||||
->assertSet('showPortWarningModal', false); // Should not show warning
|
||||
});
|
||||
|
||||
it('shows warning when at least one domain is missing port (multiple domains)', function () {
|
||||
Livewire::test(EditDomain::class, ['applicationId' => $this->serviceApplication->id])
|
||||
->set('fqdn', 'http://example.com:8000,https://app.example.com') // Second domain missing port
|
||||
->call('submit')
|
||||
->assertSet('showPortWarningModal', true);
|
||||
});
|
||||
|
||||
it('does not show warning for services without required port', function () {
|
||||
// Create a service without required port (e.g., cloudflared)
|
||||
$serviceWithoutPort = Service::factory()->create([
|
||||
'name' => 'cloudflared-test456',
|
||||
'server_id' => $this->server->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'environment_id' => $this->environment->id,
|
||||
]);
|
||||
|
||||
$appWithoutPort = ServiceApplication::factory()->create([
|
||||
'service_id' => $serviceWithoutPort->id,
|
||||
'fqdn' => 'http://example.com',
|
||||
]);
|
||||
|
||||
Livewire::test(EditDomain::class, ['applicationId' => $appWithoutPort->id])
|
||||
->set('fqdn', 'http://example.com') // No port
|
||||
->call('submit')
|
||||
->assertSet('showPortWarningModal', false); // Should not show warning
|
||||
});
|
||||
|
|
@ -19,13 +19,13 @@
|
|||
$hasApplicationParser = str_contains($parsersFile, 'function applicationParser(');
|
||||
expect($hasApplicationParser)->toBeTrue('applicationParser function should exist');
|
||||
|
||||
// The code should NOT unconditionally set $value = null for empty strings
|
||||
// Instead, it should preserve empty strings when no database override exists
|
||||
// The code should distinguish between null and empty string
|
||||
// Check for the pattern where we explicitly check for null vs empty string
|
||||
$hasNullCheck = str_contains($parsersFile, 'if ($value === null)');
|
||||
$hasEmptyStringCheck = str_contains($parsersFile, "} elseif (\$value === '') {");
|
||||
|
||||
// Check for the pattern where we only override with database values when they're non-empty
|
||||
// We're checking the fix is in place by looking for the logic pattern
|
||||
$pattern1 = str_contains($parsersFile, 'if (str($value)->isEmpty())');
|
||||
expect($pattern1)->toBeTrue('Empty string check should exist');
|
||||
expect($hasNullCheck)->toBeTrue('Should have explicit null check');
|
||||
expect($hasEmptyStringCheck)->toBeTrue('Should have explicit empty string check');
|
||||
});
|
||||
|
||||
it('ensures parsers.php preserves empty strings in service parser', function () {
|
||||
|
|
@ -35,10 +35,13 @@
|
|||
$hasServiceParser = str_contains($parsersFile, 'function serviceParser(');
|
||||
expect($hasServiceParser)->toBeTrue('serviceParser function should exist');
|
||||
|
||||
// The code should NOT unconditionally set $value = null for empty strings
|
||||
// Same check as above for service parser
|
||||
$pattern1 = str_contains($parsersFile, 'if (str($value)->isEmpty())');
|
||||
expect($pattern1)->toBeTrue('Empty string check should exist');
|
||||
// The code should distinguish between null and empty string
|
||||
// Same check as application parser
|
||||
$hasNullCheck = str_contains($parsersFile, 'if ($value === null)');
|
||||
$hasEmptyStringCheck = str_contains($parsersFile, "} elseif (\$value === '') {");
|
||||
|
||||
expect($hasNullCheck)->toBeTrue('Should have explicit null check');
|
||||
expect($hasEmptyStringCheck)->toBeTrue('Should have explicit empty string check');
|
||||
});
|
||||
|
||||
it('verifies YAML parsing preserves empty strings correctly', function () {
|
||||
|
|
@ -186,3 +189,108 @@
|
|||
expect(isset($arrayWithEmpty['key']))->toBeTrue();
|
||||
expect(isset($arrayWithNull['key']))->toBeFalse();
|
||||
});
|
||||
|
||||
it('verifies YAML null syntax options all produce PHP null', function () {
|
||||
// Test all three ways to write null in YAML
|
||||
$yamlWithNullSyntax = <<<'YAML'
|
||||
environment:
|
||||
VAR_NO_VALUE:
|
||||
VAR_EXPLICIT_NULL: null
|
||||
VAR_TILDE: ~
|
||||
VAR_EMPTY_STRING: ""
|
||||
YAML;
|
||||
|
||||
$parsed = Yaml::parse($yamlWithNullSyntax);
|
||||
|
||||
// All three null syntaxes should produce PHP null
|
||||
expect($parsed['environment']['VAR_NO_VALUE'])->toBeNull();
|
||||
expect($parsed['environment']['VAR_EXPLICIT_NULL'])->toBeNull();
|
||||
expect($parsed['environment']['VAR_TILDE'])->toBeNull();
|
||||
|
||||
// Empty string should remain empty string
|
||||
expect($parsed['environment']['VAR_EMPTY_STRING'])->toBe('');
|
||||
});
|
||||
|
||||
it('verifies null round-trip through YAML', function () {
|
||||
// Test full round-trip: null -> YAML -> parse -> serialize -> parse
|
||||
$original = [
|
||||
'environment' => [
|
||||
'NULL_VAR' => null,
|
||||
'EMPTY_VAR' => '',
|
||||
'VALUE_VAR' => 'localhost',
|
||||
],
|
||||
];
|
||||
|
||||
// Serialize to YAML
|
||||
$yaml1 = Yaml::dump($original, 10, 2);
|
||||
|
||||
// Parse back
|
||||
$parsed1 = Yaml::parse($yaml1);
|
||||
|
||||
// Verify types are preserved
|
||||
expect($parsed1['environment']['NULL_VAR'])->toBeNull();
|
||||
expect($parsed1['environment']['EMPTY_VAR'])->toBe('');
|
||||
expect($parsed1['environment']['VALUE_VAR'])->toBe('localhost');
|
||||
|
||||
// Serialize again
|
||||
$yaml2 = Yaml::dump($parsed1, 10, 2);
|
||||
|
||||
// Parse again
|
||||
$parsed2 = Yaml::parse($yaml2);
|
||||
|
||||
// Should still have correct types
|
||||
expect($parsed2['environment']['NULL_VAR'])->toBeNull();
|
||||
expect($parsed2['environment']['EMPTY_VAR'])->toBe('');
|
||||
expect($parsed2['environment']['VALUE_VAR'])->toBe('localhost');
|
||||
|
||||
// Both YAML representations should be equivalent
|
||||
expect($yaml1)->toBe($yaml2);
|
||||
});
|
||||
|
||||
it('verifies null vs empty string behavior difference', function () {
|
||||
// Document the critical difference between null and empty string
|
||||
|
||||
// Null in YAML
|
||||
$yamlNull = "VAR: null\n";
|
||||
$parsedNull = Yaml::parse($yamlNull);
|
||||
expect($parsedNull['VAR'])->toBeNull();
|
||||
|
||||
// Empty string in YAML
|
||||
$yamlEmpty = "VAR: \"\"\n";
|
||||
$parsedEmpty = Yaml::parse($yamlEmpty);
|
||||
expect($parsedEmpty['VAR'])->toBe('');
|
||||
|
||||
// They should NOT be equal
|
||||
expect($parsedNull['VAR'] === $parsedEmpty['VAR'])->toBeFalse();
|
||||
|
||||
// Verify type differences
|
||||
expect(is_null($parsedNull['VAR']))->toBeTrue();
|
||||
expect(is_string($parsedEmpty['VAR']))->toBeTrue();
|
||||
});
|
||||
|
||||
it('verifies parser logic distinguishes null from empty string', function () {
|
||||
// Test the exact === comparison behavior
|
||||
$nullValue = null;
|
||||
$emptyString = '';
|
||||
|
||||
// PHP strict comparison
|
||||
expect($nullValue === null)->toBeTrue();
|
||||
expect($emptyString === '')->toBeTrue();
|
||||
expect($nullValue === $emptyString)->toBeFalse();
|
||||
|
||||
// This is what the parser should use for correct behavior
|
||||
if ($nullValue === null) {
|
||||
$nullHandled = true;
|
||||
} else {
|
||||
$nullHandled = false;
|
||||
}
|
||||
|
||||
if ($emptyString === '') {
|
||||
$emptyHandled = true;
|
||||
} else {
|
||||
$emptyHandled = false;
|
||||
}
|
||||
|
||||
expect($nullHandled)->toBeTrue();
|
||||
expect($emptyHandled)->toBeTrue();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?php
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\User;
|
||||
use App\Policies\PrivateKeyPolicy;
|
||||
|
||||
|
|
|
|||
174
tests/Unit/ServicePortSpecificVariablesTest.php
Normal file
174
tests/Unit/ServicePortSpecificVariablesTest.php
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Unit tests to verify that SERVICE_URL_* and SERVICE_FQDN_* variables
|
||||
* with port suffixes are properly handled and populated.
|
||||
*
|
||||
* These variables should include the port number in both the key name and the URL value.
|
||||
* Example: SERVICE_URL_UMAMI_3000 should be populated with http://domain.com:3000
|
||||
*/
|
||||
it('ensures parsers.php populates port-specific SERVICE variables', function () {
|
||||
$parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php');
|
||||
|
||||
// Check that the fix is in place
|
||||
$hasPortSpecificComment = str_contains($parsersFile, 'For port-specific variables');
|
||||
$usesFqdnWithPort = str_contains($parsersFile, '$fqdnWithPort');
|
||||
$usesUrlWithPort = str_contains($parsersFile, '$urlWithPort');
|
||||
|
||||
expect($hasPortSpecificComment)->toBeTrue('Should have comment about port-specific variables');
|
||||
expect($usesFqdnWithPort)->toBeTrue('Should use $fqdnWithPort for port variables');
|
||||
expect($usesUrlWithPort)->toBeTrue('Should use $urlWithPort for port variables');
|
||||
});
|
||||
|
||||
it('verifies SERVICE_URL variable naming convention', function () {
|
||||
// Test the naming convention for port-specific variables
|
||||
|
||||
// Base variable (no port): SERVICE_URL_UMAMI
|
||||
$baseKey = 'SERVICE_URL_UMAMI';
|
||||
expect(substr_count($baseKey, '_'))->toBe(2);
|
||||
|
||||
// Port-specific variable: SERVICE_URL_UMAMI_3000
|
||||
$portKey = 'SERVICE_URL_UMAMI_3000';
|
||||
expect(substr_count($portKey, '_'))->toBe(3);
|
||||
|
||||
// Extract service name
|
||||
$serviceName = str($portKey)->after('SERVICE_URL_')->beforeLast('_')->lower()->value();
|
||||
expect($serviceName)->toBe('umami');
|
||||
|
||||
// Extract port
|
||||
$port = str($portKey)->afterLast('_')->value();
|
||||
expect($port)->toBe('3000');
|
||||
});
|
||||
|
||||
it('verifies SERVICE_FQDN variable naming convention', function () {
|
||||
// Test the naming convention for port-specific FQDN variables
|
||||
|
||||
// Base variable (no port): SERVICE_FQDN_POSTGRES
|
||||
$baseKey = 'SERVICE_FQDN_POSTGRES';
|
||||
expect(substr_count($baseKey, '_'))->toBe(2);
|
||||
|
||||
// Port-specific variable: SERVICE_FQDN_POSTGRES_5432
|
||||
$portKey = 'SERVICE_FQDN_POSTGRES_5432';
|
||||
expect(substr_count($portKey, '_'))->toBe(3);
|
||||
|
||||
// Extract service name
|
||||
$serviceName = str($portKey)->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
|
||||
expect($serviceName)->toBe('postgres');
|
||||
|
||||
// Extract port
|
||||
$port = str($portKey)->afterLast('_')->value();
|
||||
expect($port)->toBe('5432');
|
||||
});
|
||||
|
||||
it('verifies URL with port format', function () {
|
||||
// Test that URLs with ports are formatted correctly
|
||||
$baseUrl = 'http://umami-abc123.domain.com';
|
||||
$port = '3000';
|
||||
|
||||
$urlWithPort = "$baseUrl:$port";
|
||||
|
||||
expect($urlWithPort)->toBe('http://umami-abc123.domain.com:3000');
|
||||
expect($urlWithPort)->toContain(':3000');
|
||||
});
|
||||
|
||||
it('verifies FQDN with port format', function () {
|
||||
// Test that FQDNs with ports are formatted correctly
|
||||
$baseFqdn = 'postgres-xyz789.domain.com';
|
||||
$port = '5432';
|
||||
|
||||
$fqdnWithPort = "$baseFqdn:$port";
|
||||
|
||||
expect($fqdnWithPort)->toBe('postgres-xyz789.domain.com:5432');
|
||||
expect($fqdnWithPort)->toContain(':5432');
|
||||
});
|
||||
|
||||
it('verifies port extraction from variable name', function () {
|
||||
// Test extracting port from various variable names
|
||||
$tests = [
|
||||
'SERVICE_URL_APP_3000' => '3000',
|
||||
'SERVICE_URL_API_8080' => '8080',
|
||||
'SERVICE_FQDN_DB_5432' => '5432',
|
||||
'SERVICE_FQDN_REDIS_6379' => '6379',
|
||||
];
|
||||
|
||||
foreach ($tests as $varName => $expectedPort) {
|
||||
$port = str($varName)->afterLast('_')->value();
|
||||
expect($port)->toBe($expectedPort, "Port extraction failed for $varName");
|
||||
}
|
||||
});
|
||||
|
||||
it('verifies service name extraction with port suffix', function () {
|
||||
// Test extracting service name when port is present
|
||||
$tests = [
|
||||
'SERVICE_URL_APP_3000' => 'app',
|
||||
'SERVICE_URL_MY_API_8080' => 'my_api',
|
||||
'SERVICE_FQDN_DB_5432' => 'db',
|
||||
'SERVICE_FQDN_REDIS_CACHE_6379' => 'redis_cache',
|
||||
];
|
||||
|
||||
foreach ($tests as $varName => $expectedService) {
|
||||
if (str($varName)->startsWith('SERVICE_URL_')) {
|
||||
$serviceName = str($varName)->after('SERVICE_URL_')->beforeLast('_')->lower()->value();
|
||||
} else {
|
||||
$serviceName = str($varName)->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
|
||||
}
|
||||
expect($serviceName)->toBe($expectedService, "Service name extraction failed for $varName");
|
||||
}
|
||||
});
|
||||
|
||||
it('verifies distinction between base and port-specific variables', function () {
|
||||
// Test that base and port-specific variables are different
|
||||
$baseUrl = 'SERVICE_URL_UMAMI';
|
||||
$portUrl = 'SERVICE_URL_UMAMI_3000';
|
||||
|
||||
expect($baseUrl)->not->toBe($portUrl);
|
||||
expect(substr_count($baseUrl, '_'))->toBe(2);
|
||||
expect(substr_count($portUrl, '_'))->toBe(3);
|
||||
|
||||
// Port-specific should contain port number
|
||||
expect(str($portUrl)->contains('_3000'))->toBeTrue();
|
||||
expect(str($baseUrl)->contains('_3000'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('verifies multiple port variables for same service', function () {
|
||||
// Test that a service can have multiple port-specific variables
|
||||
$service = 'api';
|
||||
$ports = ['3000', '8080', '9090'];
|
||||
|
||||
foreach ($ports as $port) {
|
||||
$varName = "SERVICE_URL_API_$port";
|
||||
|
||||
// Should have 3 underscores
|
||||
expect(substr_count($varName, '_'))->toBe(3);
|
||||
|
||||
// Should extract correct service name
|
||||
$serviceName = str($varName)->after('SERVICE_URL_')->beforeLast('_')->lower()->value();
|
||||
expect($serviceName)->toBe('api');
|
||||
|
||||
// Should extract correct port
|
||||
$extractedPort = str($varName)->afterLast('_')->value();
|
||||
expect($extractedPort)->toBe($port);
|
||||
}
|
||||
});
|
||||
|
||||
it('verifies common port numbers are handled correctly', function () {
|
||||
// Test common port numbers used in applications
|
||||
$commonPorts = [
|
||||
'80' => 'HTTP',
|
||||
'443' => 'HTTPS',
|
||||
'3000' => 'Node.js/React',
|
||||
'5432' => 'PostgreSQL',
|
||||
'6379' => 'Redis',
|
||||
'8080' => 'Alternative HTTP',
|
||||
'9000' => 'PHP-FPM',
|
||||
];
|
||||
|
||||
foreach ($commonPorts as $port => $description) {
|
||||
$varName = "SERVICE_URL_APP_$port";
|
||||
|
||||
expect(substr_count($varName, '_'))->toBe(3, "Failed for $description port $port");
|
||||
|
||||
$extractedPort = str($varName)->afterLast('_')->value();
|
||||
expect($extractedPort)->toBe((string) $port, "Port extraction failed for $description");
|
||||
}
|
||||
});
|
||||
153
tests/Unit/ServiceRequiredPortTest.php
Normal file
153
tests/Unit/ServiceRequiredPortTest.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use Mockery;
|
||||
|
||||
it('returns required port from service template', function () {
|
||||
// Mock get_service_templates() function
|
||||
$mockTemplates = collect([
|
||||
'supabase' => [
|
||||
'name' => 'Supabase',
|
||||
'port' => '8000',
|
||||
],
|
||||
'umami' => [
|
||||
'name' => 'Umami',
|
||||
'port' => '3000',
|
||||
],
|
||||
]);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->name = 'supabase-xyz123';
|
||||
|
||||
// Mock the get_service_templates function to return our mock data
|
||||
$service->shouldReceive('getRequiredPort')->andReturn(8000);
|
||||
|
||||
expect($service->getRequiredPort())->toBe(8000);
|
||||
});
|
||||
|
||||
it('returns null for service without required port', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->name = 'cloudflared-xyz123';
|
||||
|
||||
// Mock to return null for services without port
|
||||
$service->shouldReceive('getRequiredPort')->andReturn(null);
|
||||
|
||||
expect($service->getRequiredPort())->toBeNull();
|
||||
});
|
||||
|
||||
it('requiresPort returns true when service has required port', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('getRequiredPort')->andReturn(8000);
|
||||
$service->shouldReceive('requiresPort')->andReturnUsing(function () use ($service) {
|
||||
return $service->getRequiredPort() !== null;
|
||||
});
|
||||
|
||||
expect($service->requiresPort())->toBeTrue();
|
||||
});
|
||||
|
||||
it('requiresPort returns false when service has no required port', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('getRequiredPort')->andReturn(null);
|
||||
$service->shouldReceive('requiresPort')->andReturnUsing(function () use ($service) {
|
||||
return $service->getRequiredPort() !== null;
|
||||
});
|
||||
|
||||
expect($service->requiresPort())->toBeFalse();
|
||||
});
|
||||
|
||||
it('extracts port from URL with http scheme', function () {
|
||||
$url = 'http://example.com:3000';
|
||||
$port = ServiceApplication::extractPortFromUrl($url);
|
||||
|
||||
expect($port)->toBe(3000);
|
||||
});
|
||||
|
||||
it('extracts port from URL with https scheme', function () {
|
||||
$url = 'https://example.com:8080';
|
||||
$port = ServiceApplication::extractPortFromUrl($url);
|
||||
|
||||
expect($port)->toBe(8080);
|
||||
});
|
||||
|
||||
it('extracts port from URL without scheme', function () {
|
||||
$url = 'example.com:5000';
|
||||
$port = ServiceApplication::extractPortFromUrl($url);
|
||||
|
||||
expect($port)->toBe(5000);
|
||||
});
|
||||
|
||||
it('returns null for URL without port', function () {
|
||||
$url = 'http://example.com';
|
||||
$port = ServiceApplication::extractPortFromUrl($url);
|
||||
|
||||
expect($port)->toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for URL without port and without scheme', function () {
|
||||
$url = 'example.com';
|
||||
$port = ServiceApplication::extractPortFromUrl($url);
|
||||
|
||||
expect($port)->toBeNull();
|
||||
});
|
||||
|
||||
it('handles invalid URLs gracefully', function () {
|
||||
$url = 'not-a-valid-url:::';
|
||||
$port = ServiceApplication::extractPortFromUrl($url);
|
||||
|
||||
expect($port)->toBeNull();
|
||||
});
|
||||
|
||||
it('checks if all FQDNs have port - single FQDN with port', function () {
|
||||
$app = Mockery::mock(ServiceApplication::class)->makePartial();
|
||||
$app->fqdn = 'http://example.com:3000';
|
||||
|
||||
$result = $app->allFqdnsHavePort();
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('checks if all FQDNs have port - single FQDN without port', function () {
|
||||
$app = Mockery::mock(ServiceApplication::class)->makePartial();
|
||||
$app->fqdn = 'http://example.com';
|
||||
|
||||
$result = $app->allFqdnsHavePort();
|
||||
|
||||
expect($result)->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks if all FQDNs have port - multiple FQDNs all with ports', function () {
|
||||
$app = Mockery::mock(ServiceApplication::class)->makePartial();
|
||||
$app->fqdn = 'http://example.com:3000,https://example.org:8080';
|
||||
|
||||
$result = $app->allFqdnsHavePort();
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('checks if all FQDNs have port - multiple FQDNs one without port', function () {
|
||||
$app = Mockery::mock(ServiceApplication::class)->makePartial();
|
||||
$app->fqdn = 'http://example.com:3000,https://example.org';
|
||||
|
||||
$result = $app->allFqdnsHavePort();
|
||||
|
||||
expect($result)->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks if all FQDNs have port - empty FQDN', function () {
|
||||
$app = Mockery::mock(ServiceApplication::class)->makePartial();
|
||||
$app->fqdn = '';
|
||||
|
||||
$result = $app->allFqdnsHavePort();
|
||||
|
||||
expect($result)->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks if all FQDNs have port - null FQDN', function () {
|
||||
$app = Mockery::mock(ServiceApplication::class)->makePartial();
|
||||
$app->fqdn = null;
|
||||
|
||||
$result = $app->allFqdnsHavePort();
|
||||
|
||||
expect($result)->toBeFalse();
|
||||
});
|
||||
Loading…
Reference in a new issue