# Testing Standards & Guidelines

## Overview
This document defines the testing standards for the Taskco SaaS multi-app system. All tests must adhere to these requirements to ensure CI/CD compatibility, reliability, and maintainability.

## Core Principles

### 1. CI/CD Safe Tests
- Tests must be fully deterministic - no random data unless explicitly seeded
- Tests must be repeatable and idempotent - running multiple times produces same results
- Tests must not require manual setup or external services
- Tests must not depend on running Docker containers or specific ports
- Tests must not assume localhost URLs or network availability

### 2. Use Laravel Testing Tools
- Prefer Feature tests over Unit tests for API endpoints
- Use `TestCase` and Laravel's built-in assertions
- Use `Queue::fake()` for job dispatching tests
- Use `Event::fake()` for event handling tests
- Use `Cache::fake()` or direct cache manipulation for caching tests
- Use `Http::fake()` or `Mockery` for external HTTP calls
- Never use `curl`, `Guzzle`, or raw HTTP requests in tests

### 3. Database Handling
- Use `RefreshDatabase` trait for database tests
- Never create real RSA keys at runtime - use pre-generated fixtures
- Use in-memory SQLite or test databases where possible
- All database changes are automatically rolled back after each test

### 4. Security & SSL
- Never disable SSL verification
- Never use real production keys or secrets
- Use test fixtures for encryption/JWT keys
- Mock or fake webhook calls to external services

### 5. Code Style
- No `echo`, `var_dump`, `dd()`, or manual output
- Tests must fail loudly with clear assertion messages
- Use PHPDoc comments to explain SCENARIO and WHY for each test
- Follow PSR-12 coding standards
- Use strict types (`declare(strict_types=1)`)

---

## Test File Structure

```php
<?php

declare(strict_types=1);

namespace Tests\Feature\[Module];

use Tests\TestCase;

/**
 * [Feature/Component] Feature Tests
 *
 * Brief description of what this test suite covers.
 *
 * Requirements tested:
 * - Requirement 1
 * - Requirement 2
 * - Requirement 3
 */
class [ClassName]Test extends TestCase
{
    /**
     * SCENARIO: [What is being tested]
     *
     * WHY: [Why this test exists - business/technical reason]
     */
    public function it_[descriptive_name](): void
    {
        // Arrange - Set up test data

        // Act - Execute the code being tested

        // Assert - Verify expected behavior
        $response->assertStatus(200);
    }
}
```

---

## Common Test Patterns

### API Endpoint Tests

```php
public function it_validates_required_fields(): void
{
    // SCENARIO: API should return 422 when required fields are missing

    $response = $this->postJson('/api/endpoint', []);

    $response->assertStatus(422)
        ->assertJsonPath('status', 'ERROR')
        ->assertJsonPath('data.errors.field_name');
}
```

### Authentication Tests

```php
public function it_requires_jwt_authentication(): void
{
    // Use pre-generated test keys (never generate at runtime)
    $response = $this->postJson('/api/protected', $data);

    $response->assertStatus(401);
}
```

### Job Dispatch Tests

```php
public function it_dispatches_correct_job(): void
{
    Queue::fake();  // Always use fake for jobs

    $response = $this->postJson('/api/endpoint', $data);

    Queue::assertPushed(JobClass::class, function ($job) use ($data) {
        return $job->property === $data['field'];
    });
}
```

### Status/State Tests

```php
public function it_returns_processing_status(): void
{
    // Mock state in cache
    Cache::put('job:status:123', [
        'status' => 'processing',
        'current_step' => 'Creating database',
    ], now()->addHours(24));

    $response = $this->getJson('/api/jobs/123/status');

    $response->assertStatus(200)
        ->assertJsonPath('data.status', 'processing')
        ->assertJsonPath('data.current_step', 'Creating database');
}
```

---

## Fixture Management

### Test Keys Location
```
tests/Fixtures/keys/
├── test_jwt_private.pem    # RSA 2048 private key
├── test_jwt_public.pem     # RSA 2048 public key
├── test_enc_private.pem    # Encryption private key
└── test_enc_public.pem     # Encryption public key
```

### Generating Test Keys (One-time)
```bash
# Generate RSA keys for JWT
openssl genrsa -out tests/Fixtures/keys/test_jwt_private.pem 2048
openssl rsa -in tests/Fixtures/keys/test_jwt_private.pem -pubout -out tests/Fixtures/keys/test_jwt_public.pem

# Generate encryption keys (if needed)
openssl genrsa -out tests/Fixtures/keys/test_enc_private.pem 2048
openssl rsa -in tests/Fixtures/keys/test_enc_private.pem -pubout -out tests/Fixtures/keys/test_enc_public.pem
```

### Using Fixtures in Tests

