# Coolify Testing Architecture & Patterns

> **Cross-Reference**: These detailed testing patterns align with the testing guidelines in **[CLAUDE.md](mdc:CLAUDE.md)**. Both documents share the same core principles about Docker execution and mocking preferences.

## Testing Philosophy

Coolify employs **comprehensive testing strategies** using modern PHP testing frameworks to ensure reliability of deployment operations, infrastructure management, and user interactions.

### Test Execution Rules

**CRITICAL**: Tests are categorized by database dependency:

#### Unit Tests (`tests/Unit/`)
- **MUST NOT** use database connections
- **MUST** use mocking for models and external dependencies
- **CAN** run outside Docker: `./vendor/bin/pest tests/Unit`
- Purpose: Test isolated logic, helper functions, and business rules

#### Feature Tests (`tests/Feature/`)
- **MAY** use database connections (factories, migrations, models)
- **MUST** run inside Docker container: `docker exec coolify php artisan test`
- **MUST** use `RefreshDatabase` trait if touching database
- Purpose: Test API endpoints, workflows, and integration scenarios

**Rule of thumb**: If your test needs `Server::factory()->create()` or any database operation, it's a Feature test and MUST run in Docker.

### Prefer Mocking Over Database

When writing tests, always prefer mocking over real database operations:

```php
// ❌ BAD: Unit test using database
it('extracts custom commands', function () {
    $server = Server::factory()->create(['ip' => '1.2.3.4']);
    $commands = extract_custom_proxy_commands($server, $yaml);
    expect($commands)->toBeArray();
});

// ✅ GOOD: Unit test using mocking
it('extracts custom commands', function () {
    $server = Mockery::mock('App\Models\Server');
    $server->shouldReceive('proxyType')->andReturn('traefik');
    $commands = extract_custom_proxy_commands($server, $yaml);
    expect($commands)->toBeArray();
});
```

**Design principles for testable code:**
- Use dependency injection instead of global state
- Create interfaces for external dependencies (SSH, Docker, etc.)
- Separate business logic from data persistence
- Make functions accept interfaces instead of concrete models when possible

## Testing Framework Stack

### Core Testing Tools
- **Pest PHP 3.8+** - Primary testing framework with expressive syntax
- **Laravel Dusk** - Browser automation and end-to-end testing
- **PHPUnit** - Underlying unit testing framework
- **Mockery** - Mocking and stubbing for isolated tests

### Testing Configuration
- **[tests/Pest.php](mdc:tests/Pest.php)** - Pest configuration and global setup (1.5KB, 45 lines)
- **[tests/TestCase.php](mdc:tests/TestCase.php)** - Base test case class (163B, 11 lines)
- **[tests/CreatesApplication.php](mdc:tests/CreatesApplication.php)** - Application factory trait (375B, 22 lines)
- **[tests/DuskTestCase.php](mdc:tests/DuskTestCase.php)** - Browser testing setup (1.4KB, 58 lines)

## Test Directory Structure

### Test Organization
- **[tests/Feature/](mdc:tests/Feature)** - Feature and integration tests
- **[tests/Unit/](mdc:tests/Unit)** - Unit tests for isolated components
- **[tests/Browser/](mdc:tests/Browser)** - Laravel Dusk browser tests
- **[tests/Traits/](mdc:tests/Traits)** - Shared testing utilities

## Unit Testing Patterns

### Model Testing
```php
// Testing Eloquent models
test('application model has correct relationships', function () {
    $application = Application::factory()->create();
    
    expect($application->server)->toBeInstanceOf(Server::class);
    expect($application->environment)->toBeInstanceOf(Environment::class);
    expect($application->deployments)->toBeInstanceOf(Collection::class);
});

test('application can generate deployment configuration', function () {
    $application = Application::factory()->create([
        'name' => 'test-app',
        'git_repository' => 'https://github.com/user/repo.git'
    ]);
    
    $config = $application->generateDockerCompose();
    
    expect($config)->toContain('test-app');
    expect($config)->toContain('image:');
    expect($config)->toContain('networks:');
});
```

### Service Layer Testing
```php
// Testing service classes
test('configuration generator creates valid docker compose', function () {
    $generator = new ConfigurationGenerator();
    $application = Application::factory()->create();
    
    $compose = $generator->generateDockerCompose($application);
    
    expect($compose)->toBeString();
    expect(yaml_parse($compose))->toBeArray();
    expect($compose)->toContain('version: "3.8"');
});

test('docker image parser validates image names', function () {
    $parser = new DockerImageParser();
    
    expect($parser->isValid('nginx:latest'))->toBeTrue();
    expect($parser->isValid('invalid-image-name'))->toBeFalse();
    expect($parser->parse('nginx:1.21'))->toEqual([
        'registry' => 'docker.io',
        'namespace' => 'library',
        'repository' => 'nginx',
        'tag' => '1.21'
    ]);
});
```

