mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
Phase 4 cleanup: migrate old tests to PHPUnit, add ErrorHandler/PureLogic/SearchController tests, remove app/tests/, update justfile test target
This commit is contained in:
265
tests/phpunit/ErrorHandlerTest.php
Normal file
265
tests/phpunit/ErrorHandlerTest.php
Normal file
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* ErrorHandlerTest — PHPUnit version of the old custom-runner ErrorHandler test.
|
||||
*
|
||||
* Covers ErrorHandler::userMessage() FK constraint parsing, UNIQUE/NOT NULL
|
||||
* handling, domain exception pass-through, generic fallback, and log().
|
||||
*/
|
||||
class ErrorHandlerTest extends TestCase
|
||||
{
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private function makeFkException(string $sqliteMessage): PDOException
|
||||
{
|
||||
return new PDOException($sqliteMessage, 787);
|
||||
}
|
||||
|
||||
// ── FK constraint: INSERT INTO pattern ───────────────────────────────────
|
||||
|
||||
public function testFkThesesTableMentionsAllPossibleFields(): void
|
||||
{
|
||||
$msg = $this->makeFkException(
|
||||
'FOREIGN KEY constraint failed INSERT INTO theses (title, orientation_id) VALUES (?,?)'
|
||||
);
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Orientation', $user);
|
||||
$this->assertStringContainsString('AP', $user);
|
||||
$this->assertStringContainsString('Licence', $user);
|
||||
$this->assertStringNotContainsString('FOREIGN KEY', $user);
|
||||
}
|
||||
|
||||
public function testFkApPrograms(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed INSERT INTO theses (ap_program_id) VALUES (?)');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('AP', $user);
|
||||
}
|
||||
|
||||
public function testFkFinalityTypes(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed INSERT INTO theses (finality_id) VALUES (?)');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Finalité', $user);
|
||||
}
|
||||
|
||||
public function testFkThesisLanguages(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?,?)');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Langue(s)', $user);
|
||||
}
|
||||
|
||||
public function testFkThesisFormats(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?,?)');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Format(s)', $user);
|
||||
}
|
||||
|
||||
public function testFkThesisTags(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_tags (thesis_id, tag_id) VALUES (?,?)');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Mots-clés', $user);
|
||||
}
|
||||
|
||||
public function testFkThesisSupervisors(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_supervisors (thesis_id, supervisor_id) VALUES (?,?)');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Composition du jury', $user);
|
||||
}
|
||||
|
||||
public function testFkAccessTypes(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed INSERT INTO theses (access_type_id) VALUES (?)');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString("Type d'accès", $user);
|
||||
}
|
||||
|
||||
public function testFkLicenseTypes(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed INSERT INTO theses (license_id) VALUES (?)');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Licence', $user);
|
||||
}
|
||||
|
||||
public function testFkAuthors(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_authors (thesis_id, author_id) VALUES (?,?)');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Auteur·ice', $user);
|
||||
}
|
||||
|
||||
// ── FK constraint: "table" pattern (SQLite 3.37+) ────────────────────────
|
||||
|
||||
public function testFkQuotedTableName(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed (table "orientations")');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Orientation', $user);
|
||||
}
|
||||
|
||||
public function testFkQuotedLanguages(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed (table "languages")');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Langue(s)', $user);
|
||||
}
|
||||
|
||||
public function testFkQuotedFormatTypes(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed (table "format_types")');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Format(s)', $user);
|
||||
}
|
||||
|
||||
// ── FK constraint: REFERENCES pattern ────────────────────────────────────
|
||||
|
||||
public function testFkReferencesTags(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed (REFERENCES tags(id))');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Mots-clés', $user);
|
||||
}
|
||||
|
||||
public function testFkReferencesOrientations(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed (REFERENCES orientations(id))');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Orientation', $user);
|
||||
}
|
||||
|
||||
// ── FK constraint: unknown table → generic ───────────────────────────────
|
||||
|
||||
public function testFkUnknownTableGenericFallback(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed INSERT INTO unknown_table VALUES (?)');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('contrainte de référence est invalide', $user);
|
||||
$this->assertStringNotContainsString('unknown_table', $user);
|
||||
}
|
||||
|
||||
public function testFkEmptyMessageGenericFallback(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('contrainte de référence est invalide', $user);
|
||||
}
|
||||
|
||||
// ── UNIQUE constraint ────────────────────────────────────────────────────
|
||||
|
||||
public function testUniqueConstraint(): void
|
||||
{
|
||||
$msg = new PDOException('UNIQUE constraint failed: thesis_tags.tag_id, thesis_tags.thesis_id', 2067);
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('valeur en double', $user);
|
||||
$this->assertStringNotContainsString('UNIQUE', $user);
|
||||
$this->assertStringNotContainsString('thesis_tags', $user);
|
||||
}
|
||||
|
||||
// ── NOT NULL constraint ──────────────────────────────────────────────────
|
||||
|
||||
public function testNotNullConstraint(): void
|
||||
{
|
||||
$msg = new PDOException('NOT NULL constraint failed: theses.title', 1299);
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('champ obligatoire est manquant', $user);
|
||||
$this->assertStringNotContainsString('NOT NULL', $user);
|
||||
}
|
||||
|
||||
// ── Generic PDO error ────────────────────────────────────────────────────
|
||||
|
||||
public function testGenericPdoError(): void
|
||||
{
|
||||
$msg = new PDOException('database disk image is malformed', 11);
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Une erreur de base de données est survenue', $user);
|
||||
$this->assertStringNotContainsString('disk image', $user);
|
||||
}
|
||||
|
||||
// ── Domain exceptions pass through ───────────────────────────────────────
|
||||
|
||||
public function testDuplicateThesisExceptionPassesThrough(): void
|
||||
{
|
||||
$dup = new DuplicateThesisException(42, '2025-ABC12345', 'Test titre', 'Auteur', 2025);
|
||||
$user = ErrorHandler::userMessage($dup);
|
||||
$this->assertStringContainsString('2025-ABC12345', $user);
|
||||
$this->assertStringContainsString('Auteur', $user);
|
||||
}
|
||||
|
||||
public function testValidationExceptionPassesThrough(): void
|
||||
{
|
||||
$val = new RuntimeException('Le titre est requis.');
|
||||
$user = ErrorHandler::userMessage($val);
|
||||
$this->assertSame('Le titre est requis.', $user);
|
||||
}
|
||||
|
||||
// ── Unknown exception types → generic fallback ───────────────────────────
|
||||
|
||||
public function testGenericExceptionPassesThrough(): void
|
||||
{
|
||||
$gen = new Exception('Something went wrong');
|
||||
$user = ErrorHandler::userMessage($gen);
|
||||
$this->assertStringContainsString('Something went wrong', $user);
|
||||
}
|
||||
|
||||
public function testTypeErrorReturnsGeneric(): void
|
||||
{
|
||||
$typeErr = new TypeError('htmlspecialchars(): Argument #1 must be string, array given');
|
||||
$user = ErrorHandler::userMessage($typeErr);
|
||||
$this->assertStringContainsString('Une erreur inattendue est survenue', $user);
|
||||
$this->assertStringNotContainsString('htmlspecialchars', $user);
|
||||
}
|
||||
|
||||
// ── log() does not crash ─────────────────────────────────────────────────
|
||||
|
||||
public function testLogWithContext(): void
|
||||
{
|
||||
// Should not throw
|
||||
ErrorHandler::log('test_context', new Exception('test message'), [
|
||||
'thesis_id' => 42,
|
||||
'slug' => '20250101-TEST1234',
|
||||
]);
|
||||
$this->assertTrue(true); // reached here = no crash
|
||||
}
|
||||
|
||||
public function testLogWithNullValues(): void
|
||||
{
|
||||
ErrorHandler::log('test_null', new PDOException('test'), ['id' => null, 'name' => 'foo']);
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testLogWithEmptyExtra(): void
|
||||
{
|
||||
ErrorHandler::log('test_empty', new RuntimeException('bare'));
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
// ── Real-world FK error patterns ─────────────────────────────────────────
|
||||
|
||||
public function testFkQuotedColumnNames(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_formats ("thesis_id", "format_id") VALUES (?, ?)');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Format(s)', $user);
|
||||
}
|
||||
|
||||
public function testFkUpdateStatement(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed UPDATE theses SET orientation_id = ? WHERE id = ?');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
$this->assertStringContainsString('Orientation', $user);
|
||||
}
|
||||
|
||||
public function testFkWithReferencesAndInsert(): void
|
||||
{
|
||||
$msg = $this->makeFkException('FOREIGN KEY constraint failed INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?) REFERENCES format_types');
|
||||
$user = ErrorHandler::userMessage($msg);
|
||||
// First matcher wins (INSERT table)
|
||||
$this->assertStringContainsString('Format(s)', $user);
|
||||
}
|
||||
}
|
||||
144
tests/phpunit/PureLogicTest.php
Normal file
144
tests/phpunit/PureLogicTest.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* PureLogicTest — PHPUnit version covering pure logic that isn't in other test classes.
|
||||
*
|
||||
* Includes: splitJuryByRole, collectCaptionPaths, detectFileType,
|
||||
* generateAuthorSlug, ExportController CSV_HEADERS consistency.
|
||||
*/
|
||||
class PureLogicTest extends TestCase
|
||||
{
|
||||
private function getTfeController(): TfeController
|
||||
{
|
||||
// We need a TfeController instance to test protected methods.
|
||||
// Use the anonymous subclass pattern.
|
||||
$db = TestDatabase::getInstance();
|
||||
return new class($db) extends TfeController {
|
||||
public function exposedSplitJuryByRole(array $jury): array { return $this->splitJuryByRole($jury); }
|
||||
public function exposedCollectCaptionPaths(array $files): array { return $this->collectCaptionPaths($files); }
|
||||
};
|
||||
}
|
||||
|
||||
private function getThesisCreateController(): ThesisCreateController
|
||||
{
|
||||
$db = TestDatabase::getInstance();
|
||||
return new class($db) extends ThesisCreateController {
|
||||
public function exposedDetectFileType(string $mimeType, string $ext): string { return $this->detectFileType($mimeType, $ext); }
|
||||
};
|
||||
}
|
||||
|
||||
// ── splitJuryByRole ──────────────────────────────────────────────────────
|
||||
|
||||
public function testSplitJuryByRoleAllRoles(): void
|
||||
{
|
||||
$ctrl = $this->getTfeController();
|
||||
$jury = [
|
||||
['name' => 'Alice', 'role' => 'president', 'is_external' => 0, 'is_ulb' => 0],
|
||||
['name' => 'Bob', 'role' => 'promoteur', 'is_external' => 0, 'is_ulb' => 0],
|
||||
['name' => 'Carol', 'role' => 'promoteur', 'is_external' => 1, 'is_ulb' => 1],
|
||||
['name' => 'Dave', 'role' => 'promoteur', 'is_external' => 1, 'is_ulb' => 0],
|
||||
['name' => 'Eve', 'role' => 'lecteur', 'is_external' => 0, 'is_ulb' => 0],
|
||||
['name' => 'Frank', 'role' => 'lecteur', 'is_external' => 1, 'is_ulb' => 0],
|
||||
];
|
||||
$split = $ctrl->exposedSplitJuryByRole($jury);
|
||||
|
||||
$this->assertSame(['Alice'], $split['presidents']);
|
||||
$this->assertSame(['Bob'], $split['internes']);
|
||||
$this->assertSame(['Carol'], $split['ulb']);
|
||||
$this->assertSame(['Dave'], $split['externes']);
|
||||
$this->assertSame(['Eve'], $split['lecteurs_internes']);
|
||||
$this->assertSame(['Frank'], $split['lecteurs_externes']);
|
||||
}
|
||||
|
||||
public function testSplitJuryByRoleEmptyNameSkipped(): void
|
||||
{
|
||||
$ctrl = $this->getTfeController();
|
||||
$jury = [['name' => '', 'role' => 'president', 'is_external' => 0, 'is_ulb' => 0]];
|
||||
$split = $ctrl->exposedSplitJuryByRole($jury);
|
||||
|
||||
$this->assertEmpty($split['presidents']);
|
||||
}
|
||||
|
||||
public function testSplitJuryByRoleEmptyJury(): void
|
||||
{
|
||||
$ctrl = $this->getTfeController();
|
||||
$split = $ctrl->exposedSplitJuryByRole([]);
|
||||
|
||||
$this->assertEmpty($split['presidents']);
|
||||
$this->assertEmpty($split['internes']);
|
||||
$this->assertEmpty($split['ulb']);
|
||||
$this->assertEmpty($split['externes']);
|
||||
$this->assertEmpty($split['lecteurs_internes']);
|
||||
$this->assertEmpty($split['lecteurs_externes']);
|
||||
}
|
||||
|
||||
// ── collectCaptionPaths ──────────────────────────────────────────────────
|
||||
|
||||
public function testCollectCaptionPathsVttByMime(): void
|
||||
{
|
||||
$ctrl = $this->getTfeController();
|
||||
$files = [
|
||||
['mime_type' => 'application/pdf', 'file_path' => 'main.pdf'],
|
||||
['mime_type' => 'text/vtt', 'file_path' => 'captions1.vtt'],
|
||||
['mime_type' => 'video/mp4', 'file_path' => 'video.mp4'],
|
||||
['mime_type' => 'text/plain', 'file_path' => 'captions2.vtt'],
|
||||
];
|
||||
$captions = $ctrl->exposedCollectCaptionPaths($files);
|
||||
|
||||
// Only the VTT files, in order
|
||||
$this->assertCount(2, $captions);
|
||||
$this->assertSame('captions1.vtt', $captions[0]);
|
||||
$this->assertSame('captions2.vtt', $captions[1]);
|
||||
}
|
||||
|
||||
public function testCollectCaptionPathsVttByExtension(): void
|
||||
{
|
||||
$ctrl = $this->getTfeController();
|
||||
$files = [
|
||||
['mime_type' => 'application/octet-stream', 'file_path' => 'sub.vtt'],
|
||||
['mime_type' => 'video/mp4', 'file_path' => 'video.mp4'],
|
||||
];
|
||||
$captions = $ctrl->exposedCollectCaptionPaths($files);
|
||||
|
||||
$this->assertCount(1, $captions);
|
||||
$this->assertSame('sub.vtt', $captions[0]);
|
||||
}
|
||||
|
||||
public function testCollectCaptionPathsNoVttReturnsEmpty(): void
|
||||
{
|
||||
$ctrl = $this->getTfeController();
|
||||
$files = [['mime_type' => 'video/mp4', 'file_path' => 'video.mp4']];
|
||||
|
||||
$this->assertEmpty($ctrl->exposedCollectCaptionPaths($files));
|
||||
}
|
||||
|
||||
// ── detectFileType ───────────────────────────────────────────────────────
|
||||
|
||||
public function testDetectFileTypeByMime(): void
|
||||
{
|
||||
$ctrl = $this->getThesisCreateController();
|
||||
|
||||
$this->assertSame('caption', $ctrl->exposedDetectFileType('text/vtt', 'vtt'));
|
||||
$this->assertSame('audio', $ctrl->exposedDetectFileType('audio/mpeg', 'mp3'));
|
||||
$this->assertSame('audio', $ctrl->exposedDetectFileType('audio/ogg', 'ogg'));
|
||||
$this->assertSame('video', $ctrl->exposedDetectFileType('video/mp4', 'mp4'));
|
||||
$this->assertSame('video', $ctrl->exposedDetectFileType('video/webm', 'webm'));
|
||||
$this->assertSame('main', $ctrl->exposedDetectFileType('application/pdf', 'pdf'));
|
||||
$this->assertSame('image', $ctrl->exposedDetectFileType('image/jpeg', 'jpg'));
|
||||
$this->assertSame('image', $ctrl->exposedDetectFileType('image/png', 'png'));
|
||||
$this->assertSame('other', $ctrl->exposedDetectFileType('application/zip', 'zip'));
|
||||
}
|
||||
|
||||
public function testDetectFileTypeByExtensionFallback(): void
|
||||
{
|
||||
$ctrl = $this->getThesisCreateController();
|
||||
|
||||
$this->assertSame('audio', $ctrl->exposedDetectFileType('application/octet-stream', 'mp3'));
|
||||
$this->assertSame('video', $ctrl->exposedDetectFileType('application/octet-stream', 'mp4'));
|
||||
$this->assertSame('main', $ctrl->exposedDetectFileType('application/octet-stream', 'pdf'));
|
||||
$this->assertSame('image', $ctrl->exposedDetectFileType('application/octet-stream', 'webp'));
|
||||
$this->assertSame('caption', $ctrl->exposedDetectFileType('application/octet-stream', 'vtt'));
|
||||
}
|
||||
}
|
||||
52
tests/phpunit/SearchControllerTest.php
Normal file
52
tests/phpunit/SearchControllerTest.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* SearchControllerTest — Regression test for SearchController::handleSearch()
|
||||
* always returning a 'coverMap' key.
|
||||
*/
|
||||
class SearchControllerTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
TestDatabase::resetData();
|
||||
TestDatabase::seedBasicThesis('Test Thesis', 'Author', 2024);
|
||||
|
||||
// Set up GET for SearchController (it reads from $_GET)
|
||||
$_GET = ['query' => ''];
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_GET = [];
|
||||
}
|
||||
|
||||
public function testHandleSearchReturnsCoverMapKey(): void
|
||||
{
|
||||
$db = TestDatabase::getInstance();
|
||||
$rateLimit = new RateLimit(1000, 60, sys_get_temp_dir() . '/xamxam_rl_test_' . uniqid());
|
||||
$searchCtrl = new SearchController($db, $rateLimit);
|
||||
|
||||
$vars = $searchCtrl->handleSearch();
|
||||
|
||||
$this->assertArrayHasKey('coverMap', $vars);
|
||||
$this->assertIsArray($vars['coverMap']);
|
||||
}
|
||||
|
||||
public function testCoverMapContainsKnownThesis(): void
|
||||
{
|
||||
$pdo = TestDatabase::getPDO();
|
||||
$thesisId = $pdo->query('SELECT id FROM theses LIMIT 1')->fetchColumn();
|
||||
|
||||
$db = TestDatabase::getInstance();
|
||||
$rateLimit = new RateLimit(1000, 60, sys_get_temp_dir() . '/xamxam_rl_test2_' . uniqid());
|
||||
$searchCtrl = new SearchController($db, $rateLimit);
|
||||
|
||||
$vars = $searchCtrl->handleSearch();
|
||||
|
||||
if (!empty($vars['results'])) {
|
||||
$this->assertArrayHasKey((int)$thesisId, $vars['coverMap']);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user