mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 03:29:19 +02:00
Phase 1: Consolidate shared infrastructure - Create shared/ directory for common code - Consolidate Database.php from front-backend and formulaire into unified shared/Database.php - Smart path detection for test.db vs posterg.db - Secure search with wildcard escaping and input validation - Support both singleton and direct instantiation patterns - Full CRUD methods for admin functionality - Move RateLimit.php to shared/ (30 requests/min) - Update all require paths across apps to use shared/ Phase 2: Reorganize directory structure - Rename front-backend/ → apps/public/ - Rename formulaire/ → apps/admin/ - Rename db/ → database/ - Update all file paths for new structure - Create root .gitignore excluding databases, cache, logs Implement secure search feature - Add apps/public/search.php with full-text search across theses - Search filters: query, year, orientation, AP program, keywords - Security features: - SQL injection prevention (prepared statements) - Wildcard injection prevention (escape % and _) - Input validation (max 200 chars, year range 1900-2100) - Rate limiting (30 req/min per IP) - Pagination limited to 100 results/page - XSS protection (htmlspecialchars on output) Add comprehensive test suite - Create apps/public/tests/ with proper structure - tests/Integration/SearchTest.php - 12 search scenarios - tests/Security/SecurityTest.php - vulnerability testing - tests/Unit/RateLimitTest.php - rate limit behavior - Create database/fixtures/CreateTestDatabase.php - Add apps/public/run-tests.php test runner - All tests passing (4/4 suites) Update deployment configuration - Rename justfile 'sync' recipe to 'deploy' - Create deploy group with separate deploy-public and deploy-admin - Add test-deploy recipe for test database - Exclude *.db, tests/, cache/, *.md from production deploy - Deploy shared/ to both public and admin locations Stats: +4482 insertions, -654 deletions across 72 files
469 lines
11 KiB
Markdown
469 lines
11 KiB
Markdown
# PHP Testing Best Practices
|
|
|
|
## Standard PHP Testing Structure
|
|
|
|
### Industry Standard: PHPUnit
|
|
|
|
The de facto standard for PHP testing is **PHPUnit**. Here's how professional PHP projects handle testing:
|
|
|
|
## Proper Directory Structure
|
|
|
|
```
|
|
front-backend/
|
|
├── src/ # Application code (or keep in root for small projects)
|
|
│ ├── Database.php
|
|
│ ├── RateLimit.php
|
|
│ └── ...
|
|
├── tests/ # All tests go here
|
|
│ ├── Unit/ # Unit tests (test individual methods)
|
|
│ │ ├── DatabaseTest.php
|
|
│ │ └── RateLimitTest.php
|
|
│ ├── Integration/ # Integration tests (test multiple components)
|
|
│ │ └── SearchTest.php
|
|
│ └── Security/ # Security-specific tests
|
|
│ └── SecurityTest.php
|
|
├── public/ # Public-facing files (or web root)
|
|
│ ├── index.php
|
|
│ ├── search.php
|
|
│ └── assets/
|
|
├── vendor/ # Dependencies (git-ignored, not deployed)
|
|
├── cache/ # Runtime cache (not deployed)
|
|
├── composer.json # Dependency management
|
|
├── phpunit.xml # PHPUnit configuration
|
|
└── .gitignore # Excludes tests, vendor, cache from git
|
|
```
|
|
|
|
## What We Currently Have (Non-Standard)
|
|
|
|
```
|
|
front-backend/
|
|
├── test_search.php ❌ Tests in root
|
|
├── test_security.php ❌ No framework
|
|
├── test_rate_limit.php ❌ Would deploy to production
|
|
├── create_test_db.php ❌ Test fixture in root
|
|
└── Database.php ✓ OK
|
|
```
|
|
|
|
## How Professional Projects Work
|
|
|
|
### 1. Composer Configuration
|
|
|
|
**composer.json** - Proper setup:
|
|
```json
|
|
{
|
|
"require": {
|
|
"php": "^7.4|^8.0"
|
|
},
|
|
"require-dev": {
|
|
"phpunit/phpunit": "^9.5",
|
|
"symfony/var-dumper": "^6.0"
|
|
},
|
|
"autoload": {
|
|
"psr-4": {
|
|
"App\\": "src/"
|
|
}
|
|
},
|
|
"autoload-dev": {
|
|
"psr-4": {
|
|
"Tests\\": "tests/"
|
|
}
|
|
},
|
|
"scripts": {
|
|
"test": "phpunit",
|
|
"test:coverage": "phpunit --coverage-html coverage"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Key points:**
|
|
- `require`: Production dependencies
|
|
- `require-dev`: Development/testing dependencies (not deployed)
|
|
- `autoload-dev`: Test autoloading (not in production)
|
|
- `scripts`: Convenient test commands
|
|
|
|
### 2. PHPUnit Configuration
|
|
|
|
**phpunit.xml** - Test configuration:
|
|
```xml
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<phpunit bootstrap="vendor/autoload.php"
|
|
colors="true"
|
|
verbose="true">
|
|
<testsuites>
|
|
<testsuite name="Unit">
|
|
<directory>tests/Unit</directory>
|
|
</testsuite>
|
|
<testsuite name="Integration">
|
|
<directory>tests/Integration</directory>
|
|
</testsuite>
|
|
<testsuite name="Security">
|
|
<directory>tests/Security</directory>
|
|
</testsuite>
|
|
</testsuites>
|
|
<coverage>
|
|
<include>
|
|
<directory suffix=".php">src</directory>
|
|
</include>
|
|
<exclude>
|
|
<directory>vendor</directory>
|
|
<directory>tests</directory>
|
|
</exclude>
|
|
</coverage>
|
|
</phpunit>
|
|
```
|
|
|
|
### 3. Example PHPUnit Test
|
|
|
|
**tests/Unit/DatabaseTest.php**:
|
|
```php
|
|
<?php
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use Database;
|
|
|
|
class DatabaseTest extends TestCase
|
|
{
|
|
private $db;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->db = Database::getInstance();
|
|
}
|
|
|
|
public function testGetPublishedTheses()
|
|
{
|
|
$results = $this->db->getPublishedTheses(10, 0);
|
|
|
|
$this->assertIsArray($results);
|
|
$this->assertLessThanOrEqual(10, count($results));
|
|
}
|
|
|
|
public function testSearchThesesWithWildcard()
|
|
{
|
|
$results = $this->db->searchTheses(['query' => '%'], 10, 0);
|
|
|
|
// Should return 0 results (wildcards are escaped)
|
|
$this->assertCount(0, $results);
|
|
}
|
|
|
|
public function testSearchThesesRejectsLongInput()
|
|
{
|
|
$this->expectException(\InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Search query too long');
|
|
|
|
$longQuery = str_repeat('a', 201);
|
|
$this->db->searchTheses(['query' => $longQuery]);
|
|
}
|
|
|
|
public function testSearchThesesRejectsInvalidYear()
|
|
{
|
|
$this->expectException(\InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Invalid year');
|
|
|
|
$this->db->searchTheses(['year' => 999999]);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Running Tests
|
|
|
|
```bash
|
|
# Install dependencies (including dev dependencies)
|
|
composer install
|
|
|
|
# Run all tests
|
|
composer test
|
|
# or
|
|
./vendor/bin/phpunit
|
|
|
|
# Run specific test suite
|
|
./vendor/bin/phpunit --testsuite Unit
|
|
|
|
# Run specific test file
|
|
./vendor/bin/phpunit tests/Unit/DatabaseTest.php
|
|
|
|
# Run with coverage report
|
|
composer test:coverage
|
|
```
|
|
|
|
### 5. .gitignore Configuration
|
|
|
|
**.gitignore**:
|
|
```
|
|
# Dependencies
|
|
/vendor/
|
|
|
|
# Test artifacts
|
|
/coverage/
|
|
/.phpunit.cache/
|
|
/phpunit.xml.local
|
|
|
|
# Cache
|
|
/cache/
|
|
|
|
# Environment
|
|
.env
|
|
.env.local
|
|
|
|
# IDE
|
|
/.idea/
|
|
/.vscode/
|
|
*.swp
|
|
|
|
# OS
|
|
.DS_Store
|
|
Thumbs.db
|
|
|
|
# Logs
|
|
*.log
|
|
error.log
|
|
```
|
|
|
|
**Important:** Tests themselves ARE committed to git, but:
|
|
- `vendor/` is excluded (regenerated via `composer install`)
|
|
- Test coverage reports are excluded
|
|
- Cache is excluded
|
|
|
|
## Production Deployment
|
|
|
|
### What Gets Deployed
|
|
|
|
```bash
|
|
# Option 1: composer install without dev dependencies
|
|
composer install --no-dev --optimize-autoloader
|
|
|
|
# This installs ONLY 'require' packages, NOT 'require-dev'
|
|
# Result: No PHPUnit, no test dependencies
|
|
```
|
|
|
|
**Deployed:**
|
|
- Application code (`src/` or root PHP files)
|
|
- Production dependencies (`vendor/` - only `require`)
|
|
- Public assets (`public/`, `assets/`)
|
|
|
|
**NOT Deployed:**
|
|
- `tests/` directory (excluded via deployment config)
|
|
- Dev dependencies (PHPUnit, etc.)
|
|
- `cache/` directory
|
|
- `.git/` directory
|
|
|
|
### Deployment Configurations
|
|
|
|
**Option 1: .deployignore** (custom deploy scripts):
|
|
```
|
|
/tests/
|
|
/coverage/
|
|
/.git/
|
|
/.github/
|
|
/cache/
|
|
phpunit.xml
|
|
phpunit.xml.dist
|
|
.env.example
|
|
README*.md
|
|
*.md
|
|
```
|
|
|
|
**Option 2: rsync with excludes** (like your justfile):
|
|
```bash
|
|
rsync -avz \
|
|
--exclude 'tests/' \
|
|
--exclude 'coverage/' \
|
|
--exclude 'cache/' \
|
|
--exclude '.git/' \
|
|
--exclude 'phpunit.xml' \
|
|
--exclude '*.md' \
|
|
./ server:/var/www/html/
|
|
```
|
|
|
|
**Option 3: Build artifact** (best for large projects):
|
|
```bash
|
|
# Build step
|
|
composer install --no-dev --optimize-autoloader
|
|
# Creates clean vendor/ with only production deps
|
|
|
|
# Then deploy only necessary files
|
|
```
|
|
|
|
## Continuous Integration (CI/CD)
|
|
|
|
Professional projects run tests automatically:
|
|
|
|
**GitHub Actions** (.github/workflows/tests.yml):
|
|
```yaml
|
|
name: Tests
|
|
|
|
on: [push, pull_request]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v2
|
|
|
|
- name: Setup PHP
|
|
uses: shivammathur/setup-php@v2
|
|
with:
|
|
php-version: '8.1'
|
|
|
|
- name: Install dependencies
|
|
run: composer install --prefer-dist --no-progress
|
|
|
|
- name: Run tests
|
|
run: composer test
|
|
|
|
- name: Check security
|
|
run: ./vendor/bin/phpunit --testsuite Security
|
|
```
|
|
|
|
## Test Types
|
|
|
|
### Unit Tests
|
|
Test individual methods in isolation:
|
|
```php
|
|
public function testEscapeLikeString()
|
|
{
|
|
$db = new Database();
|
|
$reflection = new ReflectionClass($db);
|
|
$method = $reflection->getMethod('escapeLikeString');
|
|
$method->setAccessible(true);
|
|
|
|
$result = $method->invoke($db, 'test%value_here');
|
|
$this->assertEquals('test\%value\_here', $result);
|
|
}
|
|
```
|
|
|
|
### Integration Tests
|
|
Test multiple components together:
|
|
```php
|
|
public function testSearchWithMultipleFilters()
|
|
{
|
|
$db = Database::getInstance();
|
|
$results = $db->searchTheses([
|
|
'query' => 'urbain',
|
|
'year' => 2024,
|
|
'orientation' => 'Arts Numériques'
|
|
]);
|
|
|
|
$this->assertNotEmpty($results);
|
|
foreach ($results as $result) {
|
|
$this->assertEquals(2024, $result['year']);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Security Tests
|
|
Test security measures:
|
|
```php
|
|
public function testSqlInjectionPrevention()
|
|
{
|
|
$db = Database::getInstance();
|
|
|
|
// These should not cause errors or expose data
|
|
$malicious = ["' OR 1=1--", "'; DROP TABLE theses;--"];
|
|
|
|
foreach ($malicious as $injection) {
|
|
$results = $db->searchTheses(['query' => $injection]);
|
|
// Treated as literal strings, returns valid results or empty
|
|
$this->assertIsArray($results);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Comparison: Current vs. Standard
|
|
|
|
| Aspect | Current Approach | Standard Approach |
|
|
|--------|------------------|-------------------|
|
|
| **Location** | Root directory | `tests/` directory |
|
|
| **Framework** | Raw PHP scripts | PHPUnit |
|
|
| **Naming** | `test_*.php` | `*Test.php` |
|
|
| **Running** | `php test_file.php` | `composer test` |
|
|
| **CI/CD** | Manual | Automated |
|
|
| **Production** | Must manually exclude | Auto-excluded |
|
|
| **Coverage** | None | Built-in reporting |
|
|
| **Assertions** | Manual echoing | PHPUnit assertions |
|
|
|
|
## Migration Path for Your Project
|
|
|
|
### Minimal Changes (Keep it Simple)
|
|
|
|
If you want to keep the current simple approach but make it safer:
|
|
|
|
1. **Move tests to `tests/` directory:**
|
|
```bash
|
|
mkdir tests
|
|
mv test_*.php tests/
|
|
mv create_test_db.php tests/fixtures/
|
|
```
|
|
|
|
2. **Update justfile to exclude tests:**
|
|
```just
|
|
deploy:
|
|
rsync -vur --progress \
|
|
--exclude 'tests/' \
|
|
--exclude 'cache/' \
|
|
--exclude '*.db' \
|
|
./front-backend/ server:/var/www/html/
|
|
```
|
|
|
|
3. **Add .gitignore:**
|
|
```
|
|
/cache/
|
|
/vendor/
|
|
*.log
|
|
test.db
|
|
```
|
|
|
|
### Recommended Approach (Industry Standard)
|
|
|
|
For a more professional setup:
|
|
|
|
1. **Install PHPUnit:**
|
|
```bash
|
|
composer require --dev phpunit/phpunit
|
|
```
|
|
|
|
2. **Convert tests to PHPUnit** (I can help with this)
|
|
|
|
3. **Add phpunit.xml configuration**
|
|
|
|
4. **Update deployment to use `composer install --no-dev`**
|
|
|
|
## Benefits of Standard Approach
|
|
|
|
1. **Automatic Exclusion**: Tests never deployed by accident
|
|
2. **Better Assertions**: PHPUnit provides rich assertion library
|
|
3. **Coverage Reports**: See which code is tested
|
|
4. **CI/CD Integration**: Automated testing on every commit
|
|
5. **IDE Support**: Better integration with PHPStorm, VSCode
|
|
6. **Mocking**: Easy to mock dependencies
|
|
7. **Data Providers**: Test same logic with multiple inputs
|
|
8. **Professional**: Expected by other developers
|
|
|
|
## Quick Decision Guide
|
|
|
|
**Keep Simple Approach If:**
|
|
- ✓ Small project (< 10 files)
|
|
- ✓ Solo developer
|
|
- ✓ No CI/CD pipeline
|
|
- ✓ You manually test before deploy
|
|
|
|
**Use PHPUnit If:**
|
|
- ✓ Team project
|
|
- ✓ Growing codebase
|
|
- ✓ Want automated testing
|
|
- ✓ Need coverage reports
|
|
- ✓ Planning CI/CD
|
|
|
|
## Recommendation for Your Project
|
|
|
|
Given your project size, I'd suggest a **hybrid approach**:
|
|
|
|
1. **Move tests to `tests/` directory** (immediate)
|
|
2. **Update deployment to exclude `tests/`** (immediate)
|
|
3. **Keep simple PHP test scripts for now** (works fine)
|
|
4. **Migrate to PHPUnit later** (when project grows)
|
|
|
|
Would you like me to help with any of these approaches?
|