Files
coolify/tests/Feature/Api/ApplicationSettingsApiTest.php
T
Andras Bacsai 1e42284db0 feat(proxy): override the Traefik certificate resolver per resource
Applications and service applications can now use the server resolver,
no resolver ("none"), or a custom resolver name. This allows servers
that mix Let's Encrypt domains with domains that use their own
certificates, such as Cloudflare Origin CA. The setting is available in
the UI and the API.

Closes #12138
2026-10-11 19:01:27 +02:00

678 lines
28 KiB
PHP

<?php
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
uses(RefreshDatabase::class);
beforeEach(function () {
config(['app.maintenance.driver' => 'file']);
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
session(['currentTeam' => $this->team]);
$this->bearerToken = $this->user->createToken('application-settings-api-test', ['*'])->plainTextToken;
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
$this->application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => $this->destination->getMorphClass(),
]);
});
function applicationSettingsApiHeaders(string $bearerToken): array
{
return [
'Authorization' => 'Bearer '.$bearerToken,
'Content-Type' => 'application/json',
];
}
function recommendedApplicationSettingsPayload(): array
{
return [
'is_git_submodules_enabled' => false,
'is_git_lfs_enabled' => false,
'is_git_shallow_clone_enabled' => false,
'disable_build_cache' => true,
'inject_build_args_to_dockerfile' => false,
'include_source_commit_in_build' => true,
'is_env_sorting_enabled' => true,
'is_pr_deployments_public_enabled' => true,
'stop_grace_period' => 45,
'docker_images_to_keep' => 7,
'is_gzip_enabled' => false,
'is_stripprefix_enabled' => false,
'is_raw_compose_deployment_enabled' => true,
'is_log_drain_enabled' => true,
'is_gpu_enabled' => true,
'gpu_driver' => 'nvidia',
'gpu_count' => '1',
'gpu_device_ids' => '0',
'gpu_options' => null,
'is_consistent_container_name_enabled' => true,
'custom_internal_name' => 'my-app-internal',
];
}
test('GET /api/v1/applications/{uuid} includes settings without internal metadata', function () {
$this->application->settings->update(recommendedApplicationSettingsPayload());
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->getJson("/api/v1/applications/{$this->application->uuid}")
->assertOk()
->assertJsonPath('settings.disable_build_cache', true)
->assertJsonPath('settings.stop_grace_period', 45)
->assertJsonMissingPath('settings.id')
->assertJsonMissingPath('settings.application_id')
->assertJsonMissingPath('settings.created_at')
->assertJsonMissingPath('settings.updated_at');
});
test('PATCH /api/v1/applications/{uuid} updates application settings', function () {
$this->application->update(['build_pack' => 'dockercompose']);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", recommendedApplicationSettingsPayload())
->assertOk();
$settings = $this->application->fresh()->settings;
foreach (recommendedApplicationSettingsPayload() as $field => $value) {
expect($settings->{$field})->toBe($value);
}
});
test('application creation accepts application settings', function () {
Queue::fake();
$response = $this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/public', array_merge([
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'git_repository' => 'https://gitlab.com/coolify/application-settings-test',
'git_branch' => 'main',
'build_pack' => 'dockercompose',
'autogenerate_domain' => false,
], recommendedApplicationSettingsPayload()))
->assertCreated();
$settings = Application::where('uuid', $response->json('uuid'))->firstOrFail()->settings;
foreach (recommendedApplicationSettingsPayload() as $field => $value) {
expect($settings->{$field})->toBe($value);
}
});
test('proxy settings regenerate managed labels', function () {
$this->application->settings->update([
'is_container_label_readonly_enabled' => true,
'is_gzip_enabled' => true,
'is_stripprefix_enabled' => true,
]);
$this->application->update(['custom_labels' => base64_encode('sentinel-label=true')]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'is_gzip_enabled' => false,
'is_stripprefix_enabled' => false,
])
->assertOk();
expect(base64_decode($this->application->fresh()->custom_labels))->not->toContain('sentinel-label=true');
});
test('changing a domain port regenerates managed labels with the requested port', function () {
$this->application->settings->update(['is_container_label_readonly_enabled' => true]);
$this->application->update([
'fqdn' => 'https://app.example.com',
'ports_exposes' => '80',
'domain_port_overrides' => [
'https://app.example.com' => 3000,
],
]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'domains' => 'https://app.example.com:8080',
])
->assertOk();
$application = $this->application->fresh();
$labels = $application->parseContainerLabels();
expect($application->fqdn)->toBe('https://app.example.com')
->and($application->domain_port_overrides)->toBe([
'https://app.example.com' => 8080,
])
->and($labels)->toContain('loadbalancer.server.port=8080')
->and($labels)->not->toContain('loadbalancer.server.port=3000');
});
test('compose domain ports are stored as overrides when updating through the API', function () {
$this->application->update([
'build_pack' => 'dockercompose',
'docker_compose_raw' => "services:\n api:\n image: nginx\n frontend:\n image: nginx\n",
'docker_compose_domains' => json_encode([
'api' => ['domain' => 'https://api.example.com'],
'frontend' => ['domain' => 'https://app.example.com'],
]),
]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'docker_compose_domains' => [
['name' => 'api', 'domain' => 'https://api.example.com'],
['name' => 'frontend', 'domain' => 'https://app.example.com:80'],
],
])
->assertOk();
$application = $this->application->fresh();
$domains = json_decode($application->docker_compose_domains, true);
expect(data_get($domains, 'frontend.domain'))->toBe('https://app.example.com')
->and($application->domain_port_overrides)->toBe([
'https://app.example.com' => 80,
]);
});
test('compose domain ports are stored as overrides when creating through the API', function () {
Queue::fake();
$response = $this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/public', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'git_repository' => 'https://gitlab.com/coolify/compose-domain-port-test',
'git_branch' => 'main',
'build_pack' => 'dockercompose',
'autogenerate_domain' => false,
'docker_compose_domains' => [
['name' => 'frontend', 'domain' => 'https://app.example.com:80'],
],
])
->assertCreated();
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
$domains = json_decode($application->docker_compose_domains, true);
expect(data_get($domains, 'frontend.domain'))->toBe('https://app.example.com')
->and($application->domain_port_overrides)->toBe([
'https://app.example.com' => 80,
]);
});
test('http basic auth updates regenerate managed labels', function () {
$this->application->settings->update(['is_container_label_readonly_enabled' => true]);
$this->application->update([
'fqdn' => 'https://app.example.com',
'is_http_basic_auth_enabled' => false,
'http_basic_auth_username' => null,
'http_basic_auth_password' => null,
'custom_labels' => base64_encode('sentinel-label=true'),
]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'is_http_basic_auth_enabled' => true,
'http_basic_auth_username' => 'api-user',
'http_basic_auth_password' => 'api-password',
])
->assertOk();
$application = $this->application->fresh();
$labels = $application->parseContainerLabels();
expect((bool) $application->is_http_basic_auth_enabled)->toBeTrue()
->and($application->http_basic_auth_username)->toBe('api-user')
->and($application->http_basic_auth_password)->toBe('api-password')
->and($labels)->toContain('basicauth')
->and($labels)->toContain('api-user')
->and($labels)->not->toContain('sentinel-label=true');
});
test('http basic auth updates preserve user-managed labels', function () {
$this->application->settings->update(['is_container_label_readonly_enabled' => false]);
$this->application->update([
'fqdn' => 'https://app.example.com',
'custom_labels' => base64_encode('sentinel-label=true'),
]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'is_http_basic_auth_enabled' => true,
'http_basic_auth_username' => 'api-user',
'http_basic_auth_password' => 'api-password',
])
->assertOk();
expect(base64_decode($this->application->fresh()->custom_labels))->toBe('sentinel-label=true');
});
test('nested base64 custom labels are stored in canonical form', function () {
$labels = "traefik.enable=true\ntraefik.http.routers.web.rule=Host(`example.com`)";
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'custom_labels' => base64_encode(base64_encode($labels)),
])
->assertOk();
expect($this->application->fresh()->custom_labels)->toBe(base64_encode($labels));
});
test('deployment parsing repairs historically nested custom labels', function () {
$labels = "traefik.enable=true\ntraefik.http.routers.web.rule=Host(`example.com`)";
$this->application->update(['custom_labels' => base64_encode(base64_encode($labels))]);
expect($this->application->parseContainerLabels())->toBe($labels)
->and($this->application->fresh()->custom_labels)->toBe(base64_encode($labels));
});
test('deployment parsing keeps commas in raw custom label values', function () {
$labels = "traefik.enable=true\ntraefik.http.routers.web.middlewares=gzip,redirect-to-https";
$this->application->update(['custom_labels' => $labels]);
expect($this->application->parseContainerLabels())->toBe($labels)
->and($this->application->fresh()->custom_labels)->toBe(base64_encode($labels));
});
test('rejects invalid boolean application settings', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'disable_build_cache' => 'not-a-boolean',
])
->assertUnprocessable()
->assertJsonValidationErrors('disable_build_cache');
});
test('validates stop grace period bounds', function (int $stopGracePeriod) {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'stop_grace_period' => $stopGracePeriod,
])
->assertUnprocessable()
->assertJsonValidationErrors('stop_grace_period');
})->with([
'below minimum' => 0,
'above maximum' => 3601,
]);
test('validates Docker image retention bounds', function (int $dockerImagesToKeep) {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'docker_images_to_keep' => $dockerImagesToKeep,
])
->assertUnprocessable()
->assertJsonValidationErrors('docker_images_to_keep');
})->with([
'below minimum' => -1,
'above maximum' => 101,
]);
test('stop grace period can be reset to null', function () {
$this->application->settings->update(['stop_grace_period' => 45]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'stop_grace_period' => null,
])
->assertOk();
expect($this->application->fresh()->settings->stop_grace_period)->toBeNull();
});
test('raw compose deployment can only be enabled for Docker Compose applications', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'is_raw_compose_deployment_enabled' => true,
])
->assertUnprocessable()
->assertJsonValidationErrors('is_raw_compose_deployment_enabled');
});
function advancedApplicationSettingsPayload(): array
{
return [
'is_log_drain_enabled' => true,
'is_gpu_enabled' => true,
'gpu_driver' => 'nvidia',
'gpu_count' => '1',
'gpu_device_ids' => '0',
'gpu_options' => 'capabilities=compute,utility',
'is_consistent_container_name_enabled' => true,
'custom_internal_name' => 'my-app-container',
];
}
test('PATCH /api/v1/applications/{uuid} updates advanced application settings', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", advancedApplicationSettingsPayload())
->assertOk();
$settings = $this->application->fresh()->settings;
foreach (advancedApplicationSettingsPayload() as $field => $value) {
expect($settings->{$field})->toBe($value);
}
});
test('PATCH /api/v1/applications/{uuid} accepts Docker-compatible custom internal names', function (string $name) {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'custom_internal_name' => $name,
'is_consistent_container_name_enabled' => true,
])
->assertOk();
expect($this->application->fresh()->settings->custom_internal_name)->toBe($name);
})->with([
'hyphens' => 'my-app-container',
'uppercase, underscores, and dots' => 'My_App.v2',
]);
test('PATCH /api/v1/applications/{uuid} rejects unsafe custom internal names', function (string $name) {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'custom_internal_name' => $name,
])
->assertUnprocessable()
->assertJsonValidationErrors('custom_internal_name');
expect($this->application->fresh()->settings->custom_internal_name)->toBeNull();
})->with([
'semicolon' => 'app;id',
'command substitution' => 'app$(id)',
'backticks' => 'app`id`',
'single quote' => "app'id",
'double quote' => 'app"id',
'space' => 'app name',
'newline' => "app\nid",
'option-like prefix' => '--help',
'pipe' => 'app|id',
'ampersand' => 'app&id',
]);
test('application creation rejects an unsafe custom internal name', function () {
Queue::fake();
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/public', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'git_repository' => 'https://gitlab.com/coolify/custom-name-test',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'custom_internal_name' => 'app$(id)',
])
->assertUnprocessable()
->assertJsonValidationErrors('custom_internal_name');
});
test('PATCH /api/v1/applications/{uuid} updates preview_url_template and max_restart_count', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'preview_url_template' => '{{pr_id}}.preview.example.com',
'max_restart_count' => 5,
])
->assertOk();
$application = $this->application->fresh();
expect($application->preview_url_template)->toBe('{{pr_id}}.preview.example.com')
->and($application->max_restart_count)->toBe(5);
});
test('PATCH /api/v1/applications/{uuid} clears ports_exposes with null or an empty string', function (mixed $portsExposes) {
$this->application->update(['ports_exposes' => '3000,8080']);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'ports_exposes' => $portsExposes,
])
->assertOk();
expect($this->application->fresh()->ports_exposes)->toBeNull();
})->with([
'null' => null,
'empty string' => '',
]);
test('PATCH /api/v1/applications/{uuid} rejects invalid exposed ports', function (string $portsExposes) {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'ports_exposes' => $portsExposes,
])
->assertUnprocessable()
->assertJsonValidationErrors('ports_exposes');
})->with([
'not numeric' => '80,abc',
'zero' => '0',
'above TCP range' => '65536',
]);
test('GET /api/v1/applications/{uuid} includes advanced settings', function () {
$this->application->settings->update(advancedApplicationSettingsPayload());
$this->application->update([
'preview_url_template' => '{{pr_id}}.preview.example.com',
'max_restart_count' => 3,
]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->getJson("/api/v1/applications/{$this->application->uuid}")
->assertOk()
->assertJsonPath('settings.is_log_drain_enabled', true)
->assertJsonPath('settings.is_gpu_enabled', true)
->assertJsonPath('settings.gpu_driver', 'nvidia')
->assertJsonPath('settings.custom_internal_name', 'my-app-container')
->assertJsonPath('settings.is_consistent_container_name_enabled', true)
->assertJsonPath('preview_url_template', '{{pr_id}}.preview.example.com')
->assertJsonPath('max_restart_count', 3);
});
test('application creation accepts advanced application settings', function () {
Queue::fake();
$response = $this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/public', array_merge([
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'git_repository' => 'https://gitlab.com/coolify/advanced-settings-test',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'autogenerate_domain' => false,
'preview_url_template' => '{{pr_id}}.create.example.com',
'max_restart_count' => 7,
], advancedApplicationSettingsPayload()))
->assertCreated();
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
$settings = $application->settings;
foreach (advancedApplicationSettingsPayload() as $field => $value) {
expect($settings->{$field})->toBe($value);
}
expect($application->preview_url_template)->toBe('{{pr_id}}.create.example.com')
->and($application->max_restart_count)->toBe(7);
});
test('rejects invalid max_restart_count', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'max_restart_count' => -1,
])
->assertUnprocessable()
->assertJsonValidationErrors('max_restart_count');
});
test('rejects invalid gpu boolean settings', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'is_gpu_enabled' => 'not-a-boolean',
])
->assertUnprocessable()
->assertJsonValidationErrors('is_gpu_enabled');
});
test('rejects swarm fields on application update', function (string $field, mixed $value) {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
$field => $value,
])
->assertUnprocessable()
->assertJsonValidationErrors($field);
})->with([
'swarm_replicas' => ['swarm_replicas', 3],
'swarm_placement_constraints' => ['swarm_placement_constraints', 'node.role==worker'],
'is_swarm_only_worker_nodes' => ['is_swarm_only_worker_nodes', true],
]);
test('PATCH /api/v1/applications/{uuid} saves a slugged container name prefix', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", ['custom_container_name_prefix' => 'My API'])
->assertOk();
expect($this->application->fresh()->settings->custom_container_name_prefix)->toBe('my-api');
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->getJson("/api/v1/applications/{$this->application->uuid}")
->assertOk()
->assertJsonPath('settings.custom_container_name_prefix', 'my-api');
});
test('PATCH /api/v1/applications/{uuid} rejects a container name prefix that is in use', function () {
$otherApplication = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => $this->destination->getMorphClass(),
]);
$otherApplication->settings->update(['custom_container_name_prefix' => 'shared-prefix']);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", ['custom_container_name_prefix' => 'shared-prefix'])
->assertUnprocessable()
->assertJsonValidationErrors('custom_container_name_prefix');
expect($this->application->fresh()->settings->custom_container_name_prefix)->toBeNull();
});
test('application creation rejects a non-string container name prefix with 422', function () {
Queue::fake();
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/public', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'git_repository' => 'https://gitlab.com/coolify/prefix-test',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'custom_container_name_prefix' => ['nested' => 'value'],
])
->assertUnprocessable()
->assertJsonValidationErrors('custom_container_name_prefix');
expect(Application::query()->where('git_repository', 'like', '%prefix-test%')->exists())->toBeFalse();
});
test('PATCH /api/v1/applications/{uuid} rejects a non-string container name prefix with 422', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", ['custom_container_name_prefix' => ['a', 'b']])
->assertUnprocessable()
->assertJsonValidationErrors('custom_container_name_prefix');
expect($this->application->fresh()->settings->custom_container_name_prefix)->toBeNull();
});
test('PATCH /api/v1/applications/{uuid} sets the Traefik certificate resolver', function (?string $resolver) {
$this->application->settings->update(['traefik_certificate_resolver' => 'previous']);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", ['traefik_certificate_resolver' => $resolver])
->assertOk();
expect($this->application->fresh()->settings->traefik_certificate_resolver)->toBe($resolver);
})->with([
'custom resolver' => ['cloudflare'],
'no resolver' => ['none'],
'server setting' => [null],
]);
test('PATCH /api/v1/applications/{uuid} rejects an unsafe Traefik certificate resolver', function (mixed $resolver) {
$this->application->settings->update(['traefik_certificate_resolver' => 'previous']);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", ['traefik_certificate_resolver' => $resolver])
->assertUnprocessable()
->assertJsonValidationErrors('traefik_certificate_resolver');
expect($this->application->fresh()->settings->traefik_certificate_resolver)->toBe('previous');
})->with([
'label injection' => ['a.b=c'],
'whitespace' => ['lets encrypt'],
'non-string' => [['cloudflare']],
]);
test('application creation accepts a Traefik certificate resolver', function () {
Queue::fake();
$response = $this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/public', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'git_repository' => 'https://gitlab.com/coolify/resolver-test',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'traefik_certificate_resolver' => 'none',
])
->assertCreated();
expect(Application::where('uuid', $response->json('uuid'))->firstOrFail()->settings->traefik_certificate_resolver)->toBe('none');
});
test('application creation rejects an unsafe Traefik certificate resolver', function () {
Queue::fake();
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/public', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'git_repository' => 'https://gitlab.com/coolify/resolver-test',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'traefik_certificate_resolver' => 'a.b=c',
])
->assertUnprocessable()
->assertJsonValidationErrors('traefik_certificate_resolver');
expect(Application::query()->where('git_repository', 'like', '%resolver-test%')->exists())->toBeFalse();
});