test(factories): add missing model factories for app test suite

Enable `HasFactory` on `Environment`, `Project`, `ScheduledTask`, and
`StandaloneDocker`, and add dedicated factories for related models to
stabilize feature/unit tests.

Also bump `visus/cuid2` to `^6.0` and refresh `composer.lock` with the
resulting dependency updates.
This commit is contained in:
Andras Bacsai 2026-03-03 09:50:05 +01:00
parent e1a7c64f37
commit 7ae76ebc79
20 changed files with 752 additions and 635 deletions

View file

@ -4,6 +4,7 @@
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use OpenApi\Attributes as OA;
#[OA\Schema(
@ -21,6 +22,7 @@
class Environment extends BaseModel
{
use ClearsGlobalSearchCache;
use HasFactory;
use HasSafeStringAttribute;
protected $guarded = [];

View file

@ -4,6 +4,7 @@
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use OpenApi\Attributes as OA;
use Visus\Cuid2\Cuid2;
@ -20,6 +21,7 @@
class Project extends BaseModel
{
use ClearsGlobalSearchCache;
use HasFactory;
use HasSafeStringAttribute;
protected $guarded = [];

View file

@ -3,6 +3,7 @@
namespace App\Models;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use OpenApi\Attributes as OA;
@ -25,6 +26,7 @@
)]
class ScheduledTask extends BaseModel
{
use HasFactory;
use HasSafeStringAttribute;
protected $guarded = [];

View file

@ -4,9 +4,11 @@
use App\Jobs\ConnectProxyToNetworksJob;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class StandaloneDocker extends BaseModel
{
use HasFactory;
use HasSafeStringAttribute;
protected $guarded = [];

View file

@ -54,7 +54,7 @@
"stevebauman/purify": "^6.3.1",
"stripe/stripe-php": "^16.6.0",
"symfony/yaml": "^7.4.1",
"visus/cuid2": "^4.1.0",
"visus/cuid2": "^6.0.0",
"yosymfony/toml": "^1.0.4",
"zircote/swagger-php": "^5.8.0"
},

1195
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class EnvironmentFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->unique()->word(),
'project_id' => 1,
];
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class ProjectFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->unique()->company(),
'team_id' => 1,
];
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class ServiceFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->unique()->word(),
'destination_type' => \App\Models\StandaloneDocker::class,
'destination_id' => 1,
'environment_id' => 1,
'docker_compose_raw' => 'version: "3"',
];
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class StandaloneDockerFactory extends Factory
{
public function definition(): array
{
return [
'uuid' => fake()->uuid(),
'name' => fake()->unique()->word(),
'network' => 'coolify',
'server_id' => 1,
];
}
}

View file

