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'); $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'] = 'Clean Title'; $data = $this->validate($post); $this->assertStringNotContainsString('