mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
Add integration tests (Phase 2: DatabaseExtended, ShareLinkExtended, RateLimitExtended) and controller validation tests (Phase 3: ThesisCreate, ThesisEdit, AutofocusField)
This commit is contained in:
341
tests/phpunit/ThesisCreateValidationTest.php
Normal file
341
tests/phpunit/ThesisCreateValidationTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user