mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 03:29:19 +02:00
Restructure repository and implement secure search feature
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
This commit is contained in:
468
apps/public/TESTING_BEST_PRACTICES.md
Normal file
468
apps/public/TESTING_BEST_PRACTICES.md
Normal file
@@ -0,0 +1,468 @@
|
||||
# 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?
|
||||
Reference in New Issue
Block a user