607 lines
18 KiB
Plaintext
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);
|
|
});
|
|
```
|