### Action Testing
```php
// Testing Laravel Actions
test('deploy application action creates deployment queue', function () {
    $application = Application::factory()->create();
    $action = new DeployApplicationAction();
    
    $deployment = $action->handle($application);
    
    expect($deployment)->toBeInstanceOf(ApplicationDeploymentQueue::class);
    expect($deployment->status)->toBe('queued');
    expect($deployment->application_id)->toBe($application->id);
});

test('server validation action checks ssh connectivity', function () {
    $server = Server::factory()->create([
        'ip' => '192.168.1.100',
        'port' => 22
    ]);
    
    $action = new ValidateServerAction();
    
    // Mock SSH connection
    $this->mock(SshConnection::class, function ($mock) {
        $mock->shouldReceive('connect')->andReturn(true);
        $mock->shouldReceive('execute')->with('docker --version')->andReturn('Docker version 20.10.0');
    });
    
    $result = $action->handle($server);
    
    expect($result['ssh_connection'])->toBeTrue();
    expect($result['docker_installed'])->toBeTrue();
});
```

## Feature Testing Patterns

### API Testing
```php
// Testing API endpoints
test('authenticated user can list applications', function () {
    $user = User::factory()->create();
    $team = Team::factory()->create();
    $user->teams()->attach($team);
    
    $applications = Application::factory(3)->create([
        'team_id' => $team->id
    ]);
    
    $response = $this->actingAs($user)
        ->getJson('/api/v1/applications');
    
    $response->assertStatus(200)
        ->assertJsonCount(3, 'data')
        ->assertJsonStructure([
            'data' => [
                '*' => ['id', 'name', 'fqdn', 'status', 'created_at']
            ]
        ]);
});

test('user cannot access applications from other teams', function () {
    $user = User::factory()->create();
    $otherTeam = Team::factory()->create();
    
    $application = Application::factory()->create([
        'team_id' => $otherTeam->id
    ]);
    
    $response = $this->actingAs($user)
        ->getJson("/api/v1/applications/{$application->id}");
    
    $response->assertStatus(403);
});
```

### Deployment Testing
```php
// Testing deployment workflows
test('application deployment creates docker containers', function () {
    $application = Application::factory()->create([
        'git_repository' => 'https://github.com/laravel/laravel.git',
        'git_branch' => 'main'
    ]);
    
    // Mock Docker operations
    $this->mock(DockerService::class, function ($mock) {
        $mock->shouldReceive('buildImage')->andReturn('app:latest');
        $mock->shouldReceive('createContainer')->andReturn('container_id');
        $mock->shouldReceive('startContainer')->andReturn(true);
    });
    
    $deployment = $application->deploy();
    
    expect($deployment->status)->toBe('queued');
    
    // Process the deployment job
    $this->artisan('queue:work --once');
    
    $deployment->refresh();
    expect($deployment->status)->toBe('success');
});

test('failed deployment triggers rollback', function () {
    $application = Application::factory()->create();
    
    // Mock failed deployment
    $this->mock(DockerService::class, function ($mock) {
        $mock->shouldReceive('buildImage')->andThrow(new DeploymentException('Build failed'));
    });
    
    $deployment = $application->deploy();
    
    $this->artisan('queue:work --once');
    
    $deployment->refresh();
    expect($deployment->status)->toBe('failed');
    expect($deployment->error_message)->toContain('Build failed');
});
```

### Webhook Testing
```php
// Testing webhook endpoints
test('github webhook triggers deployment', function () {
    $application = Application::factory()->create([
        'git_repository' => 'https://github.com/user/repo.git',
        'git_branch' => 'main'
    ]);
    
    $payload = [
        'ref' => 'refs/heads/main',
        'repository' => [
            'clone_url' => 'https://github.com/user/repo.git'
        ],
        'head_commit' => [
            'id' => 'abc123',
            'message' => 'Update application'
        ]
    ];
    
    $response = $this->postJson("/webhooks/github/{$application->id}", $payload);
    
    $response->assertStatus(200);
    
    expect($application->deployments()->count())->toBe(1);
    expect($application->deployments()->first()->commit_sha)->toBe('abc123');
});

test('webhook validates payload signature', function () {
    $application = Application::factory()->create();
    
    $payload = ['invalid' => 'payload'];
    
    $response = $this->postJson("/webhooks/github/{$application->id}", $payload);
    
    $response->assertStatus(400);
});
```

