mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
341 lines
11 KiB
PHP
341 lines
11 KiB
PHP
<?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');
|
||
|
||
$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);
|
||
}
|
||
}
|