Files
coolify/.cursor/rules/testing-patterns.mdc

607 lines
18 KiB
Plaintext

---
description: Testing strategies with Pest PHP, Laravel Dusk, and quality assurance patterns
globs: tests/**/*.php, database/factories/*.php
alwaysApply: false
---
# Coolify Testing Architecture & Patterns
## Testing Philosophy
Coolify employs **comprehensive testing strategies** using modern PHP testing frameworks to ensure reliability of deployment operations, infrastructure management, and user interactions.
## 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);
});
```