677 lines
24 KiB
PHP
677 lines
24 KiB
PHP
<?php
|
|
/**
|
|
* OHMJ API Tests - Functional & Error Handling
|
|
* Run: php tests.php
|
|
*/
|
|
|
|
// Load environment variables from .env file
|
|
$envFile = __DIR__ . '/.env';
|
|
if (file_exists($envFile)) {
|
|
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
foreach ($lines as $line) {
|
|
if (strpos($line, '#') === 0) continue;
|
|
if (strpos($line, '=') === false) continue;
|
|
list($key, $value) = explode('=', $line, 2);
|
|
$key = trim($key);
|
|
$value = trim($value);
|
|
if (!empty($key)) {
|
|
putenv("$key=$value");
|
|
$_ENV[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
define('BASE_URL', 'http://localhost:8000');
|
|
define('SCORES_PATH', '../legacy/Scores/');
|
|
|
|
class APITest {
|
|
private $token;
|
|
private $passed = 0;
|
|
private $failed = 0;
|
|
|
|
public function __construct() {
|
|
echo "=== OHMJ API Tests ===\n\n";
|
|
}
|
|
|
|
private function request($method, $endpoint, $data = null, $auth = true, $params = []) {
|
|
$url = BASE_URL . '/' . $endpoint;
|
|
if (!empty($params)) {
|
|
$url .= '?' . http_build_query($params);
|
|
}
|
|
|
|
$headers = ['Content-Type: application/json'];
|
|
if ($auth && $this->token) {
|
|
$headers[] = 'Authorization: Bearer ' . $this->token;
|
|
}
|
|
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => $method,
|
|
'header' => implode("\r\n", $headers),
|
|
'content' => $data ? json_encode($data) : null,
|
|
'ignore_errors' => true
|
|
]
|
|
]);
|
|
|
|
// Capture response headers globally
|
|
global $last_response_headers;
|
|
$response = @file_get_contents($url, false, $context);
|
|
$last_response_headers = $http_response_header ?? [];
|
|
|
|
$httpCode = 500;
|
|
foreach ($http_response_header ?? [] as $header) {
|
|
if (preg_match('/^HTTP\/\d+\.\d+\s+(\d+)/', $header, $matches)) {
|
|
$httpCode = (int)$matches[1];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'code' => $httpCode,
|
|
'body' => json_decode($response, true) ?? []
|
|
];
|
|
}
|
|
|
|
public function test($name, $condition) {
|
|
if ($condition) {
|
|
echo "✓ $name\n";
|
|
$this->passed++;
|
|
} else {
|
|
echo "✗ $name\n";
|
|
$this->failed++;
|
|
}
|
|
}
|
|
|
|
public function section($name) {
|
|
echo "\n--- $name ---\n";
|
|
}
|
|
|
|
// ============ AUTH TESTS ============
|
|
|
|
public function testAuthErrors() {
|
|
$this->section("Auth - Error Handling");
|
|
|
|
// Bad credentials
|
|
$result = $this->request('POST', 'login', ['username' => 'admin', 'password' => 'wrong'], false);
|
|
$this->test('Bad password returns 401', $result['code'] === 401);
|
|
|
|
// Missing credentials
|
|
$result = $this->request('POST', 'login', ['username' => '', 'password' => ''], false);
|
|
$this->test('Empty credentials returns 401', $result['code'] === 401);
|
|
|
|
// Unknown user
|
|
$result = $this->request('POST', 'login', ['username' => 'unknown', 'password' => 'pass'], false);
|
|
$this->test('Unknown user returns 401', $result['code'] === 401);
|
|
|
|
// Access without token
|
|
$result = $this->request('GET', 'scores', null, false);
|
|
$this->test('Access without token returns 401', $result['code'] === 401);
|
|
|
|
// Invalid token
|
|
$result = $this->request('GET', 'scores', null, true);
|
|
$this->test('Invalid token returns 401', $result['code'] === 401);
|
|
}
|
|
|
|
public function testAuthSuccess() {
|
|
$this->section("Auth - Success");
|
|
|
|
$result = $this->request('POST', 'login', ['username' => 'admin', 'password' => 'password'], false);
|
|
$this->test('Login returns 200', $result['code'] === 200);
|
|
|
|
if ($result['code'] === 200) {
|
|
$this->token = $result['body']['token'] ?? null;
|
|
$this->test('Token received', !empty($this->token));
|
|
$this->test('User role is admin', ($result['body']['user']['role'] ?? '') === 'admin');
|
|
}
|
|
}
|
|
|
|
// ============ SCORES TESTS ============
|
|
|
|
public function testScoresErrors() {
|
|
$this->section("Scores - Error Handling");
|
|
|
|
// Get non-existent score
|
|
$result = $this->request('GET', 'scores/999999');
|
|
$this->test('Non-existent score returns 404', $result['code'] === 404);
|
|
|
|
// Update non-existent score
|
|
$result = $this->request('PUT', 'scores/999999', ['name' => 'Test']);
|
|
$this->test('Update non-existent returns 404', $result['code'] === 404);
|
|
|
|
// Delete non-existent score
|
|
$result = $this->request('DELETE', 'admin/scores/999999');
|
|
$this->test('Delete non-existent returns 404 or 400', $result['code'] === 404 || $result['code'] === 400);
|
|
|
|
// Create score without name
|
|
$result = $this->request('POST', 'admin/scores', ['name' => '', 'compositor' => 'Test']);
|
|
$this->test('Create without name returns 400', $result['code'] === 400);
|
|
}
|
|
|
|
public function testScoresSuccess() {
|
|
$this->section("Scores - Functional");
|
|
|
|
// Get all scores
|
|
$result = $this->request('GET', 'scores');
|
|
$this->test('Get scores returns 200', $result['code'] === 200);
|
|
$this->test('Response has scores array', isset($result['body']['scores']));
|
|
|
|
if (!empty($result['body']['scores'])) {
|
|
$scoreId = $result['body']['scores'][0]['id'];
|
|
|
|
// Get single score
|
|
$result = $this->request('GET', "scores/$scoreId");
|
|
$this->test('Get single score returns 200', $result['code'] === 200);
|
|
$this->test('Score has id', isset($result['body']['score']['id']));
|
|
$this->test('Score has name', isset($result['body']['score']['name']));
|
|
|
|
// Get pieces
|
|
$result = $this->request('GET', "pieces/$scoreId");
|
|
$this->test('Get pieces returns 200', $result['code'] === 200);
|
|
$this->test('Response has pieces array', isset($result['body']['pieces']));
|
|
}
|
|
}
|
|
|
|
// ============ CREATE SCORE WITH PIECES TESTS ============
|
|
|
|
public function testCreateScoreWithPieces() {
|
|
$this->section("Create Score with Pieces - Functional");
|
|
|
|
// Test 1: Create score with 2 pieces
|
|
$testName = 'Test Score ' . date('YmdHis');
|
|
$result = $this->request('POST', 'admin/scores', [
|
|
'name' => $testName,
|
|
'compositor' => 'Test Composer',
|
|
'pieces' => [
|
|
['number' => 1, 'name' => 'Allegro'],
|
|
['number' => 2, 'name' => 'Adagio']
|
|
]
|
|
]);
|
|
|
|
$this->test('Create with pieces returns 200', $result['code'] === 200);
|
|
|
|
if ($result['code'] === 200 && isset($result['body']['score']['id'])) {
|
|
$scoreId = $result['body']['score']['id'];
|
|
echo " Created: $scoreId\n";
|
|
|
|
// Verify score.ini
|
|
$iniPath = SCORES_PATH . $scoreId . '/score.ini';
|
|
if (file_exists($iniPath)) {
|
|
$ini = parse_ini_file($iniPath, true);
|
|
$this->test('Piece count is 2', ($ini['pieces']['count'] ?? '') === '2');
|
|
$this->test('Piece 1 name saved', ($ini['pieces']['1'] ?? '') === 'Allegro');
|
|
$this->test('Piece 2 name saved', ($ini['pieces']['2'] ?? '') === 'Adagio');
|
|
}
|
|
|
|
// Verify pieces API
|
|
$result = $this->request('GET', "pieces/$scoreId");
|
|
if ($result['code'] === 200) {
|
|
$pieces = $result['body']['pieces'] ?? [];
|
|
$this->test('API returns 2 pieces', count($pieces) === 2);
|
|
$this->test('Piece 1 has correct name', ($pieces[0]['name'] ?? '') === 'Allegro');
|
|
$this->test('Piece 2 has correct name', ($pieces[1]['name'] ?? '') === 'Adagio');
|
|
}
|
|
|
|
// Cleanup
|
|
$this->request('DELETE', "admin/scores/$scoreId");
|
|
echo " Cleaned up\n";
|
|
}
|
|
|
|
// Test 2: Create score with 1 piece (default)
|
|
$testName = 'Test Score Default ' . date('YmdHis');
|
|
$result = $this->request('POST', 'admin/scores', [
|
|
'name' => $testName,
|
|
'compositor' => 'Test Composer'
|
|
]);
|
|
|
|
$this->test('Create default returns 200', $result['code'] === 200);
|
|
|
|
if ($result['code'] === 200 && isset($result['body']['score']['id'])) {
|
|
$scoreId = $result['body']['score']['id'];
|
|
$iniPath = SCORES_PATH . $scoreId . '/score.ini';
|
|
|
|
if (file_exists($iniPath)) {
|
|
$ini = parse_ini_file($iniPath, true);
|
|
$this->test('Default piece count is 1', ($ini['pieces']['count'] ?? '') === '1');
|
|
}
|
|
|
|
$this->request('DELETE', "admin/scores/$scoreId");
|
|
}
|
|
}
|
|
|
|
// ============ FILES TESTS ============
|
|
|
|
public function testFilesErrors() {
|
|
$this->section("Files - Error Handling");
|
|
|
|
// Get files for non-existent score
|
|
$result = $this->request('GET', 'admin/scores/999999/files');
|
|
$this->test('Get files non-existent score returns 400', $result['code'] === 400);
|
|
|
|
// Delete without path
|
|
$scores = $this->request('GET', 'scores');
|
|
if (!empty($scores['body']['scores'])) {
|
|
$scoreId = $scores['body']['scores'][0]['id'];
|
|
|
|
$result = $this->request('DELETE', "admin/scores/$scoreId/files");
|
|
$this->test('Delete without path returns 400', $result['code'] === 400);
|
|
|
|
// Delete non-existent file
|
|
$result = $this->request('DELETE', "admin/scores/$scoreId/files?path=nonexistent.pdf");
|
|
$this->test('Delete non-existent file returns 400', $result['code'] === 400);
|
|
}
|
|
}
|
|
|
|
public function testFilesSuccess() {
|
|
$this->section("Files - Functional");
|
|
|
|
// Get files for a score
|
|
$scores = $this->request('GET', 'scores');
|
|
|
|
if (!empty($scores['body']['scores'])) {
|
|
$scoreId = $scores['body']['scores'][0]['id'];
|
|
|
|
$result = $this->request('GET', "admin/scores/$scoreId/files");
|
|
$this->test('Get files returns 200', $result['code'] === 200);
|
|
$this->test('Response has files array', isset($result['body']['files']));
|
|
$this->test('Files is array', is_array($result['body']['files']));
|
|
|
|
// Test structure
|
|
$files = $result['body']['files'];
|
|
if (!empty($files)) {
|
|
$this->test('Files have name property', isset($files[0]['name']));
|
|
$this->test('Files have path property', isset($files[0]['path']));
|
|
$this->test('Files have type property', isset($files[0]['type']));
|
|
}
|
|
} else {
|
|
echo " ⚠ No scores available for file tests\n";
|
|
}
|
|
}
|
|
|
|
public function testWatermarkUpload() {
|
|
$this->section("Upload - Watermark");
|
|
|
|
// Reset rate limiting
|
|
array_map('unlink', glob(sys_get_temp_dir() . '/rate_*'));
|
|
|
|
// Get a score to upload to
|
|
$scores = $this->request('GET', 'scores');
|
|
|
|
if (empty($scores['body']['scores'])) {
|
|
echo " ⚠ No scores available for watermark test\n";
|
|
return;
|
|
}
|
|
|
|
$scoreId = $scores['body']['scores'][0]['id'];
|
|
|
|
// Create a minimal test PDF
|
|
$pdfContent = "%PDF-1.4
|
|
1 0 obj
|
|
<< /Type /Catalog /Pages 2 0 R >>
|
|
endobj
|
|
2 0 obj
|
|
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
|
endobj
|
|
3 0 obj
|
|
<< /Type /Page /Parent 2 0 R /Resources 4 0 R /MediaBox [0 0 612 792] /Contents 5 0 R >>
|
|
endobj
|
|
4 0 obj
|
|
<< /Font << /F1 6 0 R >> >>
|
|
endobj
|
|
5 0 obj
|
|
<< /Length 44 >>
|
|
stream
|
|
BT
|
|
/F1 12 Tf
|
|
100 700 Td
|
|
(Test) Tj
|
|
ET
|
|
endstream
|
|
endobj
|
|
6 0 obj
|
|
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
|
|
endobj
|
|
xref
|
|
0 7
|
|
0000000000 65535 f
|
|
0000000009 00000 n
|
|
0000000058 00000 n
|
|
0000000115 00000 n
|
|
0000000214 00000 n
|
|
0000000312 00000 n
|
|
0000000401 00000 n
|
|
trailer
|
|
<< /Size 7 /Root 1 0 R >>
|
|
startxref
|
|
490
|
|
%%EOF";
|
|
|
|
$tmpFile = sys_get_temp_dir() . '/test_watermark_' . time() . '.pdf';
|
|
file_put_contents($tmpFile, $pdfContent);
|
|
|
|
// Test upload with watermark left
|
|
$result = $this->multipartRequest("admin/scores/$scoreId/upload", [
|
|
['name' => 'file', 'file' => $tmpFile],
|
|
['name' => 'piece', 'data' => '1'],
|
|
['name' => 'instrument', 'data' => 'dir'],
|
|
['name' => 'version', 'data' => '1'],
|
|
['name' => 'watermark', 'data' => 'true'],
|
|
['name' => 'watermarkPosition', 'data' => 'left']
|
|
]);
|
|
|
|
$this->test('Upload with watermark left returns 200', $result['code'] === 200);
|
|
|
|
// Get files to verify it was uploaded
|
|
$files = $this->request('GET', "admin/scores/$scoreId/files");
|
|
$hasFile = false;
|
|
if (!empty($files['body']['files'])) {
|
|
foreach ($files['body']['files'] as $f) {
|
|
if (strpos($f['path'], 'direction_1.pdf') !== false) {
|
|
$hasFile = true;
|
|
// Clean up
|
|
@unlink(SCORES_PATH . "$scoreId/" . $f['path']);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test upload with watermark right
|
|
$result2 = $this->multipartRequest("admin/scores/$scoreId/upload", [
|
|
['name' => 'file', 'file' => $tmpFile],
|
|
['name' => 'piece', 'data' => '1'],
|
|
['name' => 'instrument', 'data' => 'dir'],
|
|
['name' => 'version', 'data' => '2'],
|
|
['name' => 'watermark', 'data' => 'true'],
|
|
['name' => 'watermarkPosition', 'data' => 'right']
|
|
]);
|
|
|
|
$this->test('Upload with watermark right returns 200', $result2['code'] === 200);
|
|
|
|
// Clean up
|
|
@unlink($tmpFile);
|
|
|
|
// Clean up uploaded files
|
|
$files = $this->request('GET', "admin/scores/$scoreId/files");
|
|
if (!empty($files['body']['files'])) {
|
|
foreach ($files['body']['files'] as $f) {
|
|
if (strpos($f['path'], 'direction_') !== false) {
|
|
@unlink(SCORES_PATH . "$scoreId/" . $f['path']);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function multipartRequest($endpoint, $fields) {
|
|
$url = BASE_URL . '/' . $endpoint;
|
|
|
|
$boundary = '----WebKitFormBoundary' . bin2hex(random_bytes(8));
|
|
|
|
$body = '';
|
|
foreach ($fields as $field) {
|
|
$body .= "--$boundary\r\n";
|
|
if (isset($field['file'])) {
|
|
$filename = basename($field['file']);
|
|
$content = file_get_contents($field['file']);
|
|
$body .= "Content-Disposition: form-data; name=\"{$field['name']}\"; filename=\"$filename\"\r\n";
|
|
$body .= "Content-Type: application/pdf\r\n\r\n";
|
|
$body .= $content . "\r\n";
|
|
} else {
|
|
$body .= "Content-Disposition: form-data; name=\"{$field['name']}\"\r\n\r\n";
|
|
$body .= $field['data'] . "\r\n";
|
|
}
|
|
}
|
|
$body .= "--$boundary--\r\n";
|
|
|
|
$headers = [
|
|
'Content-Type: multipart/form-data; boundary=' . $boundary,
|
|
'Authorization: Bearer ' . $this->token
|
|
];
|
|
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => 'POST',
|
|
'header' => implode("\r\n", $headers),
|
|
'content' => $body
|
|
]
|
|
]);
|
|
|
|
$response = @file_get_contents($url, false, $context);
|
|
|
|
$headers_list = $http_response_header ?? [];
|
|
$code = 0;
|
|
foreach ($headers_list as $header) {
|
|
if (preg_match('/^HTTP\/\d+(\.\d+)?\s+(\d{3})/', $header, $matches)) {
|
|
$code = (int)$matches[2];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'code' => $code,
|
|
'body' => json_decode($response, true) ?? []
|
|
];
|
|
}
|
|
|
|
// ============ SECURITY TESTS ============
|
|
|
|
public function testSecurityHeaders() {
|
|
$this->section("Security - HTTP Headers");
|
|
|
|
global $last_response_headers;
|
|
$result = $this->request('GET', 'scores', null, false);
|
|
$headers = $last_response_headers ?? [];
|
|
|
|
$hasXContentType = false;
|
|
$hasXFrameOptions = false;
|
|
$hasXXSS = false;
|
|
$hasHSTS = false;
|
|
|
|
foreach ($headers as $header) {
|
|
if (stripos($header, 'X-Content-Type-Options:') !== false) $hasXContentType = true;
|
|
if (stripos($header, 'X-Frame-Options:') !== false) $hasXFrameOptions = true;
|
|
if (stripos($header, 'X-XSS-Protection:') !== false) $hasXXSS = true;
|
|
if (stripos($header, 'Strict-Transport-Security:') !== false) $hasHSTS = true;
|
|
}
|
|
|
|
$this->test('X-Content-Type-Options header present', $hasXContentType);
|
|
$this->test('X-Frame-Options header present', $hasXFrameOptions);
|
|
$this->test('X-XSS-Protection header present', $hasXXSS);
|
|
$this->test('Strict-Transport-Security header present', $hasHSTS);
|
|
}
|
|
|
|
public function testCorsPolicy() {
|
|
$this->section("Security - CORS Policy");
|
|
|
|
// Test avec Origin autorisé
|
|
$url = BASE_URL . '/login';
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => 'OPTIONS',
|
|
'header' => "Origin: http://localhost:5173\r\nAccess-Control-Request-Method: POST",
|
|
'ignore_errors' => true
|
|
]
|
|
]);
|
|
|
|
@file_get_contents($url, false, $context);
|
|
$headers = $http_response_header ?? [];
|
|
|
|
$hasCorrectOrigin = false;
|
|
foreach ($headers as $header) {
|
|
if (stripos($header, 'Access-Control-Allow-Origin: http://localhost:5173') !== false) {
|
|
$hasCorrectOrigin = true;
|
|
}
|
|
}
|
|
|
|
$this->test('CORS allows localhost:5173', $hasCorrectOrigin);
|
|
}
|
|
|
|
public function testDirectoryTraversal() {
|
|
$this->section("Security - Directory Traversal");
|
|
|
|
// Ensure we have a valid token for these tests
|
|
if (!$this->token) {
|
|
// Clear rate limiting and login fresh
|
|
array_map('unlink', glob(sys_get_temp_dir() . '/rate_*'));
|
|
$result = $this->request('POST', 'login', ['username' => 'admin', 'password' => 'password'], false);
|
|
if ($result['code'] === 200 && isset($result['body']['token'])) {
|
|
$this->token = $result['body']['token'];
|
|
}
|
|
}
|
|
|
|
// Clear rate limiting to avoid blocking
|
|
array_map('unlink', glob(sys_get_temp_dir() . '/rate_*'));
|
|
|
|
// Test 1: Download avec path traversal
|
|
$result = $this->request('GET', 'download/../../../etc/passwd', null, true);
|
|
// Accept 401 (no token), 403 (blocked), or 404 (path not found after sanitization)
|
|
$blocked = in_array($result['code'], [401, 403, 404]);
|
|
$this->test('Directory traversal blocked (../)', $blocked);
|
|
|
|
// Clear rate limiting between tests
|
|
array_map('unlink', glob(sys_get_temp_dir() . '/rate_*'));
|
|
|
|
// Test 2: Download avec double encoding
|
|
$result = $this->request('GET', 'download/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd', null, true);
|
|
$blocked = in_array($result['code'], [401, 403, 404]);
|
|
$this->test('Directory traversal blocked (URL encoded)', $blocked);
|
|
|
|
// Clear rate limiting between tests
|
|
array_map('unlink', glob(sys_get_temp_dir() . '/rate_*'));
|
|
|
|
// Test 3: Download avec null byte (si PHP < 5.3.4)
|
|
$result = $this->request('GET', "download/file.pdf%00.jpg", null, true);
|
|
$blocked = in_array($result['code'], [400, 401, 403]);
|
|
$this->test('Null byte injection blocked', $blocked);
|
|
}
|
|
|
|
public function testRateLimiting() {
|
|
$this->section("Security - Rate Limiting");
|
|
|
|
// Clear any existing rate limiting before test
|
|
array_map('unlink', glob(sys_get_temp_dir() . '/rate_*'));
|
|
|
|
// Test login brute force
|
|
$attempts = 0;
|
|
$blocked = false;
|
|
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$result = $this->request('POST', 'login', ['username' => 'admin', 'password' => 'wrong'], false);
|
|
if ($result['code'] === 429) {
|
|
$blocked = true;
|
|
break;
|
|
}
|
|
$attempts++;
|
|
}
|
|
|
|
$this->test('Rate limiting active (429 after attempts)', $blocked);
|
|
$this->test('Rate limit triggered within 10 attempts', $attempts < 10);
|
|
|
|
// IMPORTANT: Clear rate limiting after this test so subsequent tests aren't blocked
|
|
array_map('unlink', glob(sys_get_temp_dir() . '/rate_*'));
|
|
}
|
|
|
|
public function testFileUploadSecurity() {
|
|
$this->section("Security - File Upload");
|
|
|
|
// Completely reset rate limiting
|
|
array_map('unlink', glob(sys_get_temp_dir() . '/rate_*'));
|
|
|
|
// Test directly on an existing score (001 should exist)
|
|
// This avoids needing to create a score which might be blocked
|
|
$result = $this->request('POST', "admin/scores/001/upload");
|
|
|
|
// Should return 400 (no file), not 404 (score not found) or 200 (success)
|
|
$this->test('Upload rejects empty file', $result['code'] === 400);
|
|
}
|
|
|
|
public function testInjectionAttacks() {
|
|
$this->section("Security - Injection Protection");
|
|
|
|
// Test INI injection (newline in name) on a temporary file directly
|
|
$testName = "Test\n[injection]\nmalicious=true";
|
|
$sanitizedName = str_replace(["\n", "\r", "\x00"], '', $testName);
|
|
|
|
// Create a temporary score.ini to test the sanitization
|
|
$tempDir = sys_get_temp_dir() . '/test_injection_' . time();
|
|
mkdir($tempDir);
|
|
|
|
$iniContent = "[info]\nname = $sanitizedName\ncompositor = Test\n\n[pieces]\ncount = 1\n";
|
|
file_put_contents($tempDir . '/score.ini', $iniContent);
|
|
|
|
// Verify the injection was prevented by parsing the INI file
|
|
// If injection worked, we'd have a section called [injection]
|
|
$parsed = parse_ini_file($tempDir . '/score.ini', true);
|
|
$hasInjectionSection = isset($parsed['injection']);
|
|
|
|
$this->test('INI injection prevented', !$hasInjectionSection);
|
|
|
|
// Cleanup
|
|
unlink($tempDir . '/score.ini');
|
|
rmdir($tempDir);
|
|
}
|
|
|
|
public function testHttpsEnforcement() {
|
|
$this->section("Security - HTTPS Enforcement");
|
|
|
|
// Test que le serveur redirige HTTP vers HTTPS ou refuse
|
|
// Note: En local, on vérifie juste le header HSTS
|
|
global $last_response_headers;
|
|
$result = $this->request('GET', 'scores', null, false);
|
|
$headers = $last_response_headers ?? [];
|
|
|
|
$hasHSTS = false;
|
|
foreach ($headers as $header) {
|
|
if (stripos($header, 'Strict-Transport-Security:') !== false) {
|
|
$hasHSTS = true;
|
|
}
|
|
}
|
|
|
|
$this->test('HSTS header present for HTTPS enforcement', $hasHSTS);
|
|
}
|
|
|
|
// ============ SUMMARY ============
|
|
|
|
public function summary() {
|
|
echo "\n=== Summary ===\n";
|
|
echo "Passed: {$this->passed}\n";
|
|
echo "Failed: {$this->failed}\n";
|
|
echo "Total: " . ($this->passed + $this->failed) . "\n";
|
|
|
|
if ($this->failed > 0) {
|
|
echo "\n⚠ Some tests failed!\n";
|
|
} else {
|
|
echo "\n✓ All tests passed!\n";
|
|
}
|
|
|
|
exit($this->failed > 0 ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
// Run tests
|
|
$tests = new APITest();
|
|
|
|
// Auth tests
|
|
$tests->testAuthErrors();
|
|
$tests->testAuthSuccess();
|
|
|
|
// Scores tests
|
|
$tests->testScoresErrors();
|
|
$tests->testScoresSuccess();
|
|
|
|
// Create score with pieces
|
|
$tests->testCreateScoreWithPieces();
|
|
|
|
// Files tests
|
|
$tests->testFilesErrors();
|
|
$tests->testFilesSuccess();
|
|
$tests->testWatermarkUpload();
|
|
|
|
// Security tests
|
|
$tests->testSecurityHeaders();
|
|
$tests->testCorsPolicy();
|
|
$tests->testDirectoryTraversal();
|
|
$tests->testRateLimiting();
|
|
$tests->testFileUploadSecurity();
|
|
$tests->testInjectionAttacks();
|
|
$tests->testHttpsEnforcement();
|
|
|
|
// Summary
|
|
$tests->summary();
|