## Browser Testing (Laravel Dusk)

### End-to-End Testing
```php
// Testing complete user workflows
test('user can create and deploy application', function () {
    $user = User::factory()->create();
    $server = Server::factory()->create(['team_id' => $user->currentTeam->id]);
    
    $this->browse(function (Browser $browser) use ($user, $server) {
        $browser->loginAs($user)
            ->visit('/applications/create')
            ->type('name', 'Test Application')
            ->type('git_repository', 'https://github.com/laravel/laravel.git')
            ->type('git_branch', 'main')
            ->select('server_id', $server->id)
            ->press('Create Application')
            ->assertPathIs('/applications/*')
            ->assertSee('Test Application')
            ->press('Deploy')
            ->waitForText('Deployment started', 10)
            ->assertSee('Deployment started');
    });
});

test('user can monitor deployment logs in real-time', function () {
    $user = User::factory()->create();
    $application = Application::factory()->create(['team_id' => $user->currentTeam->id]);
    
    $this->browse(function (Browser $browser) use ($user, $application) {
        $browser->loginAs($user)
            ->visit("/applications/{$application->id}")
            ->press('Deploy')
            ->waitForText('Deployment started')
            ->click('@logs-tab')
            ->waitFor('@deployment-logs')
            ->assertSee('Building Docker image')
            ->waitForText('Deployment completed', 30);
    });
});
```

### UI Component Testing
```php
// Testing Livewire components
test('server status component updates in real-time', function () {
    $user = User::factory()->create();
    $server = Server::factory()->create(['team_id' => $user->currentTeam->id]);
    
    $this->browse(function (Browser $browser) use ($user, $server) {
        $browser->loginAs($user)
            ->visit("/servers/{$server->id}")
            ->assertSee('Status: Online')
            ->waitFor('@server-metrics')
            ->assertSee('CPU Usage')
            ->assertSee('Memory Usage')
            ->assertSee('Disk Usage');
        
        // Simulate server going offline
        $server->update(['status' => 'offline']);
        
        $browser->waitForText('Status: Offline', 5)
            ->assertSee('Status: Offline');
    });
});
```

## Database Testing Patterns

### Migration Testing
```php
// Testing database migrations
test('applications table has correct structure', function () {
    expect(Schema::hasTable('applications'))->toBeTrue();
    expect(Schema::hasColumns('applications', [
        'id', 'name', 'fqdn', 'git_repository', 'git_branch',
        'server_id', 'environment_id', 'created_at', 'updated_at'
    ]))->toBeTrue();
});

test('foreign key constraints are properly set', function () {
    $application = Application::factory()->create();
    
    expect($application->server)->toBeInstanceOf(Server::class);
    expect($application->environment)->toBeInstanceOf(Environment::class);
    
    // Test cascade deletion
    $application->server->delete();
    expect(Application::find($application->id))->toBeNull();
});
```

### Factory Testing
```php
// Testing model factories
test('application factory creates valid models', function () {
    $application = Application::factory()->create();
    
    expect($application->name)->toBeString();
    expect($application->git_repository)->toStartWith('https://');
    expect($application->server_id)->toBeInt();
    expect($application->environment_id)->toBeInt();
});

test('application factory can create with custom attributes', function () {
    $application = Application::factory()->create([
        'name' => 'Custom App',
        'git_branch' => 'develop'
    ]);
    
    expect($application->name)->toBe('Custom App');
    expect($application->git_branch)->toBe('develop');
});
```

## Queue Testing

### Job Testing
```php
// Testing background jobs
test('deploy application job processes successfully', function () {
    $application = Application::factory()->create();
    $deployment = ApplicationDeploymentQueue::factory()->create([
        'application_id' => $application->id,
        'status' => 'queued'
    ]);
    
    $job = new DeployApplicationJob($deployment);
    
    // Mock external dependencies
    $this->mock(DockerService::class, function ($mock) {
        $mock->shouldReceive('buildImage')->andReturn('app:latest');
        $mock->shouldReceive('deployContainer')->andReturn(true);
    });
    
    $job->handle();
    
    $deployment->refresh();
    expect($deployment->status)->toBe('success');
});

test('failed job is retried with exponential backoff', function () {
    $application = Application::factory()->create();
    $deployment = ApplicationDeploymentQueue::factory()->create([
        'application_id' => $application->id
    ]);
    
    $job = new DeployApplicationJob($deployment);
    
    // Mock failure
    $this->mock(DockerService::class, function ($mock) {
        $mock->shouldReceive('buildImage')->andThrow(new Exception('Network error'));
    });
    
    expect(fn() => $job->handle())->toThrow(Exception::class);
    
    // Job should be retried
    expect($job->tries)->toBe(3);
    expect($job->backoff())->toBe([1, 5, 10]);
});
```

