From fbaa5eb3696f9588c0a78623988e9c07c384a115 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 4 Nov 2025 08:43:33 +0100 Subject: [PATCH] feat: Update ApplicationSetting model to include additional boolean casts - Changed `$cast` to `$casts` in ApplicationSetting model to enable proper boolean casting for new fields. - Added boolean fields: `is_spa`, `is_build_server_enabled`, `is_preserve_repository_enabled`, `is_container_label_escape_enabled`, `is_container_label_readonly_enabled`, and `use_build_secrets`. fix: Update Livewire component to reflect new property names - Updated references in the Livewire component for the new camelCase property names. - Adjusted bindings and IDs for consistency with the updated model. test: Add unit tests for ApplicationSetting boolean casting - Created tests to verify boolean casting for `is_static` and other boolean fields in ApplicationSetting. - Ensured all boolean fields are correctly defined in the casts array. test: Implement tests for SynchronizesModelData trait - Added tests to verify the functionality of the SynchronizesModelData trait, ensuring it correctly syncs properties between the component and the model. - Included tests for handling non-existent properties gracefully. --- app/Livewire/Project/Application/General.php | 525 ++++++++++-------- app/Models/ApplicationSetting.php | 8 +- .../project/application/general.blade.php | 102 ++-- .../Unit/ApplicationSettingStaticCastTest.php | 105 ++++ tests/Unit/SynchronizesModelDataTest.php | 163 ++++++ 5 files changed, 615 insertions(+), 288 deletions(-) create mode 100644 tests/Unit/ApplicationSettingStaticCastTest.php create mode 100644 tests/Unit/SynchronizesModelDataTest.php diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 8e8add430..03db8b1c8 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -3,7 +3,6 @@ namespace App\Livewire\Project\Application; use App\Actions\Application\GenerateConfig; -use App\Livewire\Concerns\SynchronizesModelData; use App\Models\Application; use App\Support\ValidationPatterns; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; @@ -15,7 +14,6 @@ class General extends Component { use AuthorizesRequests; - use SynchronizesModelData; public string $applicationId; @@ -29,85 +27,83 @@ class General extends Component public ?string $fqdn = null; - public string $git_repository; + public string $gitRepository; - public string $git_branch; + public string $gitBranch; - public ?string $git_commit_sha = null; + public ?string $gitCommitSha = null; - public ?string $install_command = null; + public ?string $installCommand = null; - public ?string $build_command = null; + public ?string $buildCommand = null; - public ?string $start_command = null; + public ?string $startCommand = null; - public string $build_pack; + public string $buildPack; - public string $static_image; + public string $staticImage; - public string $base_directory; + public string $baseDirectory; - public ?string $publish_directory = null; + public ?string $publishDirectory = null; - public ?string $ports_exposes = null; + public ?string $portsExposes = null; - public ?string $ports_mappings = null; + public ?string $portsMappings = null; - public ?string $custom_network_aliases = null; + public ?string $customNetworkAliases = null; public ?string $dockerfile = null; - public ?string $dockerfile_location = null; + public ?string $dockerfileLocation = null; - public ?string $dockerfile_target_build = null; + public ?string $dockerfileTargetBuild = null; - public ?string $docker_registry_image_name = null; + public ?string $dockerRegistryImageName = null; - public ?string $docker_registry_image_tag = null; + public ?string $dockerRegistryImageTag = null; - public ?string $docker_compose_location = null; + public ?string $dockerComposeLocation = null; - public ?string $docker_compose = null; + public ?string $dockerCompose = null; - public ?string $docker_compose_raw = null; + public ?string $dockerComposeRaw = null; - public ?string $docker_compose_custom_start_command = null; + public ?string $dockerComposeCustomStartCommand = null; - public ?string $docker_compose_custom_build_command = null; + public ?string $dockerComposeCustomBuildCommand = null; - public ?string $custom_labels = null; + public ?string $customDockerRunOptions = null; - public ?string $custom_docker_run_options = null; + public ?string $preDeploymentCommand = null; - public ?string $pre_deployment_command = null; + public ?string $preDeploymentCommandContainer = null; - public ?string $pre_deployment_command_container = null; + public ?string $postDeploymentCommand = null; - public ?string $post_deployment_command = null; + public ?string $postDeploymentCommandContainer = null; - public ?string $post_deployment_command_container = null; + public ?string $customNginxConfiguration = null; - public ?string $custom_nginx_configuration = null; + public bool $isStatic = false; - public bool $is_static = false; + public bool $isSpa = false; - public bool $is_spa = false; + public bool $isBuildServerEnabled = false; - public bool $is_build_server_enabled = false; + public bool $isPreserveRepositoryEnabled = false; - public bool $is_preserve_repository_enabled = false; + public bool $isContainerLabelEscapeEnabled = true; - public bool $is_container_label_escape_enabled = true; + public bool $isContainerLabelReadonlyEnabled = false; - public bool $is_container_label_readonly_enabled = false; + public bool $isHttpBasicAuthEnabled = false; - public bool $is_http_basic_auth_enabled = false; + public ?string $httpBasicAuthUsername = null; - public ?string $http_basic_auth_username = null; + public ?string $httpBasicAuthPassword = null; - public ?string $http_basic_auth_password = null; - - public ?string $watch_paths = null; + public ?string $watchPaths = null; public string $redirect; @@ -141,46 +137,46 @@ protected function rules(): array 'name' => ValidationPatterns::nameRules(), 'description' => ValidationPatterns::descriptionRules(), 'fqdn' => 'nullable', - 'git_repository' => 'required', - 'git_branch' => 'required', - 'git_commit_sha' => 'nullable', - 'install_command' => 'nullable', - 'build_command' => 'nullable', - 'start_command' => 'nullable', - 'build_pack' => 'required', - 'static_image' => 'required', - 'base_directory' => 'required', - 'publish_directory' => 'nullable', - 'ports_exposes' => 'required', - 'ports_mappings' => 'nullable', - 'custom_network_aliases' => 'nullable', + 'gitRepository' => 'required', + 'gitBranch' => 'required', + 'gitCommitSha' => 'nullable', + 'installCommand' => 'nullable', + 'buildCommand' => 'nullable', + 'startCommand' => 'nullable', + 'buildPack' => 'required', + 'staticImage' => 'required', + 'baseDirectory' => 'required', + 'publishDirectory' => 'nullable', + 'portsExposes' => 'required', + 'portsMappings' => 'nullable', + 'customNetworkAliases' => 'nullable', 'dockerfile' => 'nullable', - 'docker_registry_image_name' => 'nullable', - 'docker_registry_image_tag' => 'nullable', - 'dockerfile_location' => 'nullable', - 'docker_compose_location' => 'nullable', - 'docker_compose' => 'nullable', - 'docker_compose_raw' => 'nullable', - 'dockerfile_target_build' => 'nullable', - 'docker_compose_custom_start_command' => 'nullable', - 'docker_compose_custom_build_command' => 'nullable', - 'custom_labels' => 'nullable', - 'custom_docker_run_options' => 'nullable', - 'pre_deployment_command' => 'nullable', - 'pre_deployment_command_container' => 'nullable', - 'post_deployment_command' => 'nullable', - 'post_deployment_command_container' => 'nullable', - 'custom_nginx_configuration' => 'nullable', - 'is_static' => 'boolean|required', - 'is_spa' => 'boolean|required', - 'is_build_server_enabled' => 'boolean|required', - 'is_container_label_escape_enabled' => 'boolean|required', - 'is_container_label_readonly_enabled' => 'boolean|required', - 'is_preserve_repository_enabled' => 'boolean|required', - 'is_http_basic_auth_enabled' => 'boolean|required', - 'http_basic_auth_username' => 'string|nullable', - 'http_basic_auth_password' => 'string|nullable', - 'watch_paths' => 'nullable', + 'dockerRegistryImageName' => 'nullable', + 'dockerRegistryImageTag' => 'nullable', + 'dockerfileLocation' => 'nullable', + 'dockerComposeLocation' => 'nullable', + 'dockerCompose' => 'nullable', + 'dockerComposeRaw' => 'nullable', + 'dockerfileTargetBuild' => 'nullable', + 'dockerComposeCustomStartCommand' => 'nullable', + 'dockerComposeCustomBuildCommand' => 'nullable', + 'customLabels' => 'nullable', + 'customDockerRunOptions' => 'nullable', + 'preDeploymentCommand' => 'nullable', + 'preDeploymentCommandContainer' => 'nullable', + 'postDeploymentCommand' => 'nullable', + 'postDeploymentCommandContainer' => 'nullable', + 'customNginxConfiguration' => 'nullable', + 'isStatic' => 'boolean|required', + 'isSpa' => 'boolean|required', + 'isBuildServerEnabled' => 'boolean|required', + 'isContainerLabelEscapeEnabled' => 'boolean|required', + 'isContainerLabelReadonlyEnabled' => 'boolean|required', + 'isPreserveRepositoryEnabled' => 'boolean|required', + 'isHttpBasicAuthEnabled' => 'boolean|required', + 'httpBasicAuthUsername' => 'string|nullable', + 'httpBasicAuthPassword' => 'string|nullable', + 'watchPaths' => 'nullable', 'redirect' => 'string|required', ]; } @@ -193,26 +189,26 @@ protected function messages(): array 'name.required' => 'The Name field is required.', 'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', 'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', - 'git_repository.required' => 'The Git Repository field is required.', - 'git_branch.required' => 'The Git Branch field is required.', - 'build_pack.required' => 'The Build Pack field is required.', - 'static_image.required' => 'The Static Image field is required.', - 'base_directory.required' => 'The Base Directory field is required.', - 'ports_exposes.required' => 'The Exposed Ports field is required.', - 'is_static.required' => 'The Static setting is required.', - 'is_static.boolean' => 'The Static setting must be true or false.', - 'is_spa.required' => 'The SPA setting is required.', - 'is_spa.boolean' => 'The SPA setting must be true or false.', - 'is_build_server_enabled.required' => 'The Build Server setting is required.', - 'is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.', - 'is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.', - 'is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.', - 'is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.', - 'is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.', - 'is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.', - 'is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.', - 'is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.', - 'is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.', + 'gitRepository.required' => 'The Git Repository field is required.', + 'gitBranch.required' => 'The Git Branch field is required.', + 'buildPack.required' => 'The Build Pack field is required.', + 'staticImage.required' => 'The Static Image field is required.', + 'baseDirectory.required' => 'The Base Directory field is required.', + 'portsExposes.required' => 'The Exposed Ports field is required.', + 'isStatic.required' => 'The Static setting is required.', + 'isStatic.boolean' => 'The Static setting must be true or false.', + 'isSpa.required' => 'The SPA setting is required.', + 'isSpa.boolean' => 'The SPA setting must be true or false.', + 'isBuildServerEnabled.required' => 'The Build Server setting is required.', + 'isBuildServerEnabled.boolean' => 'The Build Server setting must be true or false.', + 'isContainerLabelEscapeEnabled.required' => 'The Container Label Escape setting is required.', + 'isContainerLabelEscapeEnabled.boolean' => 'The Container Label Escape setting must be true or false.', + 'isContainerLabelReadonlyEnabled.required' => 'The Container Label Readonly setting is required.', + 'isContainerLabelReadonlyEnabled.boolean' => 'The Container Label Readonly setting must be true or false.', + 'isPreserveRepositoryEnabled.required' => 'The Preserve Repository setting is required.', + 'isPreserveRepositoryEnabled.boolean' => 'The Preserve Repository setting must be true or false.', + 'isHttpBasicAuthEnabled.required' => 'The HTTP Basic Auth setting is required.', + 'isHttpBasicAuthEnabled.boolean' => 'The HTTP Basic Auth setting must be true or false.', 'redirect.required' => 'The Redirect setting is required.', 'redirect.string' => 'The Redirect setting must be a string.', ] @@ -220,43 +216,43 @@ protected function messages(): array } protected $validationAttributes = [ - 'application.name' => 'name', - 'application.description' => 'description', - 'application.fqdn' => 'FQDN', - 'application.git_repository' => 'Git repository', - 'application.git_branch' => 'Git branch', - 'application.git_commit_sha' => 'Git commit SHA', - 'application.install_command' => 'Install command', - 'application.build_command' => 'Build command', - 'application.start_command' => 'Start command', - 'application.build_pack' => 'Build pack', - 'application.static_image' => 'Static image', - 'application.base_directory' => 'Base directory', - 'application.publish_directory' => 'Publish directory', - 'application.ports_exposes' => 'Ports exposes', - 'application.ports_mappings' => 'Ports mappings', - 'application.dockerfile' => 'Dockerfile', - 'application.docker_registry_image_name' => 'Docker registry image name', - 'application.docker_registry_image_tag' => 'Docker registry image tag', - 'application.dockerfile_location' => 'Dockerfile location', - 'application.docker_compose_location' => 'Docker compose location', - 'application.docker_compose' => 'Docker compose', - 'application.docker_compose_raw' => 'Docker compose raw', - 'application.custom_labels' => 'Custom labels', - 'application.dockerfile_target_build' => 'Dockerfile target build', - 'application.custom_docker_run_options' => 'Custom docker run commands', - 'application.custom_network_aliases' => 'Custom docker network aliases', - 'application.docker_compose_custom_start_command' => 'Docker compose custom start command', - 'application.docker_compose_custom_build_command' => 'Docker compose custom build command', - 'application.custom_nginx_configuration' => 'Custom Nginx configuration', - 'application.settings.is_static' => 'Is static', - 'application.settings.is_spa' => 'Is SPA', - 'application.settings.is_build_server_enabled' => 'Is build server enabled', - 'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled', - 'application.settings.is_container_label_readonly_enabled' => 'Is container label readonly', - 'application.settings.is_preserve_repository_enabled' => 'Is preserve repository enabled', - 'application.watch_paths' => 'Watch paths', - 'application.redirect' => 'Redirect', + 'name' => 'name', + 'description' => 'description', + 'fqdn' => 'FQDN', + 'gitRepository' => 'Git repository', + 'gitBranch' => 'Git branch', + 'gitCommitSha' => 'Git commit SHA', + 'installCommand' => 'Install command', + 'buildCommand' => 'Build command', + 'startCommand' => 'Start command', + 'buildPack' => 'Build pack', + 'staticImage' => 'Static image', + 'baseDirectory' => 'Base directory', + 'publishDirectory' => 'Publish directory', + 'portsExposes' => 'Ports exposes', + 'portsMappings' => 'Ports mappings', + 'dockerfile' => 'Dockerfile', + 'dockerRegistryImageName' => 'Docker registry image name', + 'dockerRegistryImageTag' => 'Docker registry image tag', + 'dockerfileLocation' => 'Dockerfile location', + 'dockerComposeLocation' => 'Docker compose location', + 'dockerCompose' => 'Docker compose', + 'dockerComposeRaw' => 'Docker compose raw', + 'customLabels' => 'Custom labels', + 'dockerfileTargetBuild' => 'Dockerfile target build', + 'customDockerRunOptions' => 'Custom docker run commands', + 'customNetworkAliases' => 'Custom docker network aliases', + 'dockerComposeCustomStartCommand' => 'Docker compose custom start command', + 'dockerComposeCustomBuildCommand' => 'Docker compose custom build command', + 'customNginxConfiguration' => 'Custom Nginx configuration', + 'isStatic' => 'Is static', + 'isSpa' => 'Is SPA', + 'isBuildServerEnabled' => 'Is build server enabled', + 'isContainerLabelEscapeEnabled' => 'Is container label escape enabled', + 'isContainerLabelReadonlyEnabled' => 'Is container label readonly', + 'isPreserveRepositoryEnabled' => 'Is preserve repository enabled', + 'watchPaths' => 'Watch paths', + 'redirect' => 'Redirect', ]; public function mount() @@ -266,14 +262,14 @@ public function mount() if (is_null($this->parsedServices) || empty($this->parsedServices)) { $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); // Still sync data even if parse fails, so form fields are populated - $this->syncFromModel(); + $this->syncData(); return; } } catch (\Throwable $e) { $this->dispatch('error', $e->getMessage()); // Still sync data even on error, so form fields are populated - $this->syncFromModel(); + $this->syncData(); } if ($this->application->build_pack === 'dockercompose') { // Only update if user has permission @@ -325,57 +321,112 @@ public function mount() // Sync data from model to properties at the END, after all business logic // This ensures any modifications to $this->application during mount() are reflected in properties - $this->syncFromModel(); + $this->syncData(); } - protected function getModelBindings(): array + public function syncData(bool $toModel = false): void { - return [ - 'name' => 'application.name', - 'description' => 'application.description', - 'fqdn' => 'application.fqdn', - 'git_repository' => 'application.git_repository', - 'git_branch' => 'application.git_branch', - 'git_commit_sha' => 'application.git_commit_sha', - 'install_command' => 'application.install_command', - 'build_command' => 'application.build_command', - 'start_command' => 'application.start_command', - 'build_pack' => 'application.build_pack', - 'static_image' => 'application.static_image', - 'base_directory' => 'application.base_directory', - 'publish_directory' => 'application.publish_directory', - 'ports_exposes' => 'application.ports_exposes', - 'ports_mappings' => 'application.ports_mappings', - 'custom_network_aliases' => 'application.custom_network_aliases', - 'dockerfile' => 'application.dockerfile', - 'dockerfile_location' => 'application.dockerfile_location', - 'dockerfile_target_build' => 'application.dockerfile_target_build', - 'docker_registry_image_name' => 'application.docker_registry_image_name', - 'docker_registry_image_tag' => 'application.docker_registry_image_tag', - 'docker_compose_location' => 'application.docker_compose_location', - 'docker_compose' => 'application.docker_compose', - 'docker_compose_raw' => 'application.docker_compose_raw', - 'docker_compose_custom_start_command' => 'application.docker_compose_custom_start_command', - 'docker_compose_custom_build_command' => 'application.docker_compose_custom_build_command', - 'custom_labels' => 'application.custom_labels', - 'custom_docker_run_options' => 'application.custom_docker_run_options', - 'pre_deployment_command' => 'application.pre_deployment_command', - 'pre_deployment_command_container' => 'application.pre_deployment_command_container', - 'post_deployment_command' => 'application.post_deployment_command', - 'post_deployment_command_container' => 'application.post_deployment_command_container', - 'custom_nginx_configuration' => 'application.custom_nginx_configuration', - 'is_static' => 'application.settings.is_static', - 'is_spa' => 'application.settings.is_spa', - 'is_build_server_enabled' => 'application.settings.is_build_server_enabled', - 'is_preserve_repository_enabled' => 'application.settings.is_preserve_repository_enabled', - 'is_container_label_escape_enabled' => 'application.settings.is_container_label_escape_enabled', - 'is_container_label_readonly_enabled' => 'application.settings.is_container_label_readonly_enabled', - 'is_http_basic_auth_enabled' => 'application.is_http_basic_auth_enabled', - 'http_basic_auth_username' => 'application.http_basic_auth_username', - 'http_basic_auth_password' => 'application.http_basic_auth_password', - 'watch_paths' => 'application.watch_paths', - 'redirect' => 'application.redirect', - ]; + if ($toModel) { + $this->validate(); + + // Application properties + $this->application->name = $this->name; + $this->application->description = $this->description; + $this->application->fqdn = $this->fqdn; + $this->application->git_repository = $this->gitRepository; + $this->application->git_branch = $this->gitBranch; + $this->application->git_commit_sha = $this->gitCommitSha; + $this->application->install_command = $this->installCommand; + $this->application->build_command = $this->buildCommand; + $this->application->start_command = $this->startCommand; + $this->application->build_pack = $this->buildPack; + $this->application->static_image = $this->staticImage; + $this->application->base_directory = $this->baseDirectory; + $this->application->publish_directory = $this->publishDirectory; + $this->application->ports_exposes = $this->portsExposes; + $this->application->ports_mappings = $this->portsMappings; + $this->application->custom_network_aliases = $this->customNetworkAliases; + $this->application->dockerfile = $this->dockerfile; + $this->application->dockerfile_location = $this->dockerfileLocation; + $this->application->dockerfile_target_build = $this->dockerfileTargetBuild; + $this->application->docker_registry_image_name = $this->dockerRegistryImageName; + $this->application->docker_registry_image_tag = $this->dockerRegistryImageTag; + $this->application->docker_compose_location = $this->dockerComposeLocation; + $this->application->docker_compose = $this->dockerCompose; + $this->application->docker_compose_raw = $this->dockerComposeRaw; + $this->application->docker_compose_custom_start_command = $this->dockerComposeCustomStartCommand; + $this->application->docker_compose_custom_build_command = $this->dockerComposeCustomBuildCommand; + $this->application->custom_labels = base64_encode($this->customLabels); + $this->application->custom_docker_run_options = $this->customDockerRunOptions; + $this->application->pre_deployment_command = $this->preDeploymentCommand; + $this->application->pre_deployment_command_container = $this->preDeploymentCommandContainer; + $this->application->post_deployment_command = $this->postDeploymentCommand; + $this->application->post_deployment_command_container = $this->postDeploymentCommandContainer; + $this->application->custom_nginx_configuration = $this->customNginxConfiguration; + $this->application->is_http_basic_auth_enabled = $this->isHttpBasicAuthEnabled; + $this->application->http_basic_auth_username = $this->httpBasicAuthUsername; + $this->application->http_basic_auth_password = $this->httpBasicAuthPassword; + $this->application->watch_paths = $this->watchPaths; + $this->application->redirect = $this->redirect; + + // Application settings properties + $this->application->settings->is_static = $this->isStatic; + $this->application->settings->is_spa = $this->isSpa; + $this->application->settings->is_build_server_enabled = $this->isBuildServerEnabled; + $this->application->settings->is_preserve_repository_enabled = $this->isPreserveRepositoryEnabled; + $this->application->settings->is_container_label_escape_enabled = $this->isContainerLabelEscapeEnabled; + $this->application->settings->is_container_label_readonly_enabled = $this->isContainerLabelReadonlyEnabled; + + $this->application->settings->save(); + } else { + // From model to properties + $this->name = $this->application->name; + $this->description = $this->application->description; + $this->fqdn = $this->application->fqdn; + $this->gitRepository = $this->application->git_repository; + $this->gitBranch = $this->application->git_branch; + $this->gitCommitSha = $this->application->git_commit_sha; + $this->installCommand = $this->application->install_command; + $this->buildCommand = $this->application->build_command; + $this->startCommand = $this->application->start_command; + $this->buildPack = $this->application->build_pack; + $this->staticImage = $this->application->static_image; + $this->baseDirectory = $this->application->base_directory; + $this->publishDirectory = $this->application->publish_directory; + $this->portsExposes = $this->application->ports_exposes; + $this->portsMappings = $this->application->ports_mappings; + $this->customNetworkAliases = $this->application->custom_network_aliases; + $this->dockerfile = $this->application->dockerfile; + $this->dockerfileLocation = $this->application->dockerfile_location; + $this->dockerfileTargetBuild = $this->application->dockerfile_target_build; + $this->dockerRegistryImageName = $this->application->docker_registry_image_name; + $this->dockerRegistryImageTag = $this->application->docker_registry_image_tag; + $this->dockerComposeLocation = $this->application->docker_compose_location; + $this->dockerCompose = $this->application->docker_compose; + $this->dockerComposeRaw = $this->application->docker_compose_raw; + $this->dockerComposeCustomStartCommand = $this->application->docker_compose_custom_start_command; + $this->dockerComposeCustomBuildCommand = $this->application->docker_compose_custom_build_command; + $this->customLabels = $this->application->parseContainerLabels(); + $this->customDockerRunOptions = $this->application->custom_docker_run_options; + $this->preDeploymentCommand = $this->application->pre_deployment_command; + $this->preDeploymentCommandContainer = $this->application->pre_deployment_command_container; + $this->postDeploymentCommand = $this->application->post_deployment_command; + $this->postDeploymentCommandContainer = $this->application->post_deployment_command_container; + $this->customNginxConfiguration = $this->application->custom_nginx_configuration; + $this->isHttpBasicAuthEnabled = $this->application->is_http_basic_auth_enabled; + $this->httpBasicAuthUsername = $this->application->http_basic_auth_username; + $this->httpBasicAuthPassword = $this->application->http_basic_auth_password; + $this->watchPaths = $this->application->watch_paths; + $this->redirect = $this->application->redirect; + + // Application settings properties + $this->isStatic = $this->application->settings->is_static; + $this->isSpa = $this->application->settings->is_spa; + $this->isBuildServerEnabled = $this->application->settings->is_build_server_enabled; + $this->isPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled; + $this->isContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled; + $this->isContainerLabelReadonlyEnabled = $this->application->settings->is_container_label_readonly_enabled; + } } public function instantSave() @@ -387,7 +438,7 @@ public function instantSave() $oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled; $oldIsPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled; - $this->syncToModel(); + $this->syncData(toModel: true); if ($this->application->settings->isDirty('is_spa')) { $this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static'); @@ -395,24 +446,27 @@ public function instantSave() if ($this->application->isDirty('is_http_basic_auth_enabled')) { $this->application->save(); } + $this->application->settings->save(); + $this->dispatch('success', 'Settings saved.'); $this->application->refresh(); - $this->syncFromModel(); + + $this->syncData(); // If port_exposes changed, reset default labels - if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) { + if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) { $this->resetDefaultLabels(false); } - if ($oldIsPreserveRepositoryEnabled !== $this->is_preserve_repository_enabled) { - if ($this->is_preserve_repository_enabled === false) { + if ($oldIsPreserveRepositoryEnabled !== $this->isPreserveRepositoryEnabled) { + if ($this->isPreserveRepositoryEnabled === false) { $this->application->fileStorages->each(function ($storage) { - $storage->is_based_on_git = $this->is_preserve_repository_enabled; + $storage->is_based_on_git = $this->isPreserveRepositoryEnabled; $storage->save(); }); } } - if ($this->is_container_label_readonly_enabled) { + if ($this->isContainerLabelReadonlyEnabled) { $this->resetDefaultLabels(false); } } catch (\Throwable $e) { @@ -441,7 +495,7 @@ public function loadComposeFile($isInit = false, $showToast = true) // Sync the docker_compose_raw from the model to the component property // This ensures the Monaco editor displays the loaded compose file - $this->syncFromModel(); + $this->syncData(); $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; // Convert service names with dots and dashes to use underscores for HTML form binding @@ -507,7 +561,7 @@ public function generateDomain(string $serviceName) public function updatedBaseDirectory() { - if ($this->build_pack === 'dockercompose') { + if ($this->buildPack === 'dockercompose') { $this->loadComposeFile(); } } @@ -527,24 +581,24 @@ public function updatedBuildPack() } catch (\Illuminate\Auth\Access\AuthorizationException $e) { // User doesn't have permission, revert the change and return $this->application->refresh(); - $this->syncFromModel(); + $this->syncData(); return; } // Sync property to model before checking/modifying - $this->syncToModel(); + $this->syncData(toModel: true); - if ($this->build_pack !== 'nixpacks') { - $this->is_static = false; + if ($this->buildPack !== 'nixpacks') { + $this->isStatic = false; $this->application->settings->is_static = false; $this->application->settings->save(); } else { - $this->ports_exposes = 3000; - $this->application->ports_exposes = 3000; + $this->portsExposes = '3000'; + $this->application->ports_exposes = '3000'; $this->resetDefaultLabels(false); } - if ($this->build_pack === 'dockercompose') { + if ($this->buildPack === 'dockercompose') { // Only update if user has permission try { $this->authorize('update', $this->application); @@ -567,9 +621,9 @@ public function updatedBuildPack() $this->application->environment_variables_preview()->where('key', 'LIKE', 'SERVICE_URL_%')->delete(); } } - if ($this->build_pack === 'static') { - $this->ports_exposes = 80; - $this->application->ports_exposes = 80; + if ($this->buildPack === 'static') { + $this->portsExposes = '80'; + $this->application->ports_exposes = '80'; $this->resetDefaultLabels(false); $this->generateNginxConfiguration(); } @@ -586,10 +640,10 @@ public function getWildcardDomain() if ($server) { $fqdn = generateUrl(server: $server, random: $this->application->uuid); $this->fqdn = $fqdn; - $this->syncToModel(); + $this->syncData(toModel: true); $this->application->save(); $this->application->refresh(); - $this->syncFromModel(); + $this->syncData(); $this->resetDefaultLabels(); $this->dispatch('success', 'Wildcard domain generated.'); } @@ -603,11 +657,11 @@ public function generateNginxConfiguration($type = 'static') try { $this->authorize('update', $this->application); - $this->custom_nginx_configuration = defaultNginxConfiguration($type); - $this->syncToModel(); + $this->customNginxConfiguration = defaultNginxConfiguration($type); + $this->syncData(toModel: true); $this->application->save(); $this->application->refresh(); - $this->syncFromModel(); + $this->syncData(); $this->dispatch('success', 'Nginx configuration generated.'); } catch (\Throwable $e) { return handleError($e, $this); @@ -617,16 +671,15 @@ public function generateNginxConfiguration($type = 'static') public function resetDefaultLabels($manualReset = false) { try { - if (! $this->is_container_label_readonly_enabled && ! $manualReset) { + if (! $this->isContainerLabelReadonlyEnabled && ! $manualReset) { return; } $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); - $this->custom_labels = base64_encode($this->customLabels); - $this->syncToModel(); + $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); $this->application->refresh(); - $this->syncFromModel(); - if ($this->build_pack === 'dockercompose') { + $this->syncData(); + if ($this->buildPack === 'dockercompose') { $this->loadComposeFile(showToast: false); } $this->dispatch('configurationChanged'); @@ -722,7 +775,7 @@ public function submit($showToaster = true) $this->dispatch('warning', __('warning.sslipdomain')); } - $this->syncToModel(); + $this->syncData(toModel: true); if ($this->application->isDirty('redirect')) { $this->setRedirect(); @@ -742,42 +795,42 @@ public function submit($showToaster = true) $this->application->save(); } - if ($this->build_pack === 'dockercompose' && $oldDockerComposeLocation !== $this->docker_compose_location) { + if ($this->buildPack === 'dockercompose' && $oldDockerComposeLocation !== $this->dockerComposeLocation) { $compose_return = $this->loadComposeFile(showToast: false); if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) { return; } } - if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) { + if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) { $this->resetDefaultLabels(); } - if ($this->build_pack === 'dockerimage') { + if ($this->buildPack === 'dockerimage') { $this->validate([ - 'docker_registry_image_name' => 'required', + 'dockerRegistryImageName' => 'required', ]); } - if ($this->custom_docker_run_options) { - $this->custom_docker_run_options = str($this->custom_docker_run_options)->trim()->toString(); - $this->application->custom_docker_run_options = $this->custom_docker_run_options; + if ($this->customDockerRunOptions) { + $this->customDockerRunOptions = str($this->customDockerRunOptions)->trim()->toString(); + $this->application->custom_docker_run_options = $this->customDockerRunOptions; } if ($this->dockerfile) { $port = get_port_from_dockerfile($this->dockerfile); - if ($port && ! $this->ports_exposes) { - $this->ports_exposes = $port; + if ($port && ! $this->portsExposes) { + $this->portsExposes = $port; $this->application->ports_exposes = $port; } } - if ($this->base_directory && $this->base_directory !== '/') { - $this->base_directory = rtrim($this->base_directory, '/'); - $this->application->base_directory = $this->base_directory; + if ($this->baseDirectory && $this->baseDirectory !== '/') { + $this->baseDirectory = rtrim($this->baseDirectory, '/'); + $this->application->base_directory = $this->baseDirectory; } - if ($this->publish_directory && $this->publish_directory !== '/') { - $this->publish_directory = rtrim($this->publish_directory, '/'); - $this->application->publish_directory = $this->publish_directory; + if ($this->publishDirectory && $this->publishDirectory !== '/') { + $this->publishDirectory = rtrim($this->publishDirectory, '/'); + $this->application->publish_directory = $this->publishDirectory; } - if ($this->build_pack === 'dockercompose') { + if ($this->buildPack === 'dockercompose') { $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); if ($this->application->isDirty('docker_compose_domains')) { foreach ($this->parsedServiceDomains as $service) { @@ -809,11 +862,11 @@ public function submit($showToaster = true) $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); $this->application->refresh(); - $this->syncFromModel(); + $this->syncData(); $showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!'); } catch (\Throwable $e) { $this->application->refresh(); - $this->syncFromModel(); + $this->syncData(); return handleError($e, $this); } finally { diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php index 4b03c69e1..26cb937b3 100644 --- a/app/Models/ApplicationSetting.php +++ b/app/Models/ApplicationSetting.php @@ -7,8 +7,14 @@ class ApplicationSetting extends Model { - protected $cast = [ + protected $casts = [ 'is_static' => 'boolean', + 'is_spa' => 'boolean', + 'is_build_server_enabled' => 'boolean', + 'is_preserve_repository_enabled' => 'boolean', + 'is_container_label_escape_enabled' => 'boolean', + 'is_container_label_readonly_enabled' => 'boolean', + 'use_build_secrets' => 'boolean', 'is_auto_deploy_enabled' => 'boolean', 'is_force_https_enabled' => 'boolean', 'is_debug_enabled' => 'boolean', diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 7759e0604..2484005ef 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -23,7 +23,7 @@ @if (!$application->dockerfile && $application->build_pack !== 'dockerimage')
- @@ -31,7 +31,7 @@ @if ($application->settings->is_static || $application->build_pack === 'static') - @@ -66,7 +66,7 @@
@endif @if ($application->settings->is_static || $application->build_pack === 'static') - @can('update', $application) @@ -77,13 +77,13 @@ @endif
@if ($application->could_set_build_commands()) - @endif @if ($application->settings->is_static && $application->build_pack !== 'static') @endif
@@ -164,15 +164,15 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@if ($application->build_pack === 'dockerimage') @if ($application->destination->server->isSwarm()) - - @else - - @endif @@ -181,18 +181,18 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" $application->destination->server->isSwarm() || $application->additional_servers->count() > 0 || $application->settings->is_build_server_enabled) - - @else - - @@ -206,20 +206,20 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @else @if ($application->could_set_build_commands()) @if ($application->build_pack === 'nixpacks')
Nixpacks will detect the required configuration @@ -239,16 +239,16 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @endcan
@@ -261,12 +261,12 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@@ -274,36 +274,36 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@endif
@else
- @if ($application->build_pack === 'dockerfile' && !$application->dockerfile) - @endif @if ($application->build_pack === 'dockerfile') - @endif @if ($application->could_set_build_commands()) @if ($application->settings->is_static) - @else - @endif @endif @@ -313,21 +313,21 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@endif @if ($application->build_pack !== 'dockercompose')
@endif @@ -344,18 +344,18 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @endcan
@if ($application->settings->is_raw_compose_deployment_enabled) - @else @if ((int) $application->compose_parsing_version >= 3) - @endif - @@ -363,11 +363,11 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
{{-- --}} + id="isContainerLabelReadonlyEnabled" instantSave> --}}
@endif @if ($application->dockerfile) @@ -378,30 +378,30 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"

