mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-29 18:12:53 +08:00
- Stop the Redis username accessor from creating REDIS_USERNAME on read - Keep source values when cloning applications (no duplicate NIXPACKS_NODE_VERSION) - Match exact keys when adding default database environment variables - Write content files before database containers start - Resolve the database proxy upstream through Docker DNS on each connection - Remove existing duplicate REDIS_USERNAME and NIXPACKS_NODE_VERSION rows
292 lines
11 KiB
PHP
292 lines
11 KiB
PHP
<?php
|
|
|
|
use App\Models\Application;
|
|
use App\Models\Environment;
|
|
use App\Models\EnvironmentVariable;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\Project;
|
|
use App\Models\Server;
|
|
use App\Models\Service;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
InstanceSettings::forceCreate(['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->token = $this->user->createToken('test-token', ['*']);
|
|
$this->bearerToken = $this->token->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]);
|
|
});
|
|
|
|
describe('PATCH /api/v1/applications/{uuid}/envs/bulk', function () {
|
|
test('creates environment variables with comments', function () {
|
|
$application = Application::factory()->create([
|
|
'environment_id' => $this->environment->id,
|
|
'destination_id' => $this->destination->id,
|
|
'destination_type' => $this->destination->getMorphClass(),
|
|
]);
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
|
'Content-Type' => 'application/json',
|
|
])->patchJson("/api/v1/applications/{$application->uuid}/envs/bulk", [
|
|
'data' => [
|
|
[
|
|
'key' => 'DB_HOST',
|
|
'value' => 'localhost',
|
|
'comment' => 'Database host for production',
|
|
],
|
|
[
|
|
'key' => 'DB_PORT',
|
|
'value' => '5432',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response->assertStatus(201);
|
|
|
|
$envWithComment = EnvironmentVariable::where('key', 'DB_HOST')
|
|
->where('resourceable_id', $application->id)
|
|
->where('is_preview', false)
|
|
->first();
|
|
|
|
$envWithoutComment = EnvironmentVariable::where('key', 'DB_PORT')
|
|
->where('resourceable_id', $application->id)
|
|
->where('is_preview', false)
|
|
->first();
|
|
|
|
expect($envWithComment->comment)->toBe('Database host for production');
|
|
expect($envWithoutComment->comment)->toBeNull();
|
|
});
|
|
|
|
test('updates existing environment variable comment', function () {
|
|
$application = Application::factory()->create([
|
|
'environment_id' => $this->environment->id,
|
|
'destination_id' => $this->destination->id,
|
|
'destination_type' => $this->destination->getMorphClass(),
|
|
]);
|
|
|
|
EnvironmentVariable::create([
|
|
'key' => 'API_KEY',
|
|
'value' => 'old-key',
|
|
'comment' => 'Old comment',
|
|
'resourceable_type' => Application::class,
|
|
'resourceable_id' => $application->id,
|
|
'is_preview' => false,
|
|
]);
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
|
'Content-Type' => 'application/json',
|
|
])->patchJson("/api/v1/applications/{$application->uuid}/envs/bulk", [
|
|
'data' => [
|
|
[
|
|
'key' => 'API_KEY',
|
|
'value' => 'new-key',
|
|
'comment' => 'Updated comment',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response->assertStatus(201);
|
|
|
|
$env = EnvironmentVariable::where('key', 'API_KEY')
|
|
->where('resourceable_id', $application->id)
|
|
->where('is_preview', false)
|
|
->first();
|
|
|
|
expect($env->value)->toBe('new-key');
|
|
expect($env->comment)->toBe('Updated comment');
|
|
});
|
|
|
|
test('preserves existing comment when not provided in bulk update', function () {
|
|
$application = Application::factory()->create([
|
|
'environment_id' => $this->environment->id,
|
|
'destination_id' => $this->destination->id,
|
|
'destination_type' => $this->destination->getMorphClass(),
|
|
]);
|
|
|
|
EnvironmentVariable::create([
|
|
'key' => 'SECRET',
|
|
'value' => 'old-secret',
|
|
'comment' => 'Keep this comment',
|
|
'resourceable_type' => Application::class,
|
|
'resourceable_id' => $application->id,
|
|
'is_preview' => false,
|
|
]);
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
|
'Content-Type' => 'application/json',
|
|
])->patchJson("/api/v1/applications/{$application->uuid}/envs/bulk", [
|
|
'data' => [
|
|
[
|
|
'key' => 'SECRET',
|
|
'value' => 'new-secret',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response->assertStatus(201);
|
|
|
|
$env = EnvironmentVariable::where('key', 'SECRET')
|
|
->where('resourceable_id', $application->id)
|
|
->where('is_preview', false)
|
|
->first();
|
|
|
|
expect($env->value)->toBe('new-secret');
|
|
expect($env->comment)->toBe('Keep this comment');
|
|
});
|
|
|
|
test('rejects comment exceeding 256 characters', function () {
|
|
$application = Application::factory()->create([
|
|
'environment_id' => $this->environment->id,
|
|
'destination_id' => $this->destination->id,
|
|
'destination_type' => $this->destination->getMorphClass(),
|
|
]);
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
|
'Content-Type' => 'application/json',
|
|
])->patchJson("/api/v1/applications/{$application->uuid}/envs/bulk", [
|
|
'data' => [
|
|
[
|
|
'key' => 'TEST_VAR',
|
|
'value' => 'value',
|
|
'comment' => str_repeat('a', 257),
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response->assertStatus(422);
|
|
});
|
|
|
|
test('keeps one row per key and preview flag when the same key is sent more than once', function () {
|
|
$application = Application::factory()->create([
|
|
'environment_id' => $this->environment->id,
|
|
'destination_id' => $this->destination->id,
|
|
'destination_type' => $this->destination->getMorphClass(),
|
|
]);
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
|
'Content-Type' => 'application/json',
|
|
])->patchJson("/api/v1/applications/{$application->uuid}/envs/bulk", [
|
|
'data' => [
|
|
['key' => 'DUPLICATED', 'value' => 'first'],
|
|
['key' => 'DUPLICATED', 'value' => 'second'],
|
|
['key' => 'PREVIEW_ONLY', 'value' => 'preview-b', 'is_preview' => true],
|
|
['key' => 'SHARED', 'value' => 'production-a'],
|
|
['key' => 'SHARED', 'value' => 'preview-a', 'is_preview' => true],
|
|
],
|
|
]);
|
|
|
|
$response->assertStatus(201);
|
|
|
|
$rows = EnvironmentVariable::where('resourceable_type', Application::class)
|
|
->where('resourceable_id', $application->id)
|
|
->whereIn('key', ['DUPLICATED', 'PREVIEW_ONLY', 'SHARED'])
|
|
->get();
|
|
|
|
$counts = $rows->countBy(fn ($env) => $env->key.'|'.($env->is_preview ? 'preview' : 'production'))->all();
|
|
ksort($counts);
|
|
|
|
expect($counts)->toBe([
|
|
'DUPLICATED|preview' => 1,
|
|
'DUPLICATED|production' => 1,
|
|
'PREVIEW_ONLY|preview' => 1,
|
|
'SHARED|preview' => 1,
|
|
'SHARED|production' => 1,
|
|
]);
|
|
|
|
$valueOf = fn (string $key, bool $isPreview) => $rows
|
|
->first(fn ($env) => $env->key === $key && $env->is_preview === $isPreview)
|
|
->value;
|
|
|
|
expect($valueOf('DUPLICATED', false))->toBe('second')
|
|
->and($valueOf('SHARED', false))->toBe('production-a')
|
|
->and($valueOf('SHARED', true))->toBe('preview-a');
|
|
});
|
|
});
|
|
|
|
describe('PATCH /api/v1/services/{uuid}/envs/bulk', function () {
|
|
test('creates environment variables with comments', function () {
|
|
$service = Service::factory()->create([
|
|
'server_id' => $this->server->id,
|
|
'destination_id' => $this->destination->id,
|
|
'destination_type' => $this->destination->getMorphClass(),
|
|
'environment_id' => $this->environment->id,
|
|
]);
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
|
'Content-Type' => 'application/json',
|
|
])->patchJson("/api/v1/services/{$service->uuid}/envs/bulk", [
|
|
'data' => [
|
|
[
|
|
'key' => 'REDIS_HOST',
|
|
'value' => 'redis',
|
|
'comment' => 'Redis cache host',
|
|
],
|
|
[
|
|
'key' => 'REDIS_PORT',
|
|
'value' => '6379',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response->assertStatus(201);
|
|
|
|
$envWithComment = EnvironmentVariable::where('key', 'REDIS_HOST')
|
|
->where('resourceable_id', $service->id)
|
|
->where('resourceable_type', Service::class)
|
|
->first();
|
|
|
|
$envWithoutComment = EnvironmentVariable::where('key', 'REDIS_PORT')
|
|
->where('resourceable_id', $service->id)
|
|
->where('resourceable_type', Service::class)
|
|
->first();
|
|
|
|
expect($envWithComment->comment)->toBe('Redis cache host');
|
|
expect($envWithoutComment->comment)->toBeNull();
|
|
});
|
|
|
|
test('rejects comment exceeding 256 characters', function () {
|
|
$service = Service::factory()->create([
|
|
'server_id' => $this->server->id,
|
|
'destination_id' => $this->destination->id,
|
|
'destination_type' => $this->destination->getMorphClass(),
|
|
'environment_id' => $this->environment->id,
|
|
]);
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$this->bearerToken,
|
|
'Content-Type' => 'application/json',
|
|
])->patchJson("/api/v1/services/{$service->uuid}/envs/bulk", [
|
|
'data' => [
|
|
[
|
|
'key' => 'TEST_VAR',
|
|
'value' => 'value',
|
|
'comment' => str_repeat('a', 257),
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response->assertStatus(422);
|
|
});
|
|
});
|