Files
xamxam/tests/phpunit/MediaControllerVisibilityTest.php
Pontoporeia 6e1fc6a781 Add admin-only route to open Interdit thesis files from backoffice:
- reliable tab title /favicon wrapper,
- Content-Disposition filename,
- admin media route hardened to thesis-file prefixes only (defense-in-depth)
2026-09-18 16:26:36 +02:00

146 lines
5.6 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
/**
* MediaControllerVisibilityTest — integration tests for the file-visibility
* gate that MediaController relies on (Database::getFileVisibility).
*
* The MediaController::handle() public route blocks thesis files whose owner is
* 'Interdit' (access_type_id=3) with HTTP 403. The dedicated admin route
* (/admin/media.php) calls handle(adminBypass: true) to lift that single gate
* after AdminAuth::requireLogin() has confirmed an administrator session.
*
* These tests pin down the data contract (which access_type_id maps to which
* outcome) so a change to the gate logic can't silently break either route.
*/
class MediaControllerVisibilityTest extends TestCase
{
private Database $db;
protected function setUp(): void
{
TestDatabase::resetData();
$this->db = TestDatabase::getInstance();
require_once APP_ROOT . '/src/Controllers/MediaController.php';
}
/**
* An 'Interdit' thesis file (access_type_id=3) must be flagged as
* forbidden so the public route returns 403 (and only the admin bypass
* may open it).
*/
public function testInterditThesisFileIsFlaggedForbidden(): void
{
[$authorId, $thesisId] = TestDatabase::seedBasicThesis('Interdit TFE', 'Author', 2024);
$pdo = TestDatabase::getPDO();
$pdo->prepare('UPDATE theses SET access_type_id = 3 WHERE id = ?')
->execute([$thesisId]);
$pdo->prepare(
"INSERT INTO thesis_files (thesis_id, file_type, file_path, file_name, file_size, mime_type)
VALUES (?, 'main', ?, 'rapport.pdf', 0, 'application/pdf')"
)->execute([$thesisId, 'documents/2024-001/rapport.pdf']);
$accessTypeId = $this->db->getFileVisibility('documents/2024-001/rapport.pdf');
$this->assertSame(3, $accessTypeId);
}
/**
* A 'Libre' thesis file (access_type_id=1) is served publicly by /media —
* never blocked by the gate.
*/
public function testLibreThesisFileIsNotFlaggedForbidden(): void
{
[$authorId, $thesisId] = TestDatabase::seedBasicThesis('Libre TFE', 'Author', 2024);
$pdo = TestDatabase::getPDO();
$pdo->prepare('UPDATE theses SET access_type_id = 1 WHERE id = ?')
->execute([$thesisId]);
$accessTypeId = $this->db->getFileVisibility('documents/2024-001/cover.jpg');
$this->assertSame(1, $accessTypeId);
}
/**
* A path that belongs to no thesis file resolves to null — the gate is
* a no-op (file still subject to MIME + jail checks in MediaController).
*/
public function testUnrelatedPathReturnsNull(): void
{
// No files seeded at all — seed an Interdit thesis without entries.
$pdo = TestDatabase::getPDO();
$pdo->prepare(
"INSERT INTO theses (title, year, identifier, is_published, objet, access_type_id)
VALUES ('Orphan', 2024, '2024-999', 1, 'tfe', 3)"
)->execute();
$accessTypeId = $this->db->getFileVisibility('theses/2024-999/unknown.pdf');
$this->assertNull($accessTypeId);
}
// ── getFileDisplayName (tab title / download name) ────────────────────────
/**
* getFileDisplayName returns the original uploaded file_name for a known
* thesis file — used to set a useful browser tab title instead of 'media.php'.
*/
public function testFileDisplayNameFromStoredFileName(): void
{
[$authorId, $thesisId] = TestDatabase::seedBasicThesis('Display Name', 'Author', 2024);
$pdo = TestDatabase::getPDO();
$pdo->prepare(
"INSERT INTO thesis_files (thesis_id, file_type, file_path, file_name, file_size, mime_type)
VALUES (?, 'main', 'tfe/2024-001/rapport.pdf', 'Note de synthese — Étude erg.pdf', 0, 'application/pdf')"
)->execute([$thesisId]);
$this->assertSame(
'Note de synthese — Étude erg.pdf',
$this->db->getFileDisplayName('tfe/2024-001/rapport.pdf')
);
}
/**
* getFileDisplayName returns null for a path that belongs to no thesis file,
* so MediaController falls back to the basename of the requested path.
*/
public function testFileDisplayNameUnknownPathReturnsNull(): void
{
$this->assertNull($this->db->getFileDisplayName('theses/2024-999/unknown.pdf'));
}
// ── isThesisFilePath (admin-bypass defense-in-depth) ──────────────────────
/**
* The admin bypass route is restricted to thesis-file prefixes only.
* All five storage layouts (tfe/ these/ frart/ documents/ theses/) count;
* any other storage path (schema, db, tmp, backups…) is rejected.
*/
public function testIsThesisFilePathRecognisesAllThesisPrefixes(): void
{
foreach (['tfe', 'these', 'frart', 'documents', 'theses'] as $prefix) {
$this->assertTrue(
MediaController::isThesisFilePath($prefix . '/2024/2024-001/rapport.pdf'),
"Expected '{$prefix}/…' to be a thesis file path"
);
}
}
public function testIsThesisFilePathRejectsNonThesisStoragePaths(): void
{
foreach ([
'schema.sql',
'xamxam.db',
'tmp/evil.php',
'backups/xamxam-2024.db',
'cache/foo.png',
] as $nonThesisPath) {
$this->assertFalse(
MediaController::isThesisFilePath($nonThesisPath),
"Expected '{$nonThesisPath}' to be rejected as a non-thesis path"
);
}
}
}