@ -2,42 +2,23 @@
use App\Models\Application;
use App\Models\ApplicationSetting;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
describe('Application Rollback', function () {
beforeEach(function () {
$team = Team::factory()->create();
$project = Project::create([
'team_id' => $team->id,
'name' => 'Test Project',
'uuid' => (string) str()->uuid(),
]);
$environment = Environment::create([
'project_id' => $project->id,
'name' => 'rollback-test-env',
'uuid' => (string) str()->uuid(),
]);
$server = Server::factory()->create(['team_id' => $team->id]);
$this->application = Application::factory()->create([
'environment_id' => $environment->id,
'destination_id' => $server->id,
$this->application = new Application;
$this->application->forceFill([
'uuid' => 'test-app-uuid',
'git_commit_sha' => 'HEAD',
]);
$settings = new ApplicationSetting;
$settings->is_git_shallow_clone_enabled = false;
$settings->is_git_submodules_enabled = false;
$settings->is_git_lfs_enabled = false;
$this->application->setRelation('settings', $settings);
});
test('setGitImportSettings uses passed commit instead of application git_commit_sha', function () {
ApplicationSetting::create([
'application_id' => $this->application->id,
'is_git_shallow_clone_enabled' => false,
]);
$rollbackCommit = 'abc123def456abc123def456abc123def456abc1';
$result = $this->application->setGitImportSettings(
@ -51,10 +32,7 @@
});
test('setGitImportSettings with shallow clone fetches specific commit', function () {
ApplicationSetting::create([
'application_id' => $this->application->id,
'is_git_shallow_clone_enabled' => true,
]);
$this->application->settings->is_git_shallow_clone_enabled = true;
$rollbackCommit = 'abc123def456abc123def456abc123def456abc1';
@ -71,12 +49,7 @@
});
test('setGitImportSettings falls back to git_commit_sha when no commit passed', function () {
$this->application->update(['git_commit_sha' => 'def789abc012def789abc012def789abc012def7']);
ApplicationSetting::create([
'application_id' => $this->application->id,
'is_git_shallow_clone_enabled' => false,
]);
$this->application->git_commit_sha = 'def789abc012def789abc012def789abc012def7';
$result = $this->application->setGitImportSettings(
deployment_uuid: 'test-uuid',
@ -88,11 +61,6 @@
});
test('setGitImportSettings escapes shell metacharacters in commit parameter', function () {
ApplicationSetting::create([
'application_id' => $this->application->id,
'is_git_shallow_clone_enabled' => false,
]);
$maliciousCommit = 'abc123; rm -rf /';
$result = $this->application->setGitImportSettings(
@ -109,11 +77,6 @@
});
test('setGitImportSettings does not append checkout when commit is HEAD', function () {
ApplicationSetting::create([
'application_id' => $this->application->id,
'is_git_shallow_clone_enabled' => false,
]);
$result = $this->application->setGitImportSettings(
deployment_uuid: 'test-uuid',
git_clone_command: 'git clone',

View file

@ -2,6 +2,7 @@
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\ScheduledTask;
use App\Models\ScheduledTaskExecution;
@ -15,6 +16,9 @@
uses(RefreshDatabase::class);
beforeEach(function () {
// ApiAllowed middleware requires InstanceSettings with id=0
InstanceSettings::create(['id' => 0, 'is_api_enabled' => true]);
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
@ -25,12 +29,14 @@
$this->bearerToken = $this->token->plainTextToken;
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
$this->destination = StandaloneDocker::factory()->create(['server_id' => $this->server->id]);
// Server::booted() auto-creates a StandaloneDocker, reuse it
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
// Project::booted() auto-creates a 'production' Environment, reuse it
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
$this->environment = $this->project->environments()->first();
});
function authHeaders($bearerToken): array
function scheduledTaskAuthHeaders($bearerToken): array
{
return [
'Authorization' => 'Bearer '.$bearerToken,
@ -46,7 +52,7 @@ function authHeaders($bearerToken): array
'destination_type' => $this->destination->getMorphClass(),
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->getJson("/api/v1/applications/{$application->uuid}/scheduled-tasks");
$response->assertStatus(200);
@ -66,7 +72,7 @@ function authHeaders($bearerToken): array
'name' => 'Test Task',
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->getJson("/api/v1/applications/{$application->uuid}/scheduled-tasks");
$response->assertStatus(200);
@ -75,7 +81,7 @@ function authHeaders($bearerToken): array
});
test('returns 404 for unknown application uuid', function () {
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->getJson('/api/v1/applications/nonexistent-uuid/scheduled-tasks');
$response->assertStatus(404);
@ -90,7 +96,7 @@ function authHeaders($bearerToken): array
'destination_type' => $this->destination->getMorphClass(),
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
'name' => 'Backup',
'command' => 'php artisan backup',
@ -116,7 +122,7 @@ function authHeaders($bearerToken): array
'destination_type' => $this->destination->getMorphClass(),
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
'command' => 'echo test',
'frequency' => '* * * * *',
@ -132,7 +138,7 @@ function authHeaders($bearerToken): array
'destination_type' => $this->destination->getMorphClass(),
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
'name' => 'Test',
'command' => 'echo test',
@ -150,7 +156,7 @@ function authHeaders($bearerToken): array
'destination_type' => $this->destination->getMorphClass(),
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
'name' => 'Test',
'command' => 'echo test',
@ -168,7 +174,7 @@ function authHeaders($bearerToken): array
'destination_type' => $this->destination->getMorphClass(),
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
'name' => 'Test',
'command' => 'echo test',
@ -199,7 +205,7 @@ function authHeaders($bearerToken): array
'name' => 'Old Name',
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/{$task->uuid}", [
'name' => 'New Name',
]);
@ -215,7 +221,7 @@ function authHeaders($bearerToken): array
'destination_type' => $this->destination->getMorphClass(),
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/nonexistent", [
'name' => 'Test',
]);
@ -237,7 +243,7 @@ function authHeaders($bearerToken): array
'team_id' => $this->team->id,
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->deleteJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/{$task->uuid}");
$response->assertStatus(200);
@ -253,7 +259,7 @@ function authHeaders($bearerToken): array
'destination_type' => $this->destination->getMorphClass(),
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->deleteJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/nonexistent");
$response->assertStatus(404);
@ -279,7 +285,7 @@ function authHeaders($bearerToken): array
'message' => 'OK',
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->getJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/{$task->uuid}/executions");
$response->assertStatus(200);
@ -294,7 +300,7 @@ function authHeaders($bearerToken): array
'destination_type' => $this->destination->getMorphClass(),
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->getJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/nonexistent/executions");
$response->assertStatus(404);
@ -316,7 +322,7 @@ function authHeaders($bearerToken): array
'name' => 'Service Task',
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->getJson("/api/v1/services/{$service->uuid}/scheduled-tasks");
$response->assertStatus(200);
@ -332,7 +338,7 @@ function authHeaders($bearerToken): array
'environment_id' => $this->environment->id,
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->postJson("/api/v1/services/{$service->uuid}/scheduled-tasks", [
'name' => 'Service Backup',
'command' => 'pg_dump',
@ -356,7 +362,7 @@ function authHeaders($bearerToken): array
'team_id' => $this->team->id,
]);
$response = $this->withHeaders(authHeaders($this->bearerToken))
$response = $this->withHeaders(scheduledTaskAuthHeaders($this->bearerToken))
->deleteJson("/api/v1/services/{$service->uuid}/scheduled-tasks/{$task->uuid}");
$response->assertStatus(200);

View file

@ -3,7 +3,6 @@
use App\Models\Application;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Mockery;
/**
* Unit test to verify docker_compose_raw is properly synced to the Livewire component

View file

@ -11,7 +11,6 @@
use App\Models\Application;
use App\Models\EnvironmentVariable;
use Illuminate\Support\Collection;
use Mockery;
beforeEach(function () {
// Clean up Mockery after each test

View file

@ -1,7 +1,6 @@
<?php
use App\Models\Application;
use Mockery;
/**
* Unit tests to verify that containers without health checks are not

View file

@ -5,7 +5,6 @@
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationSetting;
use Illuminate\Support\Facades\Validator;
use Mockery;
beforeEach(function () {
Mockery::close();

View file

@ -1,7 +1,6 @@
<?php
use App\Notifications\Server\HetznerDeletionFailed;
use Mockery;
afterEach(function () {
Mockery::close();

View file

@ -7,7 +7,6 @@
use App\Models\Server;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Queue;
use Mockery;
beforeEach(function () {
Queue::fake();

View file

@ -3,7 +3,6 @@
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Database\Eloquent\Builder;
use Mockery;
it('filters servers by proxy type using whereProxyType scope', function () {
// Mock the Builder

View file

@ -2,7 +2,6 @@
use App\Models\Service;
use App\Models\ServiceApplication;
use Mockery;
it('returns required port from service template', function () {
// Mock get_service_templates() function