```php
private const TEST_PRIVATE_KEY_PATH = __DIR__ . '/../Fixtures/keys/test_jwt_private.pem';

private function getPrivateKey(): string
{
    $key = file_get_contents(self::TEST_PRIVATE_KEY_PATH);
    if ($key === false) {
        throw new \RuntimeException('Test key not found');
    }
    return $key;
}
```

---

## Test Data Guidelines

### Use Fixed/Deterministic Data
```php
// ✅ GOOD - Fixed values
$payload = [
    'tenant_uid' => 'tnt-test-123abc456',
    'company_name' => 'Test Company',
];

// ❌ BAD - Random values (unless seeded)
$payload = [
    'tenant_uid' => 'tnt-' . bin2hex(random_bytes(8)),
    'company_name' => 'Company ' . rand(1, 1000),
];
```

### Use Descriptive Test Names
```php
// ✅ GOOD - Clear and descriptive
public function it_rejects_invalid_jwt_signature(): void
public function it_dispatches_provisioning_job_on_valid_request(): void
public function it_returns_404_when_job_not_found(): void

// ❌ BAD - Vague or non-descriptive
public function test_validation(): void
public function test_auth(): void
public function it_works(): void
```

---

## Standardized API Response Testing

All API responses must follow the `api()` helper format. Tests should verify:

```php
$response->assertStatus(200)
    ->assertJsonPath('status', 'SUCCESS')      // or 'ERROR'
    ->assertJsonPath('code')                   // HTTP code * 100
    ->assertJsonPath('message')                 // Descriptive message
    ->assertJsonPath('data')                   // Response data
    ->assertJsonPath('locale')                  // App locale
```

### Success Response Example
```php
$response->assertStatus(202)
    ->assertJsonPath('status', 'SUCCESS')
    ->assertJsonPath('message', 'Tenant database provisioning initiated')
    ->assertJsonPath('data.job_id')
    ->assertJsonPath('data.tenant_uid', $tenantUid);
```

### Error Response Example
```php
$response->assertStatus(422)
    ->assertJsonPath('status', 'ERROR')
    ->assertJsonPath('message', 'Validation failed')
    ->assertJsonPath('data.errors.tenant_uid');
```

---

## DO NOT Do

- ❌ Write shell scripts or curl commands for tests
- ❌ Write Postman collections (keep those for manual testing)
- ❌ Assume localhost:8000 or any specific port
- ❌ Assume running Docker containers
- ❌ Generate real RSA/encryption keys at runtime
- ❌ Disable SSL verification
- ❌ Use production keys/secrets in tests
- ❌ Use `dd()`, `var_dump()`, or `echo` in test code
- ❌ Write tests that pass intermittently (flaky)
- ❌ Write tests that depend on external services
- ❌ Use random data without seeding

---

## DO Do

- ✅ Use Feature tests for API endpoints
- ✅ Use Queue::fake() for job tests
- ✅ Use Event::fake() for event tests
- ✅ Use RefreshDatabase trait
- ✅ Use pre-generated fixtures (keys, test data)
- ✅ Mock external HTTP calls
- ✅ Write deterministic tests
- ✅ Use descriptive test names
- ✅ Document SCENARIO and WHY for each test
- ✅ Fail loudly with clear assertions
- ✅ Test both success and failure cases
- ✅ Follow PSR-12 coding standards
- ✅ Use strict types

---

## Running Tests

```bash
# Run all tests
php artisan test

# Run specific test class
php artisan test --filter=TenantProvisioningTest

# Run specific test method
php artisan test --filter=it_accepts_valid_provisioning_request

# Run tests with coverage
php artisan test --coverage

# Run tests in parallel (faster)
php artisan test --parallel
```

---

## CI/CD Integration

Tests should run in CI/CD without any special setup:

```yaml
# Example GitLab CI
test:
  script:
    - composer install
    - cp .env.testing .env
    - php artisan key:generate
    - php artisan test

# Example GitHub Actions
- name: Run Tests
  run: |
    composer install
    cp .env.testing .env
    php artisan key:generate
    php artisan test
```

No manual key generation, database setup, or service startup required.

---

## Review Checklist

Before committing a test file:

- [ ] No `echo`, `var_dump`, or `dd()` calls
- [ ] No random data without fixed seed
- [ ] No runtime key generation
- [ ] No SSL verification disabled
- [ ] No external service dependencies
- [ ] No hardcoded localhost URLs or ports
- [ ] Uses Laravel testing tools (fake(), assertJsonPath, etc.)
- [ ] Has SCENARIO and WHY PHPDoc comments
- [ ] Tests both success and failure cases
- [ ] Uses pre-generated fixtures
- [ ] Uses strict types declaration
- [ ] Runs successfully in isolation
