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('