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); }); });