Add integration tests (Phase 2: DatabaseExtended, ShareLinkExtended, RateLimitExtended) and controller validation tests (Phase 3: ThesisCreate, ThesisEdit, AutofocusField)

This commit is contained in:
Pontoporeia
2026-05-20 01:51:41 +02:00
parent 7a4d0fafb2
commit 93625d09b5
10 changed files with 1543 additions and 8 deletions

View File

@@ -0,0 +1,341 @@
<?php
use PHPUnit\Framework\TestCase;
/**
* ThesisCreateValidationTest — Tests for ThesisCreateController::validateAndSanitise()
* and related validation logic.
*
* validateAndSanitise() is private; we test via a thin subclass or by driving
* the submit() method with controlled inputs. For pure validation tests,
* we use reflection.
*/
class ThesisCreateValidationTest extends TestCase
{
private PDO $pdo;
protected function setUp(): void
{
TestDatabase::resetData();
$this->pdo = TestDatabase::getPDO();
}
/**
* Invoke the private validateAndSanitise() method via reflection.
*/
private function validate(array $post, bool $adminMode = false): array
{
$ref = new ReflectionMethod(ThesisCreateController::class, 'validateAndSanitise');
$ref->setAccessible(true);
$db = TestDatabase::getInstance();
$ctrl = new ThesisCreateController($db);
return $ref->invoke($ctrl, $post, $adminMode);
}
/**
* Build minimal valid POST data for a submission.
*/
private function validPost(): array
{
return [
'auteurice' => 'John Doe',
'année' => '2024',
'orientation' => '1',
'ap' => '1',
'finality' => '1',
'titre' => 'My Thesis Title',
'synopsis' => 'A compelling synopsis.',
'jury_promoteur' => ['Promoteur One'],
'jury_lecteur_interne' => ['Lecteur Interne'],
'jury_lecteur_externe' => ['Lecteur Externe'],
'tag' => ['art', 'design', 'research'],
'languages' => ['1'],
'formats' => ['2'],
'access_type_id' => '1',
'license_id' => '1',
'objet' => 'tfe',
];
}
// ── Valid submission ─────────────────────────────────────────────────────
public function testValidSubmissionReturnsCleanedData(): void
{
$data = $this->validate($this->validPost());
$this->assertSame('My Thesis Title', $data['titre']);
$this->assertSame(2024, $data['annee']);
$this->assertSame('John Doe', $data['authorNames'][0]);
$this->assertNotEmpty($data['juryMembers']);
$this->assertCount(3, $data['keywords']);
}
// ── Missing required fields ──────────────────────────────────────────────
public function testMissingTitleThrowsException(): void
{
$post = $this->validPost();
$post['titre'] = '';
$this->expectException(Exception::class);
$this->expectExceptionMessage('Titre du TFE');
$this->validate($post);
}
public function testMissingAuthorsThrowsException(): void
{
$post = $this->validPost();
$post['auteurice'] = '';
$this->expectException(Exception::class);
$this->expectExceptionMessage('Auteur');
$this->validate($post);
}
public function testMissingSynopsisThrowsException(): void
{
$post = $this->validPost();
$post['synopsis'] = '';
$this->expectException(Exception::class);
$this->expectExceptionMessage('Synopsis');
$this->validate($post);
}
public function testMissingOrientationInNonAdminModeThrowsException(): void
{
$post = $this->validPost();
$post['orientation'] = '';
$this->expectException(Exception::class);
$this->expectExceptionMessage('orientation');
$this->validate($post);
}
public function testMissingAPProgramInNonAdminModeThrowsException(): void
{
$post = $this->validPost();
$post['ap'] = '';
$this->expectException(Exception::class);
$this->expectExceptionMessage('Atelier Pratique');
$this->validate($post);
}
public function testMissingFinalityInNonAdminModeThrowsException(): void
{
$post = $this->validPost();
$post['finality'] = '';
$this->expectException(Exception::class);
$this->expectExceptionMessage('finalité');
$this->validate($post);
}
// ── Invalid year ─────────────────────────────────────────────────────────
public function testInvalidYearFormatRejected(): void
{
$post = $this->validPost();
$post['année'] = 'not-a-year';
$this->expectException(Exception::class);
$this->expectExceptionMessage('Année invalide');
$this->validate($post);
}
public function testYearZeroRejected(): void
{
$post = $this->validPost();
$post['année'] = '0';
$this->expectException(Exception::class);
$this->expectExceptionMessage('Année invalide');
$this->validate($post);
}
public function testYearBefore2000Rejected(): void
{
$post = $this->validPost();
$post['année'] = '1999';
$this->expectException(Exception::class);
$this->expectExceptionMessage('Année invalide');
$this->validate($post);
}
public function testFarFutureYearRejected(): void
{
$post = $this->validPost();
$post['année'] = (string)((int)date('Y') + 5);
$this->expectException(Exception::class);
$this->expectExceptionMessage('Année invalide');
$this->validate($post);
}
public function testCurrentYearAccepted(): void
{
$post = $this->validPost();
$post['année'] = (string)(int)date('Y');
$data = $this->validate($post);
$this->assertSame((int)date('Y'), $data['annee']);
}
// ── Malformed URL ────────────────────────────────────────────────────────
public function testMalformedUrlRejected(): void
{
$post = $this->validPost();
$post['lien'] = 'not-a-valid-url';
$this->expectException(Exception::class);
$this->expectExceptionMessage('Lien URL invalide');
$this->validate($post);
}
public function testValidUrlAccepted(): void
{
$post = $this->validPost();
$post['lien'] = 'https://example.com';
$data = $this->validate($post);
$this->assertSame('https://example.com', $data['lien']);
}
// ── Tag deduplication ────────────────────────────────────────────────────
public function testDuplicateTagsAreDeduplicated(): void
{
$post = $this->validPost();
$post['tag'] = ['art', 'Art', 'ART', 'design', 'research', 'philosophy'];
$data = $this->validate($post);
// Tags are lowercased and deduplicated: art×3 + design + research + philosophy = 4
$this->assertCount(4, $data['keywords']);
$expected = ['art', 'design', 'philosophy', 'research'];
sort($data['keywords']);
$this->assertSame($expected, $data['keywords']);
}
public function testMaxTenKeywordsEnforced(): void
{
$post = $this->validPost();
$post['tag'] = range(1, 12);
$data = $this->validate($post);
$this->assertCount(10, $data['keywords']);
}
// ── XSS escaping ─────────────────────────────────────────────────────────
public function testXssPayloadStrippedFromTitle(): void
{
$post = $this->validPost();
$post['titre'] = '<script>alert("xss")</script>Clean Title';
$data = $this->validate($post);
$this->assertStringNotContainsString('<script>', $data['titre']);
$this->assertStringContainsString('Clean Title', $data['titre']);
}
public function testHtmlInSynopsisStripped(): void
{
$post = $this->validPost();
$post['synopsis'] = '<b>Bold</b> synopsis <img src=x onerror=alert(1)>';
$data = $this->validate($post);
$this->assertStringNotContainsString('<b>', $data['synopsis']);
$this->assertStringNotContainsString('<img', $data['synopsis']);
$this->assertStringContainsString('Bold synopsis', $data['synopsis']);
}
// ── Author processing ────────────────────────────────────────────────────
public function testMultipleAuthorsAreSorted(): void
{
$post = $this->validPost();
$post['auteurice'] = 'Zoe, Alice, Bob';
$data = $this->validate($post);
$this->assertSame(['Alice', 'Bob', 'Zoe'], $data['authorNames']);
}
// ── Jury validation ──────────────────────────────────────────────────────
public function testMissingPromoteurInNonAdminModeThrowsException(): void
{
$post = $this->validPost();
$post['jury_promoteur'] = [];
$this->expectException(Exception::class);
$this->expectExceptionMessage('promoteur');
$this->validate($post);
}
public function testMissingLecteurInterneInNonAdminModeThrowsException(): void
{
$post = $this->validPost();
$post['jury_lecteur_interne'] = [];
$this->expectException(Exception::class);
$this->expectExceptionMessage('lecteur·ice interne');
$this->validate($post);
}
// ── Language validation ──────────────────────────────────────────────────
public function testMissingLanguagesInNonAdminModeThrowsException(): void
{
$post = $this->validPost();
unset($post['languages']);
$this->expectException(Exception::class);
$this->expectExceptionMessage('langue');
$this->validate($post);
}
// ── Format validation ────────────────────────────────────────────────────
public function testMissingFormatsInNonAdminModeThrowsException(): void
{
$post = $this->validPost();
unset($post['formats']);
$this->expectException(Exception::class);
$this->expectExceptionMessage('format');
$this->validate($post);
}
// ── License validation ───────────────────────────────────────────────────
public function testMissingLicenseWithLibreAccessThrowsException(): void
{
$post = $this->validPost();
$post['access_type_id'] = '1';
$post['license_id'] = '';
$this->expectException(Exception::class);
$this->expectExceptionMessage('licence');
$this->validate($post);
}
}