refactor: define explicit fillable attributes on all Eloquent models (#9282)

This commit is contained in:
Andras Bacsai authored and GitHub committed 2026-03-30 13:11:58 +02:00
commit 3fddc795f6
77 files changed
+688 -168

No files matched your search

@@ -25,13 +25,13 @@ beforeEach(function () {
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
StandaloneDocker::withoutEvents(function () {
$this->destination = StandaloneDocker::firstOrCreate(
['server_id' => $this->server->id, 'network' => 'coolify'],
$this->destination = $this->server->standaloneDockers()->firstOrCreate(
['network' => 'coolify'],
['uuid' => (string) new Cuid2, 'name' => 'test-docker']
);
});
$this->project = Project::create([
$this->project = Project::forceCreate([
'uuid' => (string) new Cuid2,
'name' => 'test-project',
'team_id' => $this->team->id,
+6 -3
View File
@@ -14,9 +14,10 @@ it('populates fqdn from docker_compose_domains after generate_preview_fqdn_compo
]),
]);
$preview = ApplicationPreview::create([
$preview = ApplicationPreview::forceCreate([
'application_id' => $application->id,
'pull_request_id' => 42,
'pull_request_html_url' => 'https://github.com/example/repo/pull/42',
'docker_compose_domains' => $application->docker_compose_domains,
]);
@@ -38,9 +39,10 @@ it('populates fqdn with multiple domains from multiple services', function () {
]),
]);
$preview = ApplicationPreview::create([
$preview = ApplicationPreview::forceCreate([
'application_id' => $application->id,
'pull_request_id' => 7,
'pull_request_html_url' => 'https://github.com/example/repo/pull/7',
'docker_compose_domains' => $application->docker_compose_domains,
]);
@@ -63,9 +65,10 @@ it('sets fqdn to null when no domains are configured', function () {
]),
]);
$preview = ApplicationPreview::create([
$preview = ApplicationPreview::forceCreate([
'application_id' => $application->id,
'pull_request_id' => 99,
'pull_request_html_url' => 'https://github.com/example/repo/pull/99',
'docker_compose_domains' => $application->docker_compose_domains,
]);
@@ -33,7 +33,7 @@ beforeEach(function () {
function createDatabase($context): StandalonePostgresql
{
return StandalonePostgresql::create([
return StandalonePostgresql::forceCreate([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
@@ -33,7 +33,7 @@ beforeEach(function () {
describe('PATCH /api/v1/databases', function () {
test('updates public_port_timeout on a postgresql database', function () {
$database = StandalonePostgresql::create([
$database = StandalonePostgresql::forceCreate([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
@@ -57,7 +57,7 @@ describe('PATCH /api/v1/databases', function () {
});
test('updates public_port_timeout on a redis database', function () {
$database = StandaloneRedis::create([
$database = StandaloneRedis::forceCreate([
'name' => 'test-redis',
'image' => 'redis:7',
'redis_password' => 'password',
@@ -79,7 +79,7 @@ describe('PATCH /api/v1/databases', function () {
});
test('rejects invalid public_port_timeout value', function () {
$database = StandalonePostgresql::create([
$database = StandalonePostgresql::forceCreate([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
@@ -101,7 +101,7 @@ describe('PATCH /api/v1/databases', function () {
});
test('accepts null public_port_timeout', function () {
$database = StandalonePostgresql::create([
$database = StandalonePostgresql::forceCreate([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
@@ -0,0 +1,73 @@
<?php
use App\Models\Application;
use App\Models\ApplicationSetting;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('creates application settings for internally created applications', function () {
$team = Team::factory()->create();
$project = Project::factory()->create([
'team_id' => $team->id,
]);
$environment = Environment::factory()->create([
'project_id' => $project->id,
]);
$server = Server::factory()->create([
'team_id' => $team->id,
]);
$destination = $server->standaloneDockers()->firstOrFail();
$application = Application::forceCreate([
'name' => 'internal-app',
'git_repository' => 'https://github.com/coollabsio/coolify',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
$setting = ApplicationSetting::query()
->where('application_id', $application->id)
->first();
expect($application->environment_id)->toBe($environment->id);
expect($setting)->not->toBeNull();
expect($setting?->application_id)->toBe($application->id);
});
it('creates services with protected relationship ids in trusted internal paths', function () {
$team = Team::factory()->create();
$project = Project::factory()->create([
'team_id' => $team->id,
]);
$environment = Environment::factory()->create([
'project_id' => $project->id,
]);
$server = Server::factory()->create([
'team_id' => $team->id,
]);
$destination = $server->standaloneDockers()->firstOrFail();
$service = Service::forceCreate([
'docker_compose_raw' => 'services: {}',
'environment_id' => $environment->id,
'server_id' => $server->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
'service_type' => 'test-service',
]);
expect($service->environment_id)->toBe($environment->id);
expect($service->server_id)->toBe($server->id);
expect($service->destination_id)->toBe($destination->id);
expect($service->destination_type)->toBe($destination->getMorphClass());
});
@@ -168,6 +168,58 @@ describe('mass assignment protection', function () {
expect($model->isFillable('mongo_initdb_root_username'))->toBeTrue();
});
test('standalone database models allow mass assignment of public_port_timeout', function () {
$models = [
StandalonePostgresql::class,
StandaloneRedis::class,
StandaloneMysql::class,
StandaloneMariadb::class,
StandaloneMongodb::class,
StandaloneKeydb::class,
StandaloneDragonfly::class,
StandaloneClickhouse::class,
];
foreach ($models as $modelClass) {
$model = new $modelClass;
expect($model->isFillable('public_port_timeout'))
->toBeTrue("{$modelClass} should allow mass assignment of 'public_port_timeout'");
}
});
test('standalone database models allow mass assignment of SSL fields where applicable', function () {
$sslModels = [
StandalonePostgresql::class,
StandaloneMysql::class,
StandaloneMariadb::class,
StandaloneMongodb::class,
StandaloneRedis::class,
StandaloneKeydb::class,
StandaloneDragonfly::class,
];
foreach ($sslModels as $modelClass) {
$model = new $modelClass;
expect($model->isFillable('enable_ssl'))
->toBeTrue("{$modelClass} should allow mass assignment of 'enable_ssl'");
}
// Clickhouse has no SSL columns
expect((new StandaloneClickhouse)->isFillable('enable_ssl'))->toBeFalse();
$sslModeModels = [
StandalonePostgresql::class,
StandaloneMysql::class,
StandaloneMongodb::class,
];
foreach ($sslModeModels as $modelClass) {
$model = new $modelClass;
expect($model->isFillable('ssl_mode'))
->toBeTrue("{$modelClass} should allow mass assignment of 'ssl_mode'");
}
});
test('Application fill ignores non-fillable fields', function () {
$application = new Application;
$application->fill([
+17 -16
View File
@@ -7,25 +7,26 @@ use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
uses(RefreshDatabase::class);
it('returns the correct team through the service relationship chain', function () {
$team = Team::factory()->create();
$project = Project::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
$project = Project::forceCreate([
'uuid' => (string) Str::uuid(),
'name' => 'Test Project',
'team_id' => $team->id,
]);
$environment = Environment::create([
'name' => 'test-env-'.Illuminate\Support\Str::random(8),
$environment = Environment::forceCreate([
'name' => 'test-env-'.Str::random(8),
'project_id' => $project->id,
]);
$service = Service::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
$service = Service::forceCreate([
'uuid' => (string) Str::uuid(),
'name' => 'supabase',
'environment_id' => $environment->id,
'destination_id' => 1,
@@ -33,8 +34,8 @@ it('returns the correct team through the service relationship chain', function (
'docker_compose_raw' => 'version: "3"',
]);
$serviceDatabase = ServiceDatabase::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
$serviceDatabase = ServiceDatabase::forceCreate([
'uuid' => (string) Str::uuid(),
'name' => 'supabase-db',
'service_id' => $service->id,
]);
@@ -46,19 +47,19 @@ it('returns the correct team through the service relationship chain', function (
it('returns the correct team for ServiceApplication through the service relationship chain', function () {
$team = Team::factory()->create();
$project = Project::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
$project = Project::forceCreate([
'uuid' => (string) Str::uuid(),
'name' => 'Test Project',
'team_id' => $team->id,
]);
$environment = Environment::create([
'name' => 'test-env-'.Illuminate\Support\Str::random(8),
$environment = Environment::forceCreate([
'name' => 'test-env-'.Str::random(8),
'project_id' => $project->id,
]);
$service = Service::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
$service = Service::forceCreate([
'uuid' => (string) Str::uuid(),
'name' => 'supabase',
'environment_id' => $environment->id,
'destination_id' => 1,
@@ -66,8 +67,8 @@ it('returns the correct team for ServiceApplication through the service relation
'docker_compose_raw' => 'version: "3"',
]);
$serviceApplication = ServiceApplication::create([
'uuid' => (string) Illuminate\Support\Str::uuid(),
$serviceApplication = ServiceApplication::forceCreate([
'uuid' => (string) Str::uuid(),
'name' => 'supabase-studio',
'service_id' => $service->id,
]);
+1 -1
View File
@@ -49,7 +49,7 @@ function createTestApplication($context): Application
function createTestDatabase($context): StandalonePostgresql
{
return StandalonePostgresql::create([
return StandalonePostgresql::forceCreate([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
+8 -6
View File
@@ -7,22 +7,24 @@
* These tests verify the fix for the issue where changing an image in a
* docker-compose file would create a new service instead of updating the existing one.
*/
it('ensures service parser does not include image in firstOrCreate query', function () {
it('ensures service parser does not include image in trusted service creation query', function () {
// Read the serviceParser function from parsers.php
$parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php');
// Check that firstOrCreate is called with only name and service_id
// and NOT with image parameter in the ServiceApplication presave loop
// Check that trusted creation only uses name and service_id
// and does not include image in the creation payload
expect($parsersFile)
->toContain("firstOrCreate([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);")
->not->toContain("firstOrCreate([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);");
->toContain("\$databaseFound = ServiceDatabase::where('name', \$serviceName)->where('service_id', \$resource->id)->first();")
->toContain("\$applicationFound = ServiceApplication::where('name', \$serviceName)->where('service_id', \$resource->id)->first();")
->toContain("forceCreate([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);")
->not->toContain("forceCreate([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);");
});
it('ensures service parser updates image after finding or creating service', function () {
// Read the serviceParser function from parsers.php
$parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php');
// Check that image update logic exists after firstOrCreate
// Check that image update logic exists after the trusted create/find branch
expect($parsersFile)
->toContain('// Update image if it changed')
->toContain('if ($savedService->image !== $image) {')
+3 -3
View File
@@ -77,21 +77,21 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
],
]);
Project::create([
Project::forceCreate([
'uuid' => 'project-1',
'name' => 'My first project',
'description' => 'This is a test project in development',
'team_id' => 0,
]);
Project::create([
Project::forceCreate([
'uuid' => 'project-2',
'name' => 'Production API',
'description' => 'Backend services for production',
'team_id' => 0,
]);
Project::create([
Project::forceCreate([
'uuid' => 'project-3',
'name' => 'Staging Environment',
'description' => 'Staging and QA testing',