makePartial(); $env->key = $key; $env->shouldReceive('getResolvedValueWithServer')->andReturn($resolvedValue); return $env; } /** * Test to verify that null and empty environment variables are filtered out * when generating Nixpacks configuration. * * This test verifies the fix for the issue where null or empty environment variable * values would be passed to Nixpacks as `--env KEY=` (with no value), causing * JSON parsing errors: "invalid type: null, expected a string at line 12 column 27" * * The fix ensures that: * 1. User-defined environment variables with null or empty values are filtered out * 2. COOLIFY_* environment variables with null or empty values are filtered out * 3. Only environment variables with valid non-empty values are passed to Nixpacks */ it('filters out null environment variables from nixpacks build command', function () { // Mock application with nixpacks build pack $mockApplication = Mockery::mock(Application::class); $mockApplication->shouldReceive('getAttribute') ->with('build_pack') ->andReturn('nixpacks'); // Mock environment variables - some with null/empty values $envVar1 = nixpacksEnvVariable('VALID_VAR', 'valid_value'); $envVar2 = nixpacksEnvVariable('NULL_VAR', null); $envVar3 = nixpacksEnvVariable('EMPTY_VAR', ''); $envVar4 = nixpacksEnvVariable('ANOTHER_VALID_VAR', 'another_value'); $nixpacksEnvVars = collect([$envVar1, $envVar2, $envVar3, $envVar4]); $mockApplication->shouldReceive('getAttribute') ->with('nixpacks_environment_variables') ->andReturn($nixpacksEnvVars); // Mock application deployment queue $mockQueue = Mockery::mock(ApplicationDeploymentQueue::class); $mockQueue->shouldReceive('getAttribute')->with('application_id')->andReturn(1); // Mock the job $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); $job->shouldAllowMockingProtectedMethods(); $reflection = new \ReflectionClass(ApplicationDeploymentJob::class); // Set private properties $applicationProperty = $reflection->getProperty('application'); $applicationProperty->setAccessible(true); $applicationProperty->setValue($job, $mockApplication); $mainServerProperty = $reflection->getProperty('mainServer'); $mainServerProperty->setAccessible(true); $mainServerProperty->setValue($job, new Server); $pullRequestProperty = $reflection->getProperty('pull_request_id'); $pullRequestProperty->setAccessible(true); $pullRequestProperty->setValue($job, 0); // Mock generate_coolify_env_variables to return some values including null $job->shouldReceive('generate_coolify_env_variables') ->andReturn(collect([ 'COOLIFY_FQDN' => 'example.com', 'COOLIFY_URL' => null, // null value that should be filtered 'COOLIFY_BRANCH' => '', // empty value that should be filtered 'SOURCE_COMMIT' => 'abc123', ])); // Call the private method $method = $reflection->getMethod('generate_nixpacks_env_variables'); $method->setAccessible(true); $method->invoke($job); // Get the generated env_nixpacks_args $envArgsProperty = $reflection->getProperty('env_nixpacks_args'); $envArgsProperty->setAccessible(true); $envArgs = $envArgsProperty->getValue($job); // Verify that only valid environment variables are included (values are now single-quote escaped) expect($envArgs)->toContain("--env 'VALID_VAR=valid_value'"); expect($envArgs)->toContain("--env 'ANOTHER_VALID_VAR=another_value'"); expect($envArgs)->toContain("--env 'COOLIFY_FQDN=example.com'"); expect($envArgs)->toContain("--env 'SOURCE_COMMIT=abc123'"); // Verify that null and empty environment variables are filtered out expect($envArgs)->not->toContain('NULL_VAR'); expect($envArgs)->not->toContain('EMPTY_VAR'); expect($envArgs)->not->toContain('COOLIFY_URL'); expect($envArgs)->not->toContain('COOLIFY_BRANCH'); // Verify no environment variables end with just '=' (which indicates null/empty value) expect($envArgs)->not->toMatch('/--env [A-Z_]+=$/'); expect($envArgs)->not->toMatch("/--env '[A-Z_]+='$/"); }); it('filters out null environment variables from nixpacks preview deployments', function () { // Mock application with nixpacks build pack $mockApplication = Mockery::mock(Application::class); $mockApplication->shouldReceive('getAttribute') ->with('build_pack') ->andReturn('nixpacks'); // Mock preview environment variables - some with null/empty values $envVar1 = nixpacksEnvVariable('PREVIEW_VAR', 'preview_value'); $envVar2 = nixpacksEnvVariable('NULL_PREVIEW_VAR', null); $previewEnvVars = collect([$envVar1, $envVar2]); $mockApplication->shouldReceive('getAttribute') ->with('nixpacks_environment_variables_preview') ->andReturn($previewEnvVars); // Mock application deployment queue $mockQueue = Mockery::mock(ApplicationDeploymentQueue::class); $mockQueue->shouldReceive('getAttribute')->with('application_id')->andReturn(1); // Mock the job $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); $job->shouldAllowMockingProtectedMethods(); $reflection = new \ReflectionClass(ApplicationDeploymentJob::class); // Set private properties $applicationProperty = $reflection->getProperty('application'); $applicationProperty->setAccessible(true); $applicationProperty->setValue($job, $mockApplication); $mainServerProperty = $reflection->getProperty('mainServer'); $mainServerProperty->setAccessible(true); $mainServerProperty->setValue($job, new Server); $pullRequestProperty = $reflection->getProperty('pull_request_id'); $pullRequestProperty->setAccessible(true); $pullRequestProperty->setValue($job, 123); // Non-zero for preview deployment // Mock generate_coolify_env_variables $job->shouldReceive('generate_coolify_env_variables') ->andReturn(collect([ 'COOLIFY_FQDN' => 'preview.example.com', ])); // Call the private method $method = $reflection->getMethod('generate_nixpacks_env_variables'); $method->setAccessible(true); $method->invoke($job); // Get the generated env_nixpacks_args $envArgsProperty = $reflection->getProperty('env_nixpacks_args'); $envArgsProperty->setAccessible(true); $envArgs = $envArgsProperty->getValue($job); // Verify that only valid environment variables are included (values are now single-quote escaped) expect($envArgs)->toContain("--env 'PREVIEW_VAR=preview_value'"); expect($envArgs)->toContain("--env 'COOLIFY_FQDN=preview.example.com'"); // Verify that null environment variables are filtered out expect($envArgs)->not->toContain('NULL_PREVIEW_VAR'); }); it('handles all environment variables being null or empty', function () { // Mock application with nixpacks build pack $mockApplication = Mockery::mock(Application::class); $mockApplication->shouldReceive('getAttribute') ->with('build_pack') ->andReturn('nixpacks'); // Mock environment variables - all null or empty $envVar1 = nixpacksEnvVariable('NULL_VAR', null); $envVar2 = nixpacksEnvVariable('EMPTY_VAR', ''); $nixpacksEnvVars = collect([$envVar1, $envVar2]); $mockApplication->shouldReceive('getAttribute') ->with('nixpacks_environment_variables') ->andReturn($nixpacksEnvVars); // Mock application deployment queue $mockQueue = Mockery::mock(ApplicationDeploymentQueue::class); $mockQueue->shouldReceive('getAttribute')->with('application_id')->andReturn(1); // Mock the job $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); $job->shouldAllowMockingProtectedMethods(); $reflection = new \ReflectionClass(ApplicationDeploymentJob::class); // Set private properties $applicationProperty = $reflection->getProperty('application'); $applicationProperty->setAccessible(true); $applicationProperty->setValue($job, $mockApplication); $mainServerProperty = $reflection->getProperty('mainServer'); $mainServerProperty->setAccessible(true); $mainServerProperty->setValue($job, new Server); $pullRequestProperty = $reflection->getProperty('pull_request_id'); $pullRequestProperty->setAccessible(true); $pullRequestProperty->setValue($job, 0); // Mock generate_coolify_env_variables to return all null/empty values $job->shouldReceive('generate_coolify_env_variables') ->andReturn(collect([ 'COOLIFY_URL' => null, 'COOLIFY_BRANCH' => '', ])); // Call the private method $method = $reflection->getMethod('generate_nixpacks_env_variables'); $method->setAccessible(true); $method->invoke($job); // Get the generated env_nixpacks_args $envArgsProperty = $reflection->getProperty('env_nixpacks_args'); $envArgsProperty->setAccessible(true); $envArgs = $envArgsProperty->getValue($job); // Verify that the result is empty or contains no environment variables expect($envArgs)->toBe(''); }); it('filters out null coolify env variables from env_args used in nixpacks plan JSON', function () { // This test verifies the fix for GitHub issue #6830: // When application->fqdn is null, COOLIFY_FQDN/COOLIFY_URL get set to null // in generate_coolify_env_variables(). The generate_env_variables() method // merges these into env_args which become the nixpacks plan JSON "variables". // Nixpacks requires all variable values to be strings, so null causes: // "Error: Failed to parse Nixpacks config file - invalid type: null, expected a string" // Simulate the coolify env collection with null values (as produced when fqdn is null) $coolify_envs = collect([ 'COOLIFY_URL' => null, 'COOLIFY_FQDN' => null, 'COOLIFY_BRANCH' => 'main', 'COOLIFY_RESOURCE_UUID' => 'abc123', 'COOLIFY_CONTAINER_NAME' => '', ]); // Apply the same filtering logic used in generate_env_variables() $env_args = collect([]); $coolify_envs->each(function ($value, $key) use ($env_args) { if (! is_null($value) && $value !== '') { $env_args->put($key, $value); } }); // Null values must NOT be present — they cause nixpacks JSON parse errors expect($env_args->has('COOLIFY_URL'))->toBeFalse(); expect($env_args->has('COOLIFY_FQDN'))->toBeFalse(); expect($env_args->has('COOLIFY_CONTAINER_NAME'))->toBeFalse(); // Non-null values must be preserved expect($env_args->get('COOLIFY_BRANCH'))->toBe('main'); expect($env_args->get('COOLIFY_RESOURCE_UUID'))->toBe('abc123'); // The resulting array must be safe for json_encode into nixpacks config $json = json_encode(['variables' => $env_args->toArray()], JSON_PRETTY_PRINT); $parsed = json_decode($json, true); foreach ($parsed['variables'] as $value) { expect($value)->toBeString(); } }); it('preserves environment variables with zero values', function () { // Mock application with nixpacks build pack $mockApplication = Mockery::mock(Application::class); $mockApplication->shouldReceive('getAttribute') ->with('build_pack') ->andReturn('nixpacks'); // Mock environment variables with zero values (which should NOT be filtered) $envVar1 = nixpacksEnvVariable('ZERO_VALUE', '0'); $envVar2 = nixpacksEnvVariable('FALSE_VALUE', 'false'); $nixpacksEnvVars = collect([$envVar1, $envVar2]); $mockApplication->shouldReceive('getAttribute') ->with('nixpacks_environment_variables') ->andReturn($nixpacksEnvVars); // Mock application deployment queue $mockQueue = Mockery::mock(ApplicationDeploymentQueue::class); $mockQueue->shouldReceive('getAttribute')->with('application_id')->andReturn(1); // Mock the job $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); $job->shouldAllowMockingProtectedMethods(); $reflection = new \ReflectionClass(ApplicationDeploymentJob::class); // Set private properties $applicationProperty = $reflection->getProperty('application'); $applicationProperty->setAccessible(true); $applicationProperty->setValue($job, $mockApplication); $mainServerProperty = $reflection->getProperty('mainServer'); $mainServerProperty->setAccessible(true); $mainServerProperty->setValue($job, new Server); $pullRequestProperty = $reflection->getProperty('pull_request_id'); $pullRequestProperty->setAccessible(true); $pullRequestProperty->setValue($job, 0); // Mock generate_coolify_env_variables $job->shouldReceive('generate_coolify_env_variables') ->andReturn(collect([])); // Call the private method $method = $reflection->getMethod('generate_nixpacks_env_variables'); $method->setAccessible(true); $method->invoke($job); // Get the generated env_nixpacks_args $envArgsProperty = $reflection->getProperty('env_nixpacks_args'); $envArgsProperty->setAccessible(true); $envArgs = $envArgsProperty->getValue($job); // Verify that zero and false string values are preserved (values are now single-quote escaped) expect($envArgs)->toContain("--env 'ZERO_VALUE=0'"); expect($envArgs)->toContain("--env 'FALSE_VALUE=false'"); });