## Security Testing

### Authentication Testing
```php
// Testing authentication and authorization
test('unauthenticated users cannot access protected routes', function () {
    $response = $this->get('/dashboard');
    $response->assertRedirect('/login');
});

test('users can only access their team resources', function () {
    $user1 = User::factory()->create();
    $user2 = User::factory()->create();
    
    $team1 = Team::factory()->create();
    $team2 = Team::factory()->create();
    
    $user1->teams()->attach($team1);
    $user2->teams()->attach($team2);
    
    $application = Application::factory()->create(['team_id' => $team1->id]);
    
    $response = $this->actingAs($user2)
        ->get("/applications/{$application->id}");
    
    $response->assertStatus(403);
});
```

### Input Validation Testing
```php
// Testing input validation and sanitization
test('application creation validates required fields', function () {
    $user = User::factory()->create();
    
    $response = $this->actingAs($user)
        ->postJson('/api/v1/applications', []);
    
    $response->assertStatus(422)
        ->assertJsonValidationErrors(['name', 'git_repository', 'server_id']);
});

test('malicious input is properly sanitized', function () {
    $user = User::factory()->create();
    
    $response = $this->actingAs($user)
        ->postJson('/api/v1/applications', [
            'name' => '<script>alert("xss")</script>',
            'git_repository' => 'javascript:alert("xss")',
            'server_id' => 'invalid'
        ]);
    
    $response->assertStatus(422);
});
```

## Performance Testing

### Load Testing
```php
// Testing application performance under load
test('application list endpoint handles concurrent requests', function () {
    $user = User::factory()->create();
    $applications = Application::factory(100)->create(['team_id' => $user->currentTeam->id]);
    
    $startTime = microtime(true);
    
    $response = $this->actingAs($user)
        ->getJson('/api/v1/applications');
    
    $endTime = microtime(true);
    $responseTime = ($endTime - $startTime) * 1000; // Convert to milliseconds
    
    $response->assertStatus(200);
    expect($responseTime)->toBeLessThan(500); // Should respond within 500ms
});
```

### Memory Usage Testing
```php
// Testing memory efficiency
test('deployment process does not exceed memory limits', function () {
    $initialMemory = memory_get_usage();
    
    $application = Application::factory()->create();
    $deployment = $application->deploy();
    
    // Process deployment
    $this->artisan('queue:work --once');
    
    $finalMemory = memory_get_usage();
    $memoryIncrease = $finalMemory - $initialMemory;
    
    expect($memoryIncrease)->toBeLessThan(50 * 1024 * 1024); // Less than 50MB
});
```

## Test Utilities and Helpers

### Custom Assertions
```php
// Custom test assertions
expect()->extend('toBeValidDockerCompose', function () {
    $yaml = yaml_parse($this->value);
    
    return $yaml !== false &&
           isset($yaml['version']) &&
           isset($yaml['services']) &&
           is_array($yaml['services']);
});

expect()->extend('toHaveValidSshConnection', function () {
    $server = $this->value;
    
    try {
        $connection = new SshConnection($server);
        return $connection->test();
    } catch (Exception $e) {
        return false;
    }
});
```

### Test Traits
```php
// Shared testing functionality
trait CreatesTestServers
{
    protected function createTestServer(array $attributes = []): Server
    {
        return Server::factory()->create(array_merge([
            'name' => 'Test Server',
            'ip' => '127.0.0.1',
            'port' => 22,
            'team_id' => $this->user->currentTeam->id
        ], $attributes));
    }
}

trait MocksDockerOperations
{
    protected function mockDockerService(): void
    {
        $this->mock(DockerService::class, function ($mock) {
            $mock->shouldReceive('buildImage')->andReturn('test:latest');
            $mock->shouldReceive('createContainer')->andReturn('container_123');
            $mock->shouldReceive('startContainer')->andReturn(true);
            $mock->shouldReceive('stopContainer')->andReturn(true);
        });
    }
}
```

## Continuous Integration Testing

### GitHub Actions Integration
```yaml
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: password
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v3
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.4
      - name: Install dependencies
        run: composer install
      - name: Run tests
        run: ./vendor/bin/pest
```

### Test Coverage
```php
// Generate test coverage reports
test('application has adequate test coverage', function () {
    $coverage = $this->getCoverageData();
    
    expect($coverage['application'])->toBeGreaterThan(80);
    expect($coverage['models'])->toBeGreaterThan(90);
    expect($coverage['actions'])->toBeGreaterThan(85);
});
```