Network

@if ($application->settings->is_static || $application->build_pack === 'static') - @else @if ($application->settings->is_container_label_readonly_enabled === false) - @else - @endif @endif @if (!$application->destination->server->isSwarm()) - @endif @if (!$application->destination->server->isSwarm()) - + wire:model="customNetworkAliases" x-bind:disabled="!canUpdate" /> @endif
@@ -409,14 +409,14 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@if ($application->is_http_basic_auth_enabled)
- -
@endif @@ -432,11 +432,11 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@can('update', $application) @@ -455,21 +455,21 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"

Pre/Post Deployment Commands

@if ($application->build_pack === 'dockercompose') - @endif
@if ($application->build_pack === 'dockercompose') @endif
diff --git a/tests/Unit/ApplicationSettingStaticCastTest.php b/tests/Unit/ApplicationSettingStaticCastTest.php new file mode 100644 index 000000000..35ab7faaf --- /dev/null +++ b/tests/Unit/ApplicationSettingStaticCastTest.php @@ -0,0 +1,105 @@ +is_static = true; + + // Verify it's cast to boolean + expect($setting->is_static)->toBeTrue() + ->and($setting->is_static)->toBeBool(); +}); + +it('casts is_static to boolean when false', function () { + $setting = new ApplicationSetting; + $setting->is_static = false; + + // Verify it's cast to boolean + expect($setting->is_static)->toBeFalse() + ->and($setting->is_static)->toBeBool(); +}); + +it('casts is_static from string "1" to boolean true', function () { + $setting = new ApplicationSetting; + $setting->is_static = '1'; + + // Should cast string to boolean + expect($setting->is_static)->toBeTrue() + ->and($setting->is_static)->toBeBool(); +}); + +it('casts is_static from string "0" to boolean false', function () { + $setting = new ApplicationSetting; + $setting->is_static = '0'; + + // Should cast string to boolean + expect($setting->is_static)->toBeFalse() + ->and($setting->is_static)->toBeBool(); +}); + +it('casts is_static from integer 1 to boolean true', function () { + $setting = new ApplicationSetting; + $setting->is_static = 1; + + // Should cast integer to boolean + expect($setting->is_static)->toBeTrue() + ->and($setting->is_static)->toBeBool(); +}); + +it('casts is_static from integer 0 to boolean false', function () { + $setting = new ApplicationSetting; + $setting->is_static = 0; + + // Should cast integer to boolean + expect($setting->is_static)->toBeFalse() + ->and($setting->is_static)->toBeBool(); +}); + +it('has casts array property defined correctly', function () { + $setting = new ApplicationSetting; + + // Verify the casts property exists and is configured + $casts = $setting->getCasts(); + + expect($casts)->toHaveKey('is_static') + ->and($casts['is_static'])->toBe('boolean'); +}); + +it('casts all boolean fields correctly', function () { + $setting = new ApplicationSetting; + + // Get all casts + $casts = $setting->getCasts(); + + // Verify all expected boolean fields are cast + $expectedBooleanCasts = [ + 'is_static', + 'is_spa', + 'is_build_server_enabled', + 'is_preserve_repository_enabled', + 'is_container_label_escape_enabled', + 'is_container_label_readonly_enabled', + 'use_build_secrets', + 'is_auto_deploy_enabled', + 'is_force_https_enabled', + 'is_debug_enabled', + 'is_preview_deployments_enabled', + 'is_pr_deployments_public_enabled', + 'is_git_submodules_enabled', + 'is_git_lfs_enabled', + 'is_git_shallow_clone_enabled', + ]; + + foreach ($expectedBooleanCasts as $field) { + expect($casts)->toHaveKey($field) + ->and($casts[$field])->toBe('boolean'); + } +}); diff --git a/tests/Unit/SynchronizesModelDataTest.php b/tests/Unit/SynchronizesModelDataTest.php new file mode 100644 index 000000000..4551fb056 --- /dev/null +++ b/tests/Unit/SynchronizesModelDataTest.php @@ -0,0 +1,163 @@ + 'application.settings.is_static', + ]; + } + + // Expose protected method for testing + public function testSync(): void + { + $this->syncToModel(); + } + }; + + // Create real ApplicationSetting instance + $settings = new ApplicationSetting; + $settings->is_static = false; + + // Create Application instance + $application = new Application; + $application->setRelation('settings', $settings); + + $component->application = $application; + $component->is_static = true; + + // Sync to model + $component->testSync(); + + // Verify the value was set on the model + expect($component->application->settings->is_static)->toBeTrue(); +}); + +it('syncs boolean values correctly', function () { + $component = new class + { + use SynchronizesModelData; + + public bool $is_spa = true; + + public bool $is_build_server_enabled = false; + + public Application $application; + + protected function getModelBindings(): array + { + return [ + 'is_spa' => 'application.settings.is_spa', + 'is_build_server_enabled' => 'application.settings.is_build_server_enabled', + ]; + } + + public function testSync(): void + { + $this->syncToModel(); + } + }; + + $settings = new ApplicationSetting; + $settings->is_spa = false; + $settings->is_build_server_enabled = true; + + $application = new Application; + $application->setRelation('settings', $settings); + + $component->application = $application; + + $component->testSync(); + + expect($component->application->settings->is_spa)->toBeTrue() + ->and($component->application->settings->is_build_server_enabled)->toBeFalse(); +}); + +it('syncs from model to component correctly', function () { + $component = new class + { + use SynchronizesModelData; + + public bool $is_static = false; + + public bool $is_spa = false; + + public Application $application; + + protected function getModelBindings(): array + { + return [ + 'is_static' => 'application.settings.is_static', + 'is_spa' => 'application.settings.is_spa', + ]; + } + + public function testSyncFrom(): void + { + $this->syncFromModel(); + } + }; + + $settings = new ApplicationSetting; + $settings->is_static = true; + $settings->is_spa = true; + + $application = new Application; + $application->setRelation('settings', $settings); + + $component->application = $application; + + $component->testSyncFrom(); + + expect($component->is_static)->toBeTrue() + ->and($component->is_spa)->toBeTrue(); +}); + +it('handles properties that do not exist gracefully', function () { + $component = new class + { + use SynchronizesModelData; + + public Application $application; + + protected function getModelBindings(): array + { + return [ + 'non_existent_property' => 'application.name', + ]; + } + + public function testSync(): void + { + $this->syncToModel(); + } + }; + + $application = new Application; + $component->application = $application; + + // Should not throw an error + $component->testSync(); + + expect(true)->toBeTrue(); +});