fix(models): replace forceFill/forceCreate with fill/create and add fillable guards

Replace all uses of `forceFill`, `forceCreate`, and `forceFill` with their
non-force equivalents across models, actions, controllers, and Livewire
components. Add explicit `$fillable` arrays to all affected Eloquent models
to enforce mass assignment protection.

Add ModelFillableCreationTest and ModelFillableRegressionTest to verify that
model creation respects fillable constraints and prevent regressions.
This commit is contained in:
Andras Bacsai 2026-03-31 13:45:31 +02:00
parent 047aff1c82
commit 1a603a10ed
79 changed files with 1411 additions and 142 deletions

View file

@ -21,7 +21,7 @@ public function reset(User $user, array $input): void
'password' => ['required', Password::defaults(), 'confirmed'],
])->validate();
$user->forceFill([
$user->fill([
'password' => Hash::make($input['password']),
])->save();
$user->deleteAllSessions();

View file

@ -24,7 +24,7 @@ public function update(User $user, array $input): void
'current_password.current_password' => __('The provided password does not match your current password.'),
])->validateWithBag('updatePassword');
$user->forceFill([
$user->fill([
'password' => Hash::make($input['password']),
])->save();
}

View file

@ -35,7 +35,7 @@ public function update(User $user, array $input): void
) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
$user->fill([
'name' => $input['name'],
'email' => $input['email'],
])->save();
@ -49,7 +49,7 @@ public function update(User $user, array $input): void
*/
protected function updateVerifiedUser(User $user, array $input): void
{
$user->forceFill([
$user->fill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,

View file

@ -49,7 +49,7 @@ public function handle(Server $server)
}');
$found = StandaloneDocker::where('server_id', $server->id);
if ($found->count() == 0 && $server->id) {
StandaloneDocker::forceCreate([
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,

View file

@ -136,7 +136,7 @@ public function handle()
$application = Application::all()->first();
$preview = ApplicationPreview::all()->first();
if (! $preview) {
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 1,
'pull_request_html_url' => 'http://example.com',

View file

@ -258,7 +258,7 @@ public function create_project(Request $request)
], 422);
}
$project = Project::forceCreate([
$project = Project::create([
'name' => $request->name,
'description' => $request->description,
'team_id' => $teamId,

View file

@ -432,7 +432,7 @@ public function create_service(Request $request)
if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) {
data_set($servicePayload, 'connect_to_docker_network', true);
}
$service = Service::forceCreate($servicePayload);
$service = Service::create($servicePayload);
$service->name = $request->name ?? "$oneClickServiceName-".$service->uuid;
$service->description = $request->description;
if ($request->has('is_container_label_escape_enabled')) {

View file

@ -119,7 +119,7 @@ public function manual(Request $request)
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
$pr_app = ApplicationPreview::forceCreate([
$pr_app = ApplicationPreview::create([
'git_type' => 'bitbucket',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
@ -128,7 +128,7 @@ public function manual(Request $request)
]);
$pr_app->generate_preview_fqdn_compose();
} else {
$pr_app = ApplicationPreview::forceCreate([
$pr_app = ApplicationPreview::create([
'git_type' => 'bitbucket',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,

View file

@ -144,7 +144,7 @@ public function manual(Request $request)
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
$pr_app = ApplicationPreview::forceCreate([
$pr_app = ApplicationPreview::create([
'git_type' => 'gitea',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
@ -153,7 +153,7 @@ public function manual(Request $request)
]);
$pr_app->generate_preview_fqdn_compose();
} else {
$pr_app = ApplicationPreview::forceCreate([
$pr_app = ApplicationPreview::create([
'git_type' => 'gitea',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,

View file

@ -177,7 +177,7 @@ public function manual(Request $request)
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
$pr_app = ApplicationPreview::forceCreate([
$pr_app = ApplicationPreview::create([
'git_type' => 'gitlab',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
@ -186,7 +186,7 @@ public function manual(Request $request)
]);
$pr_app->generate_preview_fqdn_compose();
} else {
$pr_app = ApplicationPreview::forceCreate([
$pr_app = ApplicationPreview::create([
'git_type' => 'gitlab',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,

View file

@ -118,7 +118,7 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp
if (! $found) {
if ($application->build_pack === 'dockercompose') {
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'git_type' => 'github',
'application_id' => $application->id,
'pull_request_id' => $this->pullRequestId,
@ -127,7 +127,7 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp
]);
$preview->generate_preview_fqdn_compose();
} else {
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'git_type' => 'github',
'application_id' => $application->id,
'pull_request_id' => $this->pullRequestId,

View file

@ -441,7 +441,7 @@ public function selectExistingProject()
public function createNewProject()
{
$this->createdProject = Project::forceCreate([
$this->createdProject = Project::create([
'name' => 'My first project',
'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,

View file

@ -77,7 +77,7 @@ public function submit()
if ($found) {
throw new \Exception('Network already added to this server.');
} else {
$docker = SwarmDocker::forceCreate([
$docker = SwarmDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->selectedServer->id,
@ -88,7 +88,7 @@ public function submit()
if ($found) {
throw new \Exception('Network already added to this server.');
} else {
$docker = StandaloneDocker::forceCreate([
$docker = StandaloneDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->selectedServer->id,

View file

@ -48,7 +48,7 @@ public function submit()
$this->rateLimit(10);
$this->validate();
$firstLogin = auth()->user()->created_at == auth()->user()->updated_at;
auth()->user()->forceFill([
auth()->user()->fill([
'password' => Hash::make($this->password),
'force_password_reset' => false,
])->save();

View file

@ -30,7 +30,7 @@ public function submit()
{
try {
$this->validate();
$project = Project::forceCreate([
$project = Project::create([
'name' => $this->name,
'description' => $this->description,
'team_id' => currentTeam()->id,

View file

@ -196,7 +196,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null,
$this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && ! is_null($pull_request_html_url)) {
$found = ApplicationPreview::forceCreate([
$found = ApplicationPreview::create([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
@ -210,7 +210,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null,
$this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
$found = ApplicationPreview::forceCreate([
$found = ApplicationPreview::create([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url ?? '',
@ -262,7 +262,7 @@ public function deploy(int $pull_request_id, ?string $pull_request_html_url = nu
$this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
$found = ApplicationPreview::forceCreate([
$found = ApplicationPreview::create([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url ?? '',

View file

@ -100,7 +100,7 @@ public function clone(string $type)
if ($foundProject) {
throw new \Exception('Project with the same name already exists.');
}
$project = Project::forceCreate([
$project = Project::create([
'name' => $this->newName,
'team_id' => currentTeam()->id,
'description' => $this->project->description.' (clone)',
@ -139,7 +139,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'status' => 'exited',
'started_at' => null,
@ -188,7 +188,7 @@ public function clone(string $type)
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $newDatabase->id,
]);
@ -217,7 +217,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resource_id' => $newDatabase->id,
]);
$newStorage->save();
@ -230,7 +230,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'database_id' => $newDatabase->id,
'database_type' => $newDatabase->getMorphClass(),
@ -248,7 +248,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->forceFill($payload);
])->fill($payload);
$newEnvironmentVariable->save();
}
}
@ -259,7 +259,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination,
@ -277,7 +277,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => (string) new Cuid2,
'service_id' => $newService->id,
'team_id' => currentTeam()->id,
@ -291,7 +291,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resourceable_id' => $newService->id,
'resourceable_type' => $newService->getMorphClass(),
]);
@ -299,7 +299,7 @@ public function clone(string $type)
}
foreach ($newService->applications() as $application) {
$application->forceFill([
$application->fill([
'status' => 'exited',
])->save();
@ -317,7 +317,7 @@ public function clone(string $type)
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $application->id,
]);
@ -346,7 +346,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resource_id' => $application->id,
]);
$newStorage->save();
@ -354,7 +354,7 @@ public function clone(string $type)
}
foreach ($newService->databases() as $database) {
$database->forceFill([
$database->fill([
'status' => 'exited',
])->save();
@ -372,7 +372,7 @@ public function clone(string $type)
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $database->id,
]);
@ -401,7 +401,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resource_id' => $database->id,
]);
$newStorage->save();
@ -414,7 +414,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'database_id' => $database->id,
'database_type' => $database->getMorphClass(),

View file

@ -54,7 +54,7 @@ public function submit()
}
$destination_class = $destination->getMorphClass();
$service = Service::forceCreate([
$service = Service::create([
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,

View file

@ -133,7 +133,7 @@ public function submit()
// Determine the image tag based on whether it's a hash or regular tag
$imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag();
$application = Application::forceCreate([
$application = Application::create([
'name' => 'docker-image-'.new Cuid2,
'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify',

View file

@ -10,7 +10,7 @@ class EmptyProject extends Component
{
public function createEmptyProject()
{
$project = Project::forceCreate([
$project = Project::create([
'name' => generate_random_name(),
'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,

View file

@ -191,7 +191,7 @@ public function submit()
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
$application = Application::forceCreate([
$application = Application::create([
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
'repository_project_id' => $this->selected_repository_id,
'git_repository' => str($this->selected_repository_owner)->trim()->toString().'/'.str($this->selected_repository_repo)->trim()->toString(),

View file

@ -183,7 +183,7 @@ public function submit()
$application_init['docker_compose_location'] = $this->docker_compose_location;
$application_init['base_directory'] = $this->base_directory;
}
$application = Application::forceCreate($application_init);
$application = Application::create($application_init);
$application->settings->is_static = $this->is_static;
$application->settings->save();

View file

@ -299,7 +299,7 @@ public function submit()
$new_service['source_id'] = $this->git_source->id;
$new_service['source_type'] = $this->git_source->getMorphClass();
}
$service = Service::forceCreate($new_service);
$service = Service::create($new_service);
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
@ -346,7 +346,7 @@ public function submit()
$application_init['docker_compose_location'] = $this->docker_compose_location;
$application_init['base_directory'] = $this->base_directory;
}
$application = Application::forceCreate($application_init);
$application = Application::create($application_init);
$application->settings->is_static = $this->isStatic;
$application->settings->save();

View file

@ -52,7 +52,7 @@ public function submit()
if (! $port) {
$port = 80;
}
$application = Application::forceCreate([
$application = Application::create([
'name' => 'dockerfile-'.new Cuid2,
'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify',

View file

@ -91,7 +91,7 @@ public function mount()
if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) {
data_set($service_payload, 'connect_to_docker_network', true);
}
$service = Service::forceCreate($service_payload);
$service = Service::create($service_payload);
$service->name = "$oneClickServiceName-".$service->uuid;
$service->save();
if ($oneClickDotEnvs?->count() > 0) {

View file

@ -94,7 +94,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'status' => 'exited',
@ -143,7 +143,7 @@ public function cloneTo($destination_id)
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $new_resource->id,
]);
@ -172,7 +172,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resource_id' => $new_resource->id,
]);
$newStorage->save();
@ -185,7 +185,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'database_id' => $new_resource->id,
'database_type' => $new_resource->getMorphClass(),
@ -204,7 +204,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->forceFill($payload);
])->fill($payload);
$newEnvironmentVariable->save();
}
@ -221,7 +221,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'destination_id' => $new_destination->id,
@ -242,7 +242,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => (string) new Cuid2,
'service_id' => $new_resource->id,
'team_id' => currentTeam()->id,
@ -256,7 +256,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
]);
@ -264,7 +264,7 @@ public function cloneTo($destination_id)
}
foreach ($new_resource->applications() as $application) {
$application->forceFill([
$application->fill([
'status' => 'exited',
])->save();
@ -282,7 +282,7 @@ public function cloneTo($destination_id)
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $application->id,
]);
@ -307,7 +307,7 @@ public function cloneTo($destination_id)
}
foreach ($new_resource->databases() as $database) {
$database->forceFill([
$database->fill([
'status' => 'exited',
])->save();
@ -325,7 +325,7 @@ public function cloneTo($destination_id)
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $database->id,
]);
@ -366,7 +366,7 @@ public function moveTo($environment_id)
try {
$this->authorize('update', $this->resource);
$new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id);
$this->resource->forceFill([
$this->resource->fill([
'environment_id' => $environment_id,
])->save();
if ($this->resource->type() === 'application') {

View file

@ -42,7 +42,7 @@ public function submit()
{
try {
$this->validate();
$environment = Environment::forceCreate([
$environment = Environment::create([
'name' => $this->name,
'project_id' => $this->project->id,
'uuid' => (string) new Cuid2,

View file

@ -43,7 +43,7 @@ public function add($name)
return;
} else {
SwarmDocker::forceCreate([
SwarmDocker::create([
'name' => $this->server->name.'-'.$name,
'network' => $this->name,
'server_id' => $this->server->id,
@ -57,7 +57,7 @@ public function add($name)
return;
} else {
StandaloneDocker::forceCreate([
StandaloneDocker::create([
'name' => $this->server->name.'-'.$name,
'network' => $name,
'server_id' => $this->server->id,

View file

@ -203,6 +203,14 @@ class Application extends BaseModel
'restart_count',
'last_restart_at',
'last_restart_type',
'uuid',
'environment_id',
'destination_id',
'destination_type',
'source_id',
'source_type',
'repository_project_id',
'private_key_id',
];
protected $appends = ['server_status'];
@ -262,7 +270,7 @@ protected static function booted()
}
}
if (count($payload) > 0) {
$application->forceFill($payload);
$application->fill($payload);
}
// Buildpack switching cleanup logic
@ -299,7 +307,7 @@ protected static function booted()
}
});
static::created(function ($application) {
ApplicationSetting::forceCreate([
ApplicationSetting::create([
'application_id' => $application->id,
]);
$application->compose_parsing_version = self::$parserVersion;

View file

@ -44,6 +44,7 @@ class ApplicationDeploymentQueue extends Model
'application_id',
'deployment_uuid',
'pull_request_id',
'docker_registry_image_tag',
'force_rebuild',
'commit',
'status',

View file

@ -11,6 +11,7 @@ class ApplicationPreview extends BaseModel
use SoftDeletes;
protected $fillable = [
'uuid',
'application_id',
'pull_request_id',
'pull_request_html_url',
@ -62,7 +63,7 @@ protected static function booted()
});
static::saving(function ($preview) {
if ($preview->isDirty('status')) {
$preview->forceFill(['last_online_at' => now()]);
$preview->last_online_at = now();
}
});
}

View file

@ -29,6 +29,7 @@ class ApplicationSetting extends Model
];
protected $fillable = [
'application_id',
'is_static',
'is_git_submodules_enabled',
'is_git_lfs_enabled',

View file

@ -5,6 +5,7 @@
class CloudProviderToken extends BaseModel
{
protected $fillable = [
'team_id',
'provider',
'token',
'name',

View file

@ -28,6 +28,8 @@ class Environment extends BaseModel
protected $fillable = [
'name',
'description',
'project_id',
'uuid',
];
protected static function booted()

View file

@ -7,6 +7,8 @@
class GithubApp extends BaseModel
{
protected $fillable = [
'team_id',
'private_key_id',
'name',
'organization',
'api_url',

View file

@ -27,6 +27,8 @@ class Project extends BaseModel
protected $fillable = [
'name',
'description',
'team_id',
'uuid',
];
/**
@ -51,10 +53,10 @@ public static function ownedByCurrentTeamCached()
protected static function booted()
{
static::created(function ($project) {
ProjectSetting::forceCreate([
ProjectSetting::create([
'project_id' => $project->id,
]);
Environment::forceCreate([
Environment::create([
'name' => 'production',
'project_id' => $project->id,
'uuid' => (string) new Cuid2,

View file

@ -6,7 +6,9 @@
class ProjectSetting extends Model
{
protected $fillable = [];
protected $fillable = [
'project_id',
];
public function project()
{

View file

@ -9,6 +9,8 @@
class ScheduledDatabaseBackup extends BaseModel
{
protected $fillable = [
'uuid',
'team_id',
'description',
'enabled',
'save_s3',

View file

@ -7,6 +7,8 @@
class ScheduledDatabaseBackupExecution extends BaseModel
{
protected $fillable = [
'uuid',
'scheduled_database_backup_id',
'status',
'message',
'size',

View file

@ -30,12 +30,16 @@ class ScheduledTask extends BaseModel
use HasSafeStringAttribute;
protected $fillable = [
'uuid',
'enabled',
'name',
'command',
'frequency',
'container',
'timeout',
'team_id',
'application_id',
'service_id',
];
public static function ownedByCurrentTeamAPI(int $teamId)

View file

@ -23,6 +23,7 @@
class ScheduledTaskExecution extends BaseModel
{
protected $fillable = [
'scheduled_task_id',
'status',
'message',
'finished_at',

View file

@ -135,7 +135,7 @@ protected static function booted()
$payload['ip_previous'] = $server->getOriginal('ip');
}
}
$server->forceFill($payload);
$server->fill($payload);
});
static::saved(function ($server) {
if ($server->wasChanged('private_key_id') || $server->privateKey?->isDirty()) {
@ -265,6 +265,7 @@ public static function flushIdentityMap(): void
'detected_traefik_version',
'traefik_outdated_info',
'server_metadata',
'ip_previous',
];
use HasSafeStringAttribute;

View file

@ -54,6 +54,7 @@
class ServerSetting extends Model
{
protected $fillable = [
'server_id',
'is_swarm_manager',
'is_jump_server',
'is_build_server',

View file

@ -49,6 +49,7 @@ class Service extends BaseModel
private static $parserVersion = '5';
protected $fillable = [
'uuid',
'name',
'description',
'docker_compose_raw',
@ -58,6 +59,10 @@ class Service extends BaseModel
'config_hash',
'compose_parsing_version',
'is_container_label_escape_enabled',
'environment_id',
'server_id',
'destination_id',
'destination_type',
];
protected $appends = ['server_status', 'status'];

View file

@ -12,6 +12,7 @@ class ServiceApplication extends BaseModel
use HasFactory, SoftDeletes;
protected $fillable = [
'service_id',
'name',
'human_name',
'description',
@ -39,7 +40,7 @@ protected static function booted()
});
static::saving(function ($service) {
if ($service->isDirty('status')) {
$service->forceFill(['last_online_at' => now()]);
$service->last_online_at = now();
}
});
}

View file

@ -10,6 +10,7 @@ class ServiceDatabase extends BaseModel
use HasFactory, SoftDeletes;
protected $fillable = [
'service_id',
'name',
'human_name',
'description',
@ -44,7 +45,7 @@ protected static function booted()
});
static::saving(function ($service) {
if ($service->isDirty('status')) {
$service->forceFill(['last_online_at' => now()]);
$service->last_online_at = now();
}
});
}

View file

@ -14,6 +14,7 @@ class StandaloneClickhouse extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'clickhouse_admin_user',
@ -40,6 +41,9 @@ class StandaloneClickhouse extends BaseModel
'public_port_timeout',
'custom_docker_run_options',
'clickhouse_db',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@ -71,7 +75,7 @@ protected static function booted()
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}

View file

@ -13,6 +13,7 @@ class StandaloneDocker extends BaseModel
use HasSafeStringAttribute;
protected $fillable = [
'server_id',
'name',
'network',
];

View file

@ -14,6 +14,7 @@ class StandaloneDragonfly extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'dragonfly_password',
@ -39,6 +40,9 @@ class StandaloneDragonfly extends BaseModel
'public_port_timeout',
'enable_ssl',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@ -70,7 +74,7 @@ protected static function booted()
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}

View file

@ -14,6 +14,7 @@ class StandaloneKeydb extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'keydb_password',
@ -40,6 +41,9 @@ class StandaloneKeydb extends BaseModel
'public_port_timeout',
'enable_ssl',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];
@ -71,7 +75,7 @@ protected static function booted()
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}

View file

@ -15,6 +15,7 @@ class StandaloneMariadb extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'mariadb_root_password',
@ -43,6 +44,9 @@ class StandaloneMariadb extends BaseModel
'enable_ssl',
'is_log_drain_enabled',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@ -74,7 +78,7 @@ protected static function booted()
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}

View file

@ -14,6 +14,7 @@ class StandaloneMongodb extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'mongo_conf',
@ -43,6 +44,9 @@ class StandaloneMongodb extends BaseModel
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@ -80,7 +84,7 @@ protected static function booted()
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}

View file

@ -14,6 +14,7 @@ class StandaloneMysql extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'mysql_root_password',
@ -44,6 +45,9 @@ class StandaloneMysql extends BaseModel
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@ -76,7 +80,7 @@ protected static function booted()
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}

View file

@ -14,6 +14,7 @@ class StandalonePostgresql extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'postgres_user',
@ -46,6 +47,9 @@ class StandalonePostgresql extends BaseModel
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@ -92,7 +96,7 @@ protected static function booted()
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});
}

View file

@ -14,6 +14,7 @@ class StandaloneRedis extends BaseModel
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
'name',
'description',
'redis_conf',
@ -39,6 +40,9 @@ class StandaloneRedis extends BaseModel
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
'destination_type',
'destination_id',
'environment_id',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
@ -69,7 +73,7 @@ protected static function booted()
});
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
$database->last_online_at = now();
}
});

View file

@ -7,6 +7,7 @@
class Subscription extends Model
{
protected $fillable = [
'team_id',
'stripe_invoice_paid',
'stripe_subscription_id',
'stripe_customer_id',

View file

@ -7,6 +7,7 @@
class SwarmDocker extends BaseModel
{
protected $fillable = [
'server_id',
'name',
'network',
];

View file

@ -10,6 +10,7 @@ class Tag extends BaseModel
protected $fillable = [
'name',
'team_id',
];
protected function customizeName($value)

View file

@ -49,6 +49,9 @@ class User extends Authenticatable implements SendsEmail
'password',
'force_password_reset',
'marketing_emails',
'pending_email',
'email_change_code',
'email_change_code_expires_at',
];
protected $hidden = [
@ -409,7 +412,7 @@ public function requestEmailChange(string $newEmail): void
$expiryMinutes = config('constants.email_change.verification_code_expiry_minutes', 10);
$expiresAt = Carbon::now()->addMinutes($expiryMinutes);
$this->forceFill([
$this->fill([
'pending_email' => $newEmail,
'email_change_code' => $code,
'email_change_code_expires_at' => $expiresAt,

View file

@ -214,7 +214,7 @@ function clone_application(Application $source, $destination, array $overrides =
'updated_at',
'additional_servers_count',
'additional_networks_count',
])->forceFill(array_merge([
])->fill(array_merge([
'uuid' => $uuid,
'name' => $name,
'fqdn' => $url,
@ -237,7 +237,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'application_id' => $newApplication->id,
]);
$newApplicationSettings->save();
@ -257,7 +257,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => (string) new Cuid2,
'application_id' => $newApplication->id,
'team_id' => currentTeam()->id,
@ -272,7 +272,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'uuid' => (string) new Cuid2,
'application_id' => $newApplication->id,
'status' => 'exited',
@ -304,7 +304,7 @@ function clone_application(Application $source, $destination, array $overrides =
'created_at',
'updated_at',
'uuid',
])->forceFill([
])->fill([
'name' => $newName,
'resource_id' => $newApplication->id,
]);
@ -340,7 +340,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resource_id' => $newApplication->id,
]);
$newStorage->save();
@ -354,7 +354,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resourceable_id' => $newApplication->id,
'resourceable_type' => $newApplication->getMorphClass(),
'is_preview' => false,
@ -371,7 +371,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
])->forceFill([
])->fill([
'resourceable_id' => $newApplication->id,
'resourceable_type' => $newApplication->getMorphClass(),
'is_preview' => true,

View file

@ -1597,7 +1597,7 @@ function serviceParser(Service $resource): Collection
if ($databaseFound) {
$savedService = $databaseFound;
} else {
$savedService = ServiceDatabase::forceCreate([
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'service_id' => $resource->id,
]);
@ -1607,7 +1607,7 @@ function serviceParser(Service $resource): Collection
if ($applicationFound) {
$savedService = $applicationFound;
} else {
$savedService = ServiceApplication::forceCreate([
$savedService = ServiceApplication::create([
'name' => $serviceName,
'service_id' => $resource->id,
]);

View file

@ -1919,7 +1919,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// Create new serviceApplication or serviceDatabase
if ($isDatabase) {
if ($isNew) {
$savedService = ServiceDatabase::forceCreate([
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
@ -1930,7 +1930,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'service_id' => $resource->id,
])->first();
if (is_null($savedService)) {
$savedService = ServiceDatabase::forceCreate([
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
@ -1939,7 +1939,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
} else {
if ($isNew) {
$savedService = ServiceApplication::forceCreate([
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
@ -1950,7 +1950,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'service_id' => $resource->id,
])->first();
if (is_null($savedService)) {
$savedService = ServiceApplication::forceCreate([
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,

View file

@ -31,7 +31,7 @@
);
});
$this->project = Project::forceCreate([
$this->project = Project::create([
'uuid' => (string) new Cuid2,
'name' => 'test-project',
'team_id' => $this->team->id,

View file

@ -6,7 +6,7 @@
describe('Application Rollback', function () {
beforeEach(function () {
$this->application = new Application;
$this->application->forceFill([
$this->application->fill([
'uuid' => 'test-app-uuid',
'git_commit_sha' => 'HEAD',
]);

View file

@ -31,7 +31,7 @@
'redirect' => 'both',
]);
$this->application->settings->forceFill([
$this->application->settings->fill([
'is_container_label_readonly_enabled' => false,
])->save();
@ -92,7 +92,7 @@
});
test('cloning application reassigns settings to the cloned application', function () {
$this->application->settings->forceFill([
$this->application->settings->fill([
'is_static' => true,
'is_spa' => true,
'is_build_server_enabled' => true,
@ -118,7 +118,7 @@
});
test('cloning application reassigns scheduled tasks and previews to the cloned application', function () {
$scheduledTask = ScheduledTask::forceCreate([
$scheduledTask = ScheduledTask::create([
'uuid' => 'scheduled-task-original',
'application_id' => $this->application->id,
'team_id' => $this->team->id,
@ -129,7 +129,7 @@
'timeout' => 120,
]);
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'uuid' => 'preview-original',
'application_id' => $this->application->id,
'pull_request_id' => 123,

View file

@ -14,7 +14,7 @@
]),
]);
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 42,
'pull_request_html_url' => 'https://github.com/example/repo/pull/42',
@ -39,7 +39,7 @@
]),
]);
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 7,
'pull_request_html_url' => 'https://github.com/example/repo/pull/7',
@ -65,7 +65,7 @@
]),
]);
$preview = ApplicationPreview::forceCreate([
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 99,
'pull_request_html_url' => 'https://github.com/example/repo/pull/99',

View file

@ -33,7 +33,7 @@
function createDatabase($context): StandalonePostgresql
{
return StandalonePostgresql::forceCreate([
return StandalonePostgresql::create([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',

View file

@ -33,7 +33,7 @@
describe('PATCH /api/v1/databases', function () {
test('updates public_port_timeout on a postgresql database', function () {
$database = StandalonePostgresql::forceCreate([
$database = StandalonePostgresql::create([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
@ -57,7 +57,7 @@
});
test('updates public_port_timeout on a redis database', function () {
$database = StandaloneRedis::forceCreate([
$database = StandaloneRedis::create([
'name' => 'test-redis',
'image' => 'redis:7',
'redis_password' => 'password',
@ -79,7 +79,7 @@
});
test('rejects invalid public_port_timeout value', function () {
$database = StandalonePostgresql::forceCreate([
$database = StandalonePostgresql::create([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',
@ -101,7 +101,7 @@
});
test('accepts null public_port_timeout', function () {
$database = StandalonePostgresql::forceCreate([
$database = StandalonePostgresql::create([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',

View file

@ -52,7 +52,7 @@
$project = Project::factory()->create(['team_id' => $this->team->id]);
$environment = Environment::factory()->create(['project_id' => $project->id]);
$database = StandaloneMysql::forceCreate([
$database = StandaloneMysql::create([
'name' => 'test-mysql',
'image' => 'mysql:8',
'mysql_root_password' => 'password',
@ -70,7 +70,7 @@
$component = Livewire::test(MysqlGeneral::class, ['database' => $database])
->assertDontSee('Database should be stopped to change this settings.');
$database->forceFill(['status' => 'running:healthy'])->save();
$database->fill(['status' => 'running:healthy'])->save();
$component->call('refresh')
->assertSee('Database should be stopped to change this settings.');

View file

@ -69,7 +69,7 @@
describe('GetLogs Livewire action validation', function () {
test('getLogs rejects invalid container name', function () {
// Make server functional by setting settings directly
$this->server->settings->forceFill([
$this->server->settings->fill([
'is_reachable' => true,
'is_usable' => true,
'force_disabled' => false,
@ -100,7 +100,7 @@
});
test('downloadAllLogs returns empty for invalid container name', function () {
$this->server->settings->forceFill([
$this->server->settings->fill([
'is_reachable' => true,
'is_usable' => true,
'force_disabled' => false,

View file

@ -31,7 +31,7 @@
'team_id' => $this->team->id,
]);
$this->githubApp = GithubApp::forceCreate([
$this->githubApp = GithubApp::create([
'name' => 'Test GitHub App',
'api_url' => 'https://api.github.com',
'html_url' => 'https://github.com',

View file

@ -24,7 +24,7 @@
]);
$destination = $server->standaloneDockers()->firstOrFail();
$application = Application::forceCreate([
$application = Application::create([
'name' => 'internal-app',
'git_repository' => 'https://github.com/coollabsio/coolify',
'git_branch' => 'main',
@ -57,7 +57,7 @@
]);
$destination = $server->standaloneDockers()->firstOrFail();
$service = Service::forceCreate([
$service = Service::create([
'docker_compose_raw' => 'services: {}',
'environment_id' => $environment->id,
'server_id' => $server->id,

File diff suppressed because it is too large Load diff

View file

@ -14,18 +14,18 @@
it('returns the correct team through the service relationship chain', function () {
$team = Team::factory()->create();
$project = Project::forceCreate([
$project = Project::create([
'uuid' => (string) Str::uuid(),
'name' => 'Test Project',
'team_id' => $team->id,
]);
$environment = Environment::forceCreate([
$environment = Environment::create([
'name' => 'test-env-'.Str::random(8),
'project_id' => $project->id,
]);
$service = Service::forceCreate([
$service = Service::create([
'uuid' => (string) Str::uuid(),
'name' => 'supabase',
'environment_id' => $environment->id,
@ -34,7 +34,7 @@
'docker_compose_raw' => 'version: "3"',
]);
$serviceDatabase = ServiceDatabase::forceCreate([
$serviceDatabase = ServiceDatabase::create([
'uuid' => (string) Str::uuid(),
'name' => 'supabase-db',
'service_id' => $service->id,
@ -47,18 +47,18 @@
it('returns the correct team for ServiceApplication through the service relationship chain', function () {
$team = Team::factory()->create();
$project = Project::forceCreate([
$project = Project::create([
'uuid' => (string) Str::uuid(),
'name' => 'Test Project',
'team_id' => $team->id,
]);
$environment = Environment::forceCreate([
$environment = Environment::create([
'name' => 'test-env-'.Str::random(8),
'project_id' => $project->id,
]);
$service = Service::forceCreate([
$service = Service::create([
'uuid' => (string) Str::uuid(),
'name' => 'supabase',
'environment_id' => $environment->id,
@ -67,7 +67,7 @@
'docker_compose_raw' => 'version: "3"',
]);
$serviceApplication = ServiceApplication::forceCreate([
$serviceApplication = ServiceApplication::create([
'uuid' => (string) Str::uuid(),
'name' => 'supabase-studio',
'service_id' => $service->id,

View file

@ -49,7 +49,7 @@ function createTestApplication($context): Application
function createTestDatabase($context): StandalonePostgresql
{
return StandalonePostgresql::forceCreate([
return StandalonePostgresql::create([
'name' => 'test-postgres',
'image' => 'postgres:15-alpine',
'postgres_user' => 'postgres',

View file

@ -1,12 +1,14 @@
<?php
use App\Models\Application;
use App\Models\ApplicationSetting;
/**
* Security tests for git ref validation (GHSA-mw5w-2vvh-mgf4).
*
* Ensures that git_commit_sha and related inputs are validated
* to prevent OS command injection via shell metacharacters.
*/
describe('validateGitRef', function () {
test('accepts valid hex commit SHAs', function () {
expect(validateGitRef('abc123def456'))->toBe('abc123def456');
@ -93,31 +95,31 @@
describe('executeInDocker git log escaping', function () {
test('git log command escapes commit SHA to prevent injection', function () {
$maliciousCommit = "HEAD'; id; #";
$command = "cd /workdir && git log -1 ".escapeshellarg($maliciousCommit).' --pretty=%B';
$command = 'cd /workdir && git log -1 '.escapeshellarg($maliciousCommit).' --pretty=%B';
$result = executeInDocker('test-container', $command);
// The malicious payload must not be able to break out of quoting
expect($result)->not->toContain("id;");
expect($result)->not->toContain('id;');
expect($result)->toContain("'HEAD'\\''");
});
});
describe('buildGitCheckoutCommand escaping', function () {
test('checkout command escapes target to prevent injection', function () {
$app = new \App\Models\Application;
$app->forceFill(['uuid' => 'test-uuid']);
$app = new Application;
$app->fill(['uuid' => 'test-uuid']);
$settings = new \App\Models\ApplicationSetting;
$settings = new ApplicationSetting;
$settings->is_git_submodules_enabled = false;
$app->setRelation('settings', $settings);
$method = new \ReflectionMethod($app, 'buildGitCheckoutCommand');
$method = new ReflectionMethod($app, 'buildGitCheckoutCommand');
$result = $method->invoke($app, 'abc123');
expect($result)->toContain("git checkout 'abc123'");
$result = $method->invoke($app, "abc'; id; #");
expect($result)->not->toContain("id;");
expect($result)->not->toContain('id;');
expect($result)->toContain("git checkout 'abc'");
});
});

View file

@ -0,0 +1,76 @@
<?php
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\ApplicationSetting;
use App\Models\CloudProviderToken;
use App\Models\Environment;
use App\Models\GithubApp;
use App\Models\Project;
use App\Models\ProjectSetting;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\ScheduledTask;
use App\Models\ScheduledTaskExecution;
use App\Models\Server;
use App\Models\ServerSetting;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDocker;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Models\Subscription;
use App\Models\SwarmDocker;
use App\Models\Tag;
use App\Models\User;
it('keeps required mass-assignment attributes fillable for internal create flows', function (string $modelClass, array $expectedAttributes) {
$model = new $modelClass;
expect($model->getFillable())->toContain(...$expectedAttributes);
})->with([
// Relationship/ownership keys
[CloudProviderToken::class, ['team_id']],
[Tag::class, ['team_id']],
[Subscription::class, ['team_id']],
[ScheduledTaskExecution::class, ['scheduled_task_id']],
[ScheduledDatabaseBackupExecution::class, ['uuid', 'scheduled_database_backup_id']],
[ScheduledDatabaseBackup::class, ['uuid', 'team_id']],
[ScheduledTask::class, ['uuid', 'team_id', 'application_id', 'service_id']],
[ServiceDatabase::class, ['service_id']],
[ServiceApplication::class, ['service_id']],
[ApplicationDeploymentQueue::class, ['docker_registry_image_tag']],
[Project::class, ['team_id', 'uuid']],
[Environment::class, ['project_id', 'uuid']],
[ProjectSetting::class, ['project_id']],
[ApplicationSetting::class, ['application_id']],
[ServerSetting::class, ['server_id']],
[SwarmDocker::class, ['server_id']],
[StandaloneDocker::class, ['server_id']],
[User::class, ['pending_email', 'email_change_code', 'email_change_code_expires_at']],
[Server::class, ['ip_previous']],
[GithubApp::class, ['team_id', 'private_key_id']],
// Application/Service resource keys (including uuid for clone flows)
[Application::class, ['uuid', 'environment_id', 'destination_id', 'destination_type', 'source_id', 'source_type', 'repository_project_id', 'private_key_id']],
[ApplicationPreview::class, ['uuid', 'application_id']],
[Service::class, ['uuid', 'environment_id', 'server_id', 'destination_id', 'destination_type']],
// Standalone database resource keys (including uuid for clone flows)
[StandalonePostgresql::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneMysql::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneMariadb::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneMongodb::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneRedis::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneKeydb::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneDragonfly::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
[StandaloneClickhouse::class, ['uuid', 'destination_type', 'destination_id', 'environment_id']],
]);

View file

@ -16,8 +16,8 @@
expect($parsersFile)
->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 ]);");
->toContain("create([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);")
->not->toContain("create([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);");
});
it('ensures service parser updates image after finding or creating service', function () {
@ -41,8 +41,8 @@
// The new code checks for null within the else block and creates only if needed
expect($sharedFile)
->toContain('if (is_null($savedService)) {')
->toContain('$savedService = ServiceDatabase::forceCreate([')
->toContain('$savedService = ServiceApplication::forceCreate([');
->toContain('$savedService = ServiceDatabase::create([')
->toContain('$savedService = ServiceApplication::create([');
});
it('verifies image update logic is present in parseDockerComposeFile', function () {

View file

@ -77,21 +77,21 @@
],
]);
Project::forceCreate([
Project::create([
'uuid' => 'project-1',
'name' => 'My first project',
'description' => 'This is a test project in development',
'team_id' => 0,
]);
Project::forceCreate([
Project::create([
'uuid' => 'project-2',
'name' => 'Production API',
'description' => 'Backend services for production',
'team_id' => 0,
]);
Project::forceCreate([
Project::create([
'uuid' => 'project-3',
'name' => 'Staging Environment',
'description' => 'Staging and QA testing',