mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
test: add ShareLinkTest + PureLogicTest (TDD), fix coverMap undefined in SearchController
This commit is contained in:
450
app/tests/Unit/FormSaveTest.php
Normal file
450
app/tests/Unit/FormSaveTest.php
Normal file
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Form Save Round-Trip Test
|
||||
*
|
||||
* TDD: verifies that every field the form collects is persisted and can be
|
||||
* read back from the database after create and edit operations.
|
||||
*
|
||||
* Covered fields:
|
||||
* - titre, subtitle, synopsis, année
|
||||
* - orientation, ap, finality
|
||||
* - duration_pages, duration_minutes, has_annexes → file_size_info + individual columns
|
||||
* - languages (checkboxes), language_autre (free-text)
|
||||
* - formats (checkboxes)
|
||||
* - jury (promoteur, lecteur interne, lecteur externe)
|
||||
* - tags / keywords
|
||||
* - lien (baiu_link), license_id, access_type_id
|
||||
* - objet
|
||||
* - context_note
|
||||
* - remarks, jury_points, exemplaire_baiu, exemplaire_erg, cc4r (backoffice)
|
||||
* - is_published
|
||||
*/
|
||||
|
||||
putenv('DB_ENV=test');
|
||||
|
||||
if (!defined('APP_ROOT')) {
|
||||
define('APP_ROOT', dirname(__DIR__, 2));
|
||||
}
|
||||
if (!defined('STORAGE_ROOT')) {
|
||||
define('STORAGE_ROOT', APP_ROOT . '/storage');
|
||||
}
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/Controllers/ThesisCreateController.php';
|
||||
require_once APP_ROOT . '/src/Controllers/ThesisEditController.php';
|
||||
require_once APP_ROOT . '/src/DuplicateThesisException.php';
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Build a minimal-but-complete POST payload.
|
||||
* Accepts overrides to test specific fields.
|
||||
*/
|
||||
function buildPost(Database $db, array $overrides = []): array
|
||||
{
|
||||
$orientations = $db->getAllOrientations();
|
||||
$apPrograms = $db->getAllAPPrograms();
|
||||
$finalityTypes = $db->getAllFinalityTypes();
|
||||
$languages = $db->getAllLanguages();
|
||||
$formatTypes = $db->getAllFormatTypes();
|
||||
$licenseTypes = $db->getAllLicenseTypes();
|
||||
|
||||
if (empty($orientations) || empty($apPrograms) || empty($finalityTypes)
|
||||
|| empty($languages) || empty($formatTypes) || empty($licenseTypes)) {
|
||||
throw new RuntimeException('Lookup tables empty — cannot build POST fixture');
|
||||
}
|
||||
|
||||
$base = [
|
||||
'titre' => 'Test TFE Title',
|
||||
'subtitle' => 'Test Subtitle',
|
||||
'auteurice' => 'Doe, Jane',
|
||||
'mail' => 'jane@example.com',
|
||||
'synopsis' => 'A short synopsis for testing purposes.',
|
||||
'année' => '2025',
|
||||
'orientation' => (string)$orientations[0]['id'],
|
||||
'ap' => (string)$apPrograms[0]['id'],
|
||||
'finality' => (string)$finalityTypes[0]['id'],
|
||||
'duration_pages' => '120',
|
||||
'duration_minutes' => '',
|
||||
'has_annexes' => '',
|
||||
'languages' => [(string)$languages[0]['id']],
|
||||
'language_autre' => '',
|
||||
'formats' => [(string)$formatTypes[0]['id']],
|
||||
'tag' => 'art, test',
|
||||
'jury_promoteur' => 'Prof. Smith',
|
||||
'jury_lecteur_interne' => ['Dr. Internal'],
|
||||
'jury_lecteur_externe' => ['Dr. External'],
|
||||
'license_id' => (string)$licenseTypes[0]['id'],
|
||||
'license_custom' => '',
|
||||
'access_type_id' => '2',
|
||||
'objet' => 'tfe',
|
||||
'lien' => '',
|
||||
'context_note' => '',
|
||||
'remarks' => '',
|
||||
'jury_points' => '',
|
||||
'exemplaire_baiu' => '',
|
||||
'exemplaire_erg' => '',
|
||||
'cc2r' => '',
|
||||
'is_published' => '',
|
||||
];
|
||||
|
||||
return array_merge($base, $overrides);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert helper — throws on failure, echoes on pass.
|
||||
*/
|
||||
function assertEq(mixed $expected, mixed $actual, string $label): void
|
||||
{
|
||||
if ($expected == $actual) {
|
||||
echo " ✓ $label\n";
|
||||
} else {
|
||||
$e = var_export($expected, true);
|
||||
$a = var_export($actual, true);
|
||||
throw new RuntimeException("FAIL $label\n expected: $e\n actual: $a");
|
||||
}
|
||||
}
|
||||
|
||||
function assertContains(mixed $needle, array $haystack, string $label): void
|
||||
{
|
||||
if (in_array($needle, $haystack, false)) {
|
||||
echo " ✓ $label\n";
|
||||
} else {
|
||||
$a = implode(', ', $haystack);
|
||||
throw new RuntimeException("FAIL $label: $needle not in [$a]");
|
||||
}
|
||||
}
|
||||
|
||||
function assertNotEmpty(mixed $value, string $label): void
|
||||
{
|
||||
if (!empty($value)) {
|
||||
echo " ✓ $label\n";
|
||||
} else {
|
||||
throw new RuntimeException("FAIL $label: value is empty");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test setup ────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "Form Save Round-Trip Test\n";
|
||||
echo "=========================\n\n";
|
||||
|
||||
$db = Database::getInstance();
|
||||
$createCtrl = new ThesisCreateController($db);
|
||||
$editCtrl = new ThesisEditController($db);
|
||||
|
||||
$createdIds = [];
|
||||
|
||||
try {
|
||||
|
||||
// =========================================================================
|
||||
// TEST 1: Create — basic fields persisted
|
||||
// =========================================================================
|
||||
echo "Test 1: Create — basic fields persisted\n";
|
||||
$post = buildPost($db, [
|
||||
'titre' => 'Round-trip test titre',
|
||||
'subtitle' => 'Round-trip subtitle',
|
||||
'synopsis' => 'Round-trip synopsis',
|
||||
'année' => '2025',
|
||||
]);
|
||||
|
||||
$thesisId = $createCtrl->submit($post, []);
|
||||
$createdIds[] = $thesisId;
|
||||
$row = $db->getThesis($thesisId);
|
||||
|
||||
assertEq('Round-trip test titre', $row['title'], 'title saved');
|
||||
assertEq('Round-trip subtitle', $row['subtitle'], 'subtitle saved');
|
||||
assertEq('Round-trip synopsis', $row['synopsis'], 'synopsis saved');
|
||||
assertEq(2025, (int)$row['year'], 'year saved');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 2: Create — duration_pages saved as individual column
|
||||
// =========================================================================
|
||||
echo "Test 2: Create — duration_pages saved as individual column\n";
|
||||
$post = buildPost($db, [
|
||||
'titre' => 'Duration pages test',
|
||||
'duration_pages' => '84',
|
||||
]);
|
||||
|
||||
$thesisId = $createCtrl->submit($post, []);
|
||||
$createdIds[] = $thesisId;
|
||||
$raw = $db->getThesisRawFields($thesisId);
|
||||
|
||||
assertEq(84, (int)$raw['duration_pages'], 'duration_pages column saved');
|
||||
assertEq(null, $raw['duration_minutes'], 'duration_minutes null when not set');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 3: Create — duration_minutes saved as individual column
|
||||
// =========================================================================
|
||||
echo "Test 3: Create — duration_minutes saved as individual column\n";
|
||||
$post = buildPost($db, [
|
||||
'titre' => 'Duration minutes test',
|
||||
'duration_pages' => '',
|
||||
'duration_minutes' => '42',
|
||||
]);
|
||||
|
||||
$thesisId = $createCtrl->submit($post, []);
|
||||
$createdIds[] = $thesisId;
|
||||
$raw = $db->getThesisRawFields($thesisId);
|
||||
|
||||
assertEq(null, $raw['duration_pages'], 'duration_pages null when not set');
|
||||
assertEq(42, (int)$raw['duration_minutes'], 'duration_minutes column saved');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 4: Create — language_autre creates and links new language
|
||||
// =========================================================================
|
||||
echo "Test 4: Create — language_autre creates and links new language\n";
|
||||
$uniqueLang = 'TestLang_' . bin2hex(random_bytes(4));
|
||||
$post = buildPost($db, [
|
||||
'titre' => 'Language autre test',
|
||||
'languages' => [], // no checkbox
|
||||
'language_autre' => $uniqueLang,
|
||||
]);
|
||||
|
||||
$thesisId = $createCtrl->submit($post, []);
|
||||
$createdIds[] = $thesisId;
|
||||
|
||||
$langIds = $db->getThesisLanguageIds($thesisId);
|
||||
$allLangs = $db->getAllLanguages();
|
||||
$found = array_filter($allLangs, fn($l) => $l['name'] === $uniqueLang);
|
||||
assertNotEmpty($found, "language '$uniqueLang' created in languages table");
|
||||
|
||||
$createdLangId = (int)array_values($found)[0]['id'];
|
||||
assertContains((string)$createdLangId, array_map('strval', $langIds),
|
||||
'language_autre ID linked to thesis');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 5: Create — language_autre + checkbox together
|
||||
// =========================================================================
|
||||
echo "Test 5: Create — language_autre appended alongside checked languages\n";
|
||||
$db2 = Database::getInstance();
|
||||
$allLangs = $db2->getAllLanguages();
|
||||
$uniqueLang2 = 'TestLang2_' . bin2hex(random_bytes(4));
|
||||
$post = buildPost($db, [
|
||||
'titre' => 'Language combo test',
|
||||
'languages' => [(string)$allLangs[0]['id']],
|
||||
'language_autre' => $uniqueLang2,
|
||||
]);
|
||||
|
||||
$thesisId = $createCtrl->submit($post, []);
|
||||
$createdIds[] = $thesisId;
|
||||
$langIds = $db->getThesisLanguageIds($thesisId);
|
||||
|
||||
assertContains((string)$allLangs[0]['id'], array_map('strval', $langIds),
|
||||
'checkbox language linked');
|
||||
|
||||
$found2 = array_filter($db->getAllLanguages(), fn($l) => $l['name'] === $uniqueLang2);
|
||||
$createdLang2 = (int)array_values($found2)[0]['id'];
|
||||
assertContains((string)$createdLang2, array_map('strval', $langIds),
|
||||
'language_autre also linked');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 6: Edit — duration_pages / duration_minutes updated
|
||||
// =========================================================================
|
||||
echo "Test 6: Edit — duration_pages / duration_minutes updated\n";
|
||||
$post = buildPost($db, ['titre' => 'Edit duration test']);
|
||||
$thesisId = $createCtrl->submit($post, []);
|
||||
$createdIds[] = $thesisId;
|
||||
|
||||
$editPost = buildPost($db, [
|
||||
'titre' => 'Edit duration test',
|
||||
'duration_pages' => '99',
|
||||
'duration_minutes' => '30',
|
||||
]);
|
||||
$editCtrl->save($thesisId, $editPost, []);
|
||||
$raw = $db->getThesisRawFields($thesisId);
|
||||
|
||||
assertEq(99, (int)$raw['duration_pages'], 'duration_pages updated on edit');
|
||||
assertEq(30, (int)$raw['duration_minutes'], 'duration_minutes updated on edit');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 7: Edit — clearing duration_pages saves null
|
||||
// =========================================================================
|
||||
echo "Test 7: Edit — clearing duration_pages saves null\n";
|
||||
$editPost = buildPost($db, [
|
||||
'titre' => 'Edit duration test',
|
||||
'duration_pages' => '',
|
||||
'duration_minutes' => '',
|
||||
]);
|
||||
$editCtrl->save($thesisId, $editPost, []);
|
||||
$raw = $db->getThesisRawFields($thesisId);
|
||||
|
||||
assertEq(null, $raw['duration_pages'], 'duration_pages cleared to null');
|
||||
assertEq(null, $raw['duration_minutes'], 'duration_minutes cleared to null');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 8: Edit — languages pre-populated (checkboxes round-trip)
|
||||
// =========================================================================
|
||||
echo "Test 8: Edit — language checkboxes round-trip\n";
|
||||
$allLangs = $db->getAllLanguages();
|
||||
$lang1 = (string)$allLangs[0]['id'];
|
||||
$lang2 = (string)$allLangs[1]['id'];
|
||||
|
||||
$post = buildPost($db, [
|
||||
'titre' => 'Lang checkbox test',
|
||||
'languages' => [$lang1],
|
||||
]);
|
||||
$thesisId = $createCtrl->submit($post, []);
|
||||
$createdIds[] = $thesisId;
|
||||
|
||||
$editPost = buildPost($db, [
|
||||
'titre' => 'Lang checkbox test',
|
||||
'languages' => [$lang1, $lang2],
|
||||
]);
|
||||
$editCtrl->save($thesisId, $editPost, []);
|
||||
$langIds = $db->getThesisLanguageIds($thesisId);
|
||||
|
||||
assertContains($lang1, array_map('strval', $langIds), 'first language retained on edit');
|
||||
assertContains($lang2, array_map('strval', $langIds), 'second language added on edit');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 9: Edit — language_autre adds new language
|
||||
// =========================================================================
|
||||
echo "Test 9: Edit — language_autre creates and links on edit\n";
|
||||
$uniqueLang3 = 'EditLang_' . bin2hex(random_bytes(4));
|
||||
$editPost = buildPost($db, [
|
||||
'titre' => 'Lang checkbox test',
|
||||
'languages' => [$lang1],
|
||||
'language_autre' => $uniqueLang3,
|
||||
]);
|
||||
$editCtrl->save($thesisId, $editPost, []);
|
||||
|
||||
$langIds = $db->getThesisLanguageIds($thesisId);
|
||||
$found3 = array_filter($db->getAllLanguages(), fn($l) => $l['name'] === $uniqueLang3);
|
||||
assertNotEmpty($found3, "language '$uniqueLang3' created on edit");
|
||||
$createdLang3 = (int)array_values($found3)[0]['id'];
|
||||
assertContains((string)$createdLang3, array_map('strval', $langIds),
|
||||
'language_autre linked on edit');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 10: Create — backoffice fields persisted
|
||||
// =========================================================================
|
||||
echo "Test 10: Create — backoffice fields (remarks, jury_points, exemplaires, cc4r)\n";
|
||||
$post = buildPost($db, [
|
||||
'titre' => 'Backoffice fields test',
|
||||
'remarks' => 'Internal note here',
|
||||
'jury_points' => '15.5',
|
||||
'exemplaire_baiu' => '1',
|
||||
'exemplaire_erg' => '1',
|
||||
'cc2r' => '1',
|
||||
]);
|
||||
|
||||
$thesisId = $createCtrl->submit($post, []);
|
||||
$createdIds[] = $thesisId;
|
||||
$raw = $db->getThesisRawFields($thesisId);
|
||||
|
||||
assertEq('Internal note here', $raw['remarks'], 'remarks saved');
|
||||
assertEq(15.5, (float)$raw['jury_points'], 'jury_points saved');
|
||||
assertEq(1, (int)$raw['exemplaire_baiu'], 'exemplaire_baiu saved');
|
||||
assertEq(1, (int)$raw['exemplaire_erg'], 'exemplaire_erg saved');
|
||||
assertEq(1, (int)$raw['cc4r'], 'cc4r saved');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 11: Edit — backoffice fields updated
|
||||
// =========================================================================
|
||||
echo "Test 11: Edit — backoffice fields updated\n";
|
||||
$editPost = buildPost($db, [
|
||||
'titre' => 'Backoffice fields test',
|
||||
'remarks' => 'Updated note',
|
||||
'jury_points' => '18',
|
||||
'exemplaire_baiu' => '',
|
||||
'exemplaire_erg' => '1',
|
||||
'cc2r' => '',
|
||||
]);
|
||||
$editCtrl->save($thesisId, $editPost, []);
|
||||
$raw = $db->getThesisRawFields($thesisId);
|
||||
|
||||
assertEq('Updated note', $raw['remarks'], 'remarks updated');
|
||||
assertEq(18.0, (float)$raw['jury_points'], 'jury_points updated');
|
||||
assertEq(0, (int)$raw['exemplaire_baiu'], 'exemplaire_baiu cleared');
|
||||
assertEq(1, (int)$raw['exemplaire_erg'], 'exemplaire_erg retained');
|
||||
assertEq(0, (int)$raw['cc4r'], 'cc4r cleared');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 12: getOrCreateLanguage — idempotent
|
||||
// =========================================================================
|
||||
echo "Test 12: getOrCreateLanguage — idempotent (same name returns same ID)\n";
|
||||
$uniqueName = 'Idempotent_' . bin2hex(random_bytes(4));
|
||||
$id1 = $db->getOrCreateLanguage($uniqueName);
|
||||
$id2 = $db->getOrCreateLanguage($uniqueName);
|
||||
$id3 = $db->getOrCreateLanguage(strtolower($uniqueName)); // case-insensitive
|
||||
|
||||
assertEq($id1, $id2, 'same ID on second call');
|
||||
assertEq($id1, $id3, 'same ID with different case');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 13: Create — has_annexes flag in file_size_info
|
||||
// =========================================================================
|
||||
echo "Test 13: Create — has_annexes appended to file_size_info\n";
|
||||
$post = buildPost($db, [
|
||||
'titre' => 'Annexes test',
|
||||
'duration_pages' => '50',
|
||||
'has_annexes' => '1',
|
||||
]);
|
||||
$thesisId = $createCtrl->submit($post, []);
|
||||
$createdIds[] = $thesisId;
|
||||
$row = $db->getThesis($thesisId);
|
||||
|
||||
assertNotEmpty($row['file_size_info'], 'file_size_info not empty');
|
||||
if (strpos((string)$row['file_size_info'], 'annexe') === false) {
|
||||
throw new RuntimeException("FAIL has_annexes: 'annexe' not in file_size_info: " . $row['file_size_info']);
|
||||
}
|
||||
echo " ✓ 'annexe' present in file_size_info\n\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 14: Edit — context_note saved
|
||||
// =========================================================================
|
||||
echo "Test 14: Edit — context_note saved\n";
|
||||
$post = buildPost($db, ['titre' => 'Context note test']);
|
||||
$thesisId = $createCtrl->submit($post, []);
|
||||
$createdIds[] = $thesisId;
|
||||
|
||||
$editPost = buildPost($db, [
|
||||
'titre' => 'Context note test',
|
||||
'context_note' => 'A contextual note visible publicly.',
|
||||
]);
|
||||
$editCtrl->save($thesisId, $editPost, []);
|
||||
$raw = $db->getThesisRawFields($thesisId);
|
||||
|
||||
assertEq('A contextual note visible publicly.', $raw['context_note'], 'context_note saved');
|
||||
echo "\n";
|
||||
|
||||
echo "✅ All form save tests passed!\n";
|
||||
$result = true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo '❌ FAIL: ' . $e->getMessage() . "\n";
|
||||
$result = false;
|
||||
} finally {
|
||||
// Clean up test theses
|
||||
foreach ($createdIds as $id) {
|
||||
try { $db->deleteThesis($id); } catch (Exception $e) { /* ignore */ }
|
||||
}
|
||||
// Clean up test languages
|
||||
$allLangs = $db->getAllLanguages();
|
||||
foreach ($allLangs as $lang) {
|
||||
if (str_starts_with($lang['name'], 'TestLang_')
|
||||
|| str_starts_with($lang['name'], 'TestLang2_')
|
||||
|| str_starts_with($lang['name'], 'EditLang_')
|
||||
|| str_starts_with($lang['name'], 'Idempotent_')) {
|
||||
try {
|
||||
$db->getConnection()->prepare('DELETE FROM languages WHERE id = ?')->execute([$lang['id']]);
|
||||
} catch (Exception $e) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result ?? false;
|
||||
422
app/tests/Unit/PureLogicTest.php
Normal file
422
app/tests/Unit/PureLogicTest.php
Normal file
@@ -0,0 +1,422 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Pure Logic Unit Test
|
||||
*
|
||||
* Tests deterministic, I/O-free logic extracted from controllers:
|
||||
*
|
||||
* TfeController (via subclass to access protected methods):
|
||||
* - buildMetaDescription() — truncation at 160 chars, empty synopsis fallback
|
||||
* - resolveOgImage() — cover preferred over image, fallback chain
|
||||
* - splitJuryByRole() — correct binning of promoteur/lecteur/president/ulb
|
||||
* - collectCaptionPaths() — VTT files extracted in order
|
||||
*
|
||||
* ThesisCreateController (static / public helpers):
|
||||
* - autofocusFieldForError()
|
||||
* - detectFileType() — via subclass to access private
|
||||
* - generateAuthorSlug() — accent stripping, uppercase, underscore
|
||||
* - buildFileSizeInfo() — pages, minutes, annexes combinations
|
||||
*
|
||||
* ExportController:
|
||||
* - CSV_HEADERS count matches exportAllTheses() row width
|
||||
*
|
||||
* SearchController:
|
||||
* - handleSearch() return array always contains 'coverMap' key (regression for undefined var bug)
|
||||
*/
|
||||
|
||||
putenv('DB_ENV=test');
|
||||
|
||||
if (!defined('APP_ROOT')) {
|
||||
define('APP_ROOT', dirname(__DIR__, 2));
|
||||
}
|
||||
if (!defined('STORAGE_ROOT')) {
|
||||
define('STORAGE_ROOT', APP_ROOT . '/storage');
|
||||
}
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/RateLimit.php';
|
||||
require_once APP_ROOT . '/src/Controllers/TfeController.php';
|
||||
require_once APP_ROOT . '/src/Controllers/ThesisCreateController.php';
|
||||
require_once APP_ROOT . '/src/Controllers/ThesisEditController.php';
|
||||
require_once APP_ROOT . '/src/Controllers/SearchController.php';
|
||||
require_once APP_ROOT . '/src/Controllers/ExportController.php';
|
||||
|
||||
// ── Test harness helpers ──────────────────────────────────────────────────────
|
||||
|
||||
function plAssert(bool $cond, string $label): void
|
||||
{
|
||||
if ($cond) {
|
||||
echo " ✓ $label\n";
|
||||
} else {
|
||||
throw new RuntimeException("FAIL: $label");
|
||||
}
|
||||
}
|
||||
|
||||
function plAssertEq(mixed $expected, mixed $actual, string $label): void
|
||||
{
|
||||
if ($expected === $actual) {
|
||||
echo " ✓ $label\n";
|
||||
} else {
|
||||
$e = var_export($expected, true);
|
||||
$a = var_export($actual, true);
|
||||
throw new RuntimeException("FAIL $label\n expected: $e\n actual: $a");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Subclasses to expose protected / private methods ─────────────────────────
|
||||
|
||||
class TfeControllerTestable extends TfeController
|
||||
{
|
||||
public function testBuildMetaDescription(string $synopsis): string
|
||||
{
|
||||
return $this->buildMetaDescription($synopsis);
|
||||
}
|
||||
|
||||
public function testResolveOgImage(array $files): string
|
||||
{
|
||||
return $this->resolveOgImage($files);
|
||||
}
|
||||
|
||||
public function testSplitJuryByRole(array $jury): array
|
||||
{
|
||||
return $this->splitJuryByRole($jury);
|
||||
}
|
||||
|
||||
public function testCollectCaptionPaths(array $files): array
|
||||
{
|
||||
return $this->collectCaptionPaths($files);
|
||||
}
|
||||
}
|
||||
|
||||
class ThesisCreateControllerTestable extends ThesisCreateController
|
||||
{
|
||||
public function testDetectFileType(string $mimeType, string $ext, string $name): string
|
||||
{
|
||||
return $this->detectFileType($mimeType, $ext, $name);
|
||||
}
|
||||
|
||||
public function testGenerateAuthorSlug(string $name): string
|
||||
{
|
||||
return $this->generateAuthorSlug($name);
|
||||
}
|
||||
}
|
||||
|
||||
class ThesisEditControllerTestable extends ThesisEditController
|
||||
{
|
||||
public function testBuildFileSizeInfo(array $post): string
|
||||
{
|
||||
return $this->buildFileSizeInfo($post);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Setup ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "Pure Logic Unit Test\n";
|
||||
echo "====================\n\n";
|
||||
|
||||
$db = Database::getInstance();
|
||||
$tfe = new TfeControllerTestable($db);
|
||||
$createCtrl = new ThesisCreateControllerTestable($db);
|
||||
$editCtrl = new ThesisEditControllerTestable($db);
|
||||
|
||||
try {
|
||||
|
||||
// =========================================================================
|
||||
// SECTION A: TfeController helpers
|
||||
// =========================================================================
|
||||
|
||||
// ── A1: buildMetaDescription ──────────────────────────────────────────────
|
||||
echo "A1: buildMetaDescription — normal synopsis\n";
|
||||
$desc = $tfe->testBuildMetaDescription('A short synopsis.');
|
||||
plAssertEq('A short synopsis.', $desc, 'short synopsis returned as-is');
|
||||
echo "\n";
|
||||
|
||||
echo "A2: buildMetaDescription — synopsis over 160 chars truncated\n";
|
||||
$long = str_repeat('a', 200);
|
||||
$desc = $tfe->testBuildMetaDescription($long);
|
||||
plAssert(strlen($desc) <= 160, 'length <= 160');
|
||||
plAssert(str_ends_with($desc, '…'), 'ends with ellipsis');
|
||||
echo "\n";
|
||||
|
||||
echo "A3: buildMetaDescription — exactly 160 chars not truncated\n";
|
||||
$exact = str_repeat('b', 160);
|
||||
$desc = $tfe->testBuildMetaDescription($exact);
|
||||
plAssertEq($exact, $desc, '160-char synopsis not truncated');
|
||||
echo "\n";
|
||||
|
||||
echo "A4: buildMetaDescription — empty synopsis returns fallback\n";
|
||||
$desc = $tfe->testBuildMetaDescription('');
|
||||
plAssert($desc !== '', 'non-empty fallback returned');
|
||||
plAssert(strlen($desc) <= 160, 'fallback length <= 160');
|
||||
echo "\n";
|
||||
|
||||
echo "A5: buildMetaDescription — HTML tags stripped\n";
|
||||
$desc = $tfe->testBuildMetaDescription('<p>Hello <strong>world</strong></p>');
|
||||
plAssertEq('Hello world', $desc, 'HTML tags stripped');
|
||||
echo "\n";
|
||||
|
||||
// ── A6: resolveOgImage ────────────────────────────────────────────────────
|
||||
echo "A6: resolveOgImage — cover preferred over image files\n";
|
||||
$files = [
|
||||
['file_type' => 'image', 'file_path' => 'theses/2025/img.jpg'],
|
||||
['file_type' => 'cover', 'file_path' => 'covers/cover.jpg'],
|
||||
['file_type' => 'main', 'file_path' => 'theses/2025/main.pdf'],
|
||||
];
|
||||
$url = $tfe->testResolveOgImage($files);
|
||||
plAssert(str_contains($url, rawurlencode('covers/cover.jpg')), 'cover used when available');
|
||||
echo "\n";
|
||||
|
||||
echo "A7: resolveOgImage — falls back to first image when no cover\n";
|
||||
$files = [
|
||||
['file_type' => 'main', 'file_path' => 'theses/2025/main.pdf'],
|
||||
['file_type' => 'image', 'file_path' => 'theses/2025/img.jpg'],
|
||||
['file_type' => 'image', 'file_path' => 'theses/2025/img2.png'],
|
||||
];
|
||||
$url = $tfe->testResolveOgImage($files);
|
||||
plAssert(str_contains($url, rawurlencode('theses/2025/img.jpg')), 'first image file used as fallback');
|
||||
plAssert(!str_contains($url, rawurlencode('img2.png')), 'second image not used');
|
||||
echo "\n";
|
||||
|
||||
echo "A8: resolveOgImage — empty string when no image at all\n";
|
||||
$files = [
|
||||
['file_type' => 'main', 'file_path' => 'theses/2025/main.pdf'],
|
||||
['file_type' => 'audio', 'file_path' => 'theses/2025/audio.mp3'],
|
||||
];
|
||||
$url = $tfe->testResolveOgImage($files);
|
||||
plAssertEq('', $url, 'empty string when no image');
|
||||
echo "\n";
|
||||
|
||||
echo "A9: resolveOgImage — empty files array returns empty string\n";
|
||||
plAssertEq('', $tfe->testResolveOgImage([]), 'empty array → empty string');
|
||||
echo "\n";
|
||||
|
||||
// ── A10: splitJuryByRole ──────────────────────────────────────────────────
|
||||
echo "A10: splitJuryByRole — all roles correctly binned\n";
|
||||
$jury = [
|
||||
['name' => 'Alice', 'role' => 'president', 'is_external' => 0, 'is_ulb' => 0],
|
||||
['name' => 'Bob', 'role' => 'promoteur', 'is_external' => 0, 'is_ulb' => 0],
|
||||
['name' => 'Carol', 'role' => 'promoteur', 'is_external' => 1, 'is_ulb' => 1],
|
||||
['name' => 'Dave', 'role' => 'promoteur', 'is_external' => 1, 'is_ulb' => 0],
|
||||
['name' => 'Eve', 'role' => 'lecteur', 'is_external' => 0, 'is_ulb' => 0],
|
||||
['name' => 'Frank', 'role' => 'lecteur', 'is_external' => 1, 'is_ulb' => 0],
|
||||
];
|
||||
$split = $tfe->testSplitJuryByRole($jury);
|
||||
|
||||
plAssertEq(['Alice'], $split['presidents'], 'president');
|
||||
plAssertEq(['Bob'], $split['internes'], 'interne promoteur');
|
||||
plAssertEq(['Carol'], $split['ulb'], 'ulb promoteur');
|
||||
plAssertEq(['Dave'], $split['externes'], 'externe promoteur (non-ULB)');
|
||||
plAssertEq(['Eve'], $split['lecteurs_internes'], 'lecteur interne');
|
||||
plAssertEq(['Frank'], $split['lecteurs_externes'], 'lecteur externe');
|
||||
echo "\n";
|
||||
|
||||
echo "A11: splitJuryByRole — empty name skipped\n";
|
||||
$jury = [['name' => '', 'role' => 'president', 'is_external' => 0, 'is_ulb' => 0]];
|
||||
$split = $tfe->testSplitJuryByRole($jury);
|
||||
plAssertEq([], $split['presidents'], 'empty name not added');
|
||||
echo "\n";
|
||||
|
||||
echo "A12: splitJuryByRole — empty jury returns all-empty arrays\n";
|
||||
$split = $tfe->testSplitJuryByRole([]);
|
||||
plAssertEq([], $split['presidents'], 'presidents empty');
|
||||
plAssertEq([], $split['internes'], 'internes empty');
|
||||
plAssertEq([], $split['ulb'], 'ulb empty');
|
||||
plAssertEq([], $split['externes'], 'externes empty');
|
||||
plAssertEq([], $split['lecteurs_internes'], 'lecteurs_internes empty');
|
||||
plAssertEq([], $split['lecteurs_externes'], 'lecteurs_externes empty');
|
||||
echo "\n";
|
||||
|
||||
// ── A13: collectCaptionPaths ──────────────────────────────────────────────
|
||||
echo "A13: collectCaptionPaths — VTT files extracted in order\n";
|
||||
$files = [
|
||||
['mime_type' => 'application/pdf', 'file_path' => 'main.pdf'],
|
||||
['mime_type' => 'text/vtt', 'file_path' => 'captions1.vtt'],
|
||||
['mime_type' => 'video/mp4', 'file_path' => 'video.mp4'],
|
||||
['mime_type' => 'text/plain', 'file_path' => 'captions2.vtt'],
|
||||
];
|
||||
$captions = $tfe->testCollectCaptionPaths($files);
|
||||
plAssertEq(['captions1.vtt', 'captions2.vtt'], $captions, 'both VTT files returned in order');
|
||||
echo "\n";
|
||||
|
||||
echo "A14: collectCaptionPaths — .vtt extension without mime match\n";
|
||||
$files = [
|
||||
['mime_type' => 'application/octet-stream', 'file_path' => 'sub.vtt'],
|
||||
['mime_type' => 'video/mp4', 'file_path' => 'video.mp4'],
|
||||
];
|
||||
$captions = $tfe->testCollectCaptionPaths($files);
|
||||
plAssertEq(['sub.vtt'], $captions, '.vtt extension matches even with generic mime');
|
||||
echo "\n";
|
||||
|
||||
echo "A15: collectCaptionPaths — no VTT returns empty array\n";
|
||||
$files = [
|
||||
['mime_type' => 'video/mp4', 'file_path' => 'video.mp4'],
|
||||
];
|
||||
plAssertEq([], $tfe->testCollectCaptionPaths($files), 'empty array when no VTT');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// SECTION B: ThesisCreateController helpers
|
||||
// =========================================================================
|
||||
|
||||
// ── B1: autofocusFieldForError ────────────────────────────────────────────
|
||||
echo "B1: autofocusFieldForError — known error messages map to fields\n";
|
||||
$cases = [
|
||||
["Titre du mémoire", 'titre'],
|
||||
["Nom/Prénom/Pseudo", 'auteurice'],
|
||||
["Synopsis", 'synopsis'],
|
||||
["Année invalide", 'année'],
|
||||
["orientation", 'orientation'],
|
||||
["Atelier Pratique", 'ap'],
|
||||
["finalité", 'finality'],
|
||||
["langue", 'languages'],
|
||||
["promoteur", 'jury_promoteur'],
|
||||
["lecteur·ice interne", 'jury_lecteur_interne[]'],
|
||||
["lecteur·ice externe", 'jury_lecteur_externe[]'],
|
||||
["format", 'formats'],
|
||||
["licence", 'license_id'],
|
||||
["Lien URL", 'lien'],
|
||||
];
|
||||
foreach ($cases as [$message, $expected]) {
|
||||
$actual = ThesisCreateController::autofocusFieldForError($message);
|
||||
plAssertEq($expected, $actual, "\"$message\" → $expected");
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
echo "B2: autofocusFieldForError — unknown message returns null\n";
|
||||
plAssertEq(null, ThesisCreateController::autofocusFieldForError('completely unknown'), 'null for unknown');
|
||||
echo "\n";
|
||||
|
||||
// ── B3: detectFileType ────────────────────────────────────────────────────
|
||||
echo "B3: detectFileType — mime-based detection\n";
|
||||
$cases = [
|
||||
['text/vtt', 'vtt', 'caption.vtt', 'caption'],
|
||||
['audio/mpeg', 'mp3', 'track.mp3', 'audio'],
|
||||
['audio/ogg', 'ogg', 'track.ogg', 'audio'],
|
||||
['video/mp4', 'mp4', 'film.mp4', 'video'],
|
||||
['video/webm', 'webm', 'film.webm', 'video'],
|
||||
['application/pdf', 'pdf', 'report.pdf', 'main'],
|
||||
['image/jpeg', 'jpg', 'photo.jpg', 'image'],
|
||||
['image/png', 'png', 'photo.png', 'image'],
|
||||
['application/zip', 'zip', 'archive.zip', 'other'],
|
||||
];
|
||||
foreach ($cases as [$mime, $ext, $name, $expected]) {
|
||||
$actual = $createCtrl->testDetectFileType($mime, $ext, $name);
|
||||
plAssertEq($expected, $actual, "$name ($mime) → $expected");
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
echo "B4: detectFileType — extension-based fallback\n";
|
||||
// application/octet-stream with known extensions
|
||||
$cases = [
|
||||
['application/octet-stream', 'mp3', 'track.mp3', 'audio'],
|
||||
['application/octet-stream', 'mp4', 'video.mp4', 'video'],
|
||||
['application/octet-stream', 'pdf', 'doc.pdf', 'main'],
|
||||
['application/octet-stream', 'webp', 'img.webp', 'image'],
|
||||
['application/octet-stream', 'vtt', 'subs.vtt', 'caption'],
|
||||
];
|
||||
foreach ($cases as [$mime, $ext, $name, $expected]) {
|
||||
$actual = $createCtrl->testDetectFileType($mime, $ext, $name);
|
||||
plAssertEq($expected, $actual, "octet-stream + .$ext → $expected");
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
// ── B5: generateAuthorSlug ────────────────────────────────────────────────
|
||||
echo "B5: generateAuthorSlug — basic ASCII\n";
|
||||
plAssertEq('JANE_DOE', $createCtrl->testGenerateAuthorSlug('Jane Doe'), 'spaces to underscores, uppercase');
|
||||
plAssertEq('AUTHOR', $createCtrl->testGenerateAuthorSlug(''), 'empty → AUTHOR');
|
||||
echo "\n";
|
||||
|
||||
echo "B6: generateAuthorSlug — French accents stripped\n";
|
||||
plAssertEq('ELEONORE_DUPONT', $createCtrl->testGenerateAuthorSlug('Éléonore Dupont'), 'accents stripped');
|
||||
plAssertEq('FRANCOISE', $createCtrl->testGenerateAuthorSlug('Françoise'), 'ç → C');
|
||||
echo "\n";
|
||||
|
||||
echo "B7: generateAuthorSlug — multiple authors comma-separated\n";
|
||||
$slug = $createCtrl->testGenerateAuthorSlug('Alice Martin, Bob Durand');
|
||||
plAssert(str_contains($slug, 'ALICE'), 'contains ALICE');
|
||||
plAssert(str_contains($slug, 'BOB'), 'contains BOB');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// SECTION C: ThesisEditController — buildFileSizeInfo
|
||||
// =========================================================================
|
||||
|
||||
echo "C1: buildFileSizeInfo — pages only\n";
|
||||
$info = $editCtrl->testBuildFileSizeInfo(['duration_pages' => '84', 'duration_minutes' => '']);
|
||||
plAssertEq('84 pages', $info, 'pages only');
|
||||
echo "\n";
|
||||
|
||||
echo "C2: buildFileSizeInfo — minutes only\n";
|
||||
$info = $editCtrl->testBuildFileSizeInfo(['duration_pages' => '', 'duration_minutes' => '32']);
|
||||
plAssertEq('32 minutes', $info, 'minutes only');
|
||||
echo "\n";
|
||||
|
||||
echo "C3: buildFileSizeInfo — pages + minutes\n";
|
||||
$info = $editCtrl->testBuildFileSizeInfo(['duration_pages' => '84', 'duration_minutes' => '32']);
|
||||
plAssertEq('84 pages + 32 minutes', $info, 'pages + minutes');
|
||||
echo "\n";
|
||||
|
||||
echo "C4: buildFileSizeInfo — annexes appended\n";
|
||||
$info = $editCtrl->testBuildFileSizeInfo(['duration_pages' => '50', 'duration_minutes' => '', 'has_annexes' => '1']);
|
||||
plAssertEq('50 pages + annexe(s)', $info, 'annexes appended');
|
||||
echo "\n";
|
||||
|
||||
echo "C5: buildFileSizeInfo — annexes only (no pages/minutes)\n";
|
||||
$info = $editCtrl->testBuildFileSizeInfo(['duration_pages' => '', 'duration_minutes' => '', 'has_annexes' => '1']);
|
||||
plAssertEq('Annexe(s)', $info, 'annexes only');
|
||||
echo "\n";
|
||||
|
||||
echo "C6: buildFileSizeInfo — empty fields = empty string\n";
|
||||
$info = $editCtrl->testBuildFileSizeInfo(['duration_pages' => '', 'duration_minutes' => '']);
|
||||
plAssertEq('', $info, 'empty when nothing set');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// SECTION D: ExportController — CSV column count consistency
|
||||
// =========================================================================
|
||||
|
||||
echo "D1: ExportController — CSV_HEADERS count matches row column count\n";
|
||||
$export = new ExportController($db);
|
||||
$rows = $export->exportAllTheses();
|
||||
$headerCount = count(ExportController::CSV_HEADERS);
|
||||
|
||||
plAssert($headerCount > 0, 'CSV_HEADERS is non-empty');
|
||||
|
||||
if (!empty($rows)) {
|
||||
foreach ($rows as $i => $row) {
|
||||
if (count($row) !== $headerCount) {
|
||||
throw new RuntimeException(
|
||||
"FAIL: row $i has " . count($row) . " columns, expected $headerCount"
|
||||
);
|
||||
}
|
||||
}
|
||||
echo " ✓ all " . count($rows) . " rows have $headerCount columns matching CSV_HEADERS\n";
|
||||
} else {
|
||||
echo " ✓ no rows to check (empty export) — header count is $headerCount\n";
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// SECTION E: SearchController — coverMap key always present (regression)
|
||||
// =========================================================================
|
||||
|
||||
echo "E1: SearchController::handleSearch() — coverMap key always in return array\n";
|
||||
// Simulate $_GET for the method (it reads from $_GET directly via collectSearchParams)
|
||||
$_GET = ['query' => ''];
|
||||
$rateLimit = new RateLimit(1000, 60);
|
||||
$searchCtrl = new SearchController($db, $rateLimit);
|
||||
$vars = $searchCtrl->handleSearch();
|
||||
plAssert(array_key_exists('coverMap', $vars), 'coverMap key present in handleSearch() return');
|
||||
plAssert(is_array($vars['coverMap']), 'coverMap is an array');
|
||||
$_GET = [];
|
||||
echo "\n";
|
||||
|
||||
echo "✅ All pure logic tests passed!\n";
|
||||
$result = true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo '❌ FAIL: ' . $e->getMessage() . "\n";
|
||||
$result = false;
|
||||
}
|
||||
|
||||
return $result ?? false;
|
||||
234
app/tests/Unit/ShareLinkTest.php
Normal file
234
app/tests/Unit/ShareLinkTest.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ShareLink Unit Test
|
||||
*
|
||||
* Tests pure-logic methods that require no HTTP context:
|
||||
* - generateSlug() — format, uniqueness, entropy
|
||||
* - validateLink() — all branches: not_found, archived, disabled, expired, needs_password, valid
|
||||
* - verifyPassword() — correct / wrong / no-password links
|
||||
*/
|
||||
|
||||
putenv('DB_ENV=test');
|
||||
|
||||
if (!defined('APP_ROOT')) {
|
||||
define('APP_ROOT', dirname(__DIR__, 2));
|
||||
}
|
||||
if (!defined('STORAGE_ROOT')) {
|
||||
define('STORAGE_ROOT', APP_ROOT . '/storage');
|
||||
}
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/ShareLink.php';
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function slAssert(bool $cond, string $label): void
|
||||
{
|
||||
if ($cond) {
|
||||
echo " ✓ $label\n";
|
||||
} else {
|
||||
throw new RuntimeException("FAIL: $label");
|
||||
}
|
||||
}
|
||||
|
||||
function slAssertEq(mixed $expected, mixed $actual, string $label): void
|
||||
{
|
||||
if ($expected === $actual) {
|
||||
echo " ✓ $label\n";
|
||||
} else {
|
||||
$e = var_export($expected, true);
|
||||
$a = var_export($actual, true);
|
||||
throw new RuntimeException("FAIL $label\n expected: $e\n actual: $a");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Setup ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "ShareLink Unit Test\n";
|
||||
echo "===================\n\n";
|
||||
|
||||
$db = Database::getInstance();
|
||||
$model = new ShareLink($db);
|
||||
|
||||
// We need a dummy admin user id — just use 1 (or any int; share_links.created_by is not FK-checked)
|
||||
$adminId = 1;
|
||||
$createdIds = [];
|
||||
|
||||
try {
|
||||
|
||||
// =========================================================================
|
||||
// TEST 1: generateSlug — format YYYYMMDD-XXXXXXXX
|
||||
// =========================================================================
|
||||
echo "Test 1: generateSlug — format\n";
|
||||
$slug = ShareLink::generateSlug();
|
||||
slAssert(
|
||||
(bool) preg_match('/^\d{8}-[A-Z2-7]{8}$/', $slug),
|
||||
"slug matches YYYYMMDD-[BASE32]{8}: $slug"
|
||||
);
|
||||
$year = (int) substr($slug, 0, 4);
|
||||
$month = (int) substr($slug, 4, 2);
|
||||
$day = (int) substr($slug, 6, 2);
|
||||
slAssert($year >= 2020 && $year <= 2100, 'year in plausible range');
|
||||
slAssert($month >= 1 && $month <= 12, 'month in range');
|
||||
slAssert($day >= 1 && $day <= 31, 'day in range');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 2: generateSlug — two calls produce different slugs
|
||||
// =========================================================================
|
||||
echo "Test 2: generateSlug — uniqueness\n";
|
||||
$slugs = [];
|
||||
for ($i = 0; $i < 20; $i++) {
|
||||
$slugs[] = ShareLink::generateSlug();
|
||||
}
|
||||
slAssertEq(count($slugs), count(array_unique($slugs)), '20 consecutive slugs are all unique');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 3: validateLink — not_found on missing slug
|
||||
// =========================================================================
|
||||
echo "Test 3: validateLink — not_found on missing slug\n";
|
||||
$result = $model->validateLink('NONEXISTENT-SLUG');
|
||||
slAssertEq(false, $result['valid'], 'valid=false');
|
||||
slAssertEq('not_found', $result['reason'], 'reason=not_found');
|
||||
|
||||
$result = $model->validateLink(null);
|
||||
slAssertEq(false, $result['valid'], 'null slug: valid=false');
|
||||
slAssertEq('not_found', $result['reason'], 'null slug: reason=not_found');
|
||||
|
||||
$result = $model->validateLink('');
|
||||
slAssertEq(false, $result['valid'], 'empty slug: valid=false');
|
||||
slAssertEq('not_found', $result['reason'], 'empty slug: reason=not_found');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 4: validateLink — valid active link with no password
|
||||
// =========================================================================
|
||||
echo "Test 4: validateLink — valid active link\n";
|
||||
$link = $model->create($adminId, null, null);
|
||||
$createdIds[] = $link['id'];
|
||||
|
||||
$result = $model->validateLink($link['slug']);
|
||||
slAssertEq(true, $result['valid'], 'valid=true');
|
||||
slAssert(isset($result['link']), 'link row returned');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 5: validateLink — disabled link
|
||||
// =========================================================================
|
||||
echo "Test 5: validateLink — disabled link\n";
|
||||
$model->toggleActive($link['id']); // deactivate
|
||||
$result = $model->validateLink($link['slug']);
|
||||
slAssertEq(false, $result['valid'], 'valid=false after disable');
|
||||
slAssertEq('disabled', $result['reason'], 'reason=disabled');
|
||||
$model->toggleActive($link['id']); // restore
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 6: validateLink — archived link
|
||||
// =========================================================================
|
||||
echo "Test 6: validateLink — archived link\n";
|
||||
$archivedLink = $model->create($adminId, null, null);
|
||||
$createdIds[] = $archivedLink['id'];
|
||||
$model->archive($archivedLink['id']);
|
||||
$result = $model->validateLink($archivedLink['slug']);
|
||||
slAssertEq(false, $result['valid'], 'valid=false for archived');
|
||||
slAssertEq('archived', $result['reason'], 'reason=archived');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 7: validateLink — expired link
|
||||
// =========================================================================
|
||||
echo "Test 7: validateLink — expired link\n";
|
||||
$pastDate = date('Y-m-d H:i:s', strtotime('-1 day'));
|
||||
$expiredLink = $model->create($adminId, null, $pastDate);
|
||||
$createdIds[] = $expiredLink['id'];
|
||||
$result = $model->validateLink($expiredLink['slug']);
|
||||
slAssertEq(false, $result['valid'], 'valid=false for expired');
|
||||
slAssertEq('expired', $result['reason'], 'reason=expired');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 8: validateLink — not expired (future date)
|
||||
// =========================================================================
|
||||
echo "Test 8: validateLink — future expiry is still valid\n";
|
||||
$futureDate = date('Y-m-d H:i:s', strtotime('+30 days'));
|
||||
$futureLink = $model->create($adminId, null, $futureDate);
|
||||
$createdIds[] = $futureLink['id'];
|
||||
$result = $model->validateLink($futureLink['slug']);
|
||||
slAssertEq(true, $result['valid'], 'valid=true for future expiry');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 9: validateLink — needs_password when password is set
|
||||
// =========================================================================
|
||||
echo "Test 9: validateLink — needs_password\n";
|
||||
$pwLink = $model->create($adminId, 'secret123', null);
|
||||
$createdIds[] = $pwLink['id'];
|
||||
$result = $model->validateLink($pwLink['slug']);
|
||||
slAssertEq(false, $result['valid'], 'valid=false (needs password)');
|
||||
slAssertEq('needs_password', $result['reason'], 'reason=needs_password');
|
||||
slAssert(isset($result['link']), 'link row returned even when password needed');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 10: verifyPassword — correct password
|
||||
// =========================================================================
|
||||
echo "Test 10: verifyPassword — correct password\n";
|
||||
$pwLinkRow = $model->findBySlug($pwLink['slug']);
|
||||
slAssertEq(true, $model->verifyPassword($pwLinkRow, 'secret123'), 'correct password accepted');
|
||||
slAssertEq(false, $model->verifyPassword($pwLinkRow, 'wrongpass'), 'wrong password rejected');
|
||||
slAssertEq(false, $model->verifyPassword($pwLinkRow, ''), 'empty password rejected');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 11: verifyPassword — link with no password always passes
|
||||
// =========================================================================
|
||||
echo "Test 11: verifyPassword — no password set always returns true\n";
|
||||
$noPwRow = $model->findBySlug($link['slug']);
|
||||
slAssertEq(true, $model->verifyPassword($noPwRow, ''), 'no-pw link: empty string passes');
|
||||
slAssertEq(true, $model->verifyPassword($noPwRow, 'anything'), 'no-pw link: any string passes');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 12: incrementUsage — counter goes up
|
||||
// =========================================================================
|
||||
echo "Test 12: incrementUsage — counter increments\n";
|
||||
$fresh = $model->findById($link['id']);
|
||||
$before = (int)$fresh['usage_count'];
|
||||
$model->incrementUsage($link['id']);
|
||||
$model->incrementUsage($link['id']);
|
||||
$after = (int)($model->findById($link['id'])['usage_count'] ?? 0);
|
||||
slAssertEq($before + 2, $after, 'usage_count incremented by 2');
|
||||
echo "\n";
|
||||
|
||||
// =========================================================================
|
||||
// TEST 13: objet_restriction is stored and returned
|
||||
// =========================================================================
|
||||
echo "Test 13: objet_restriction stored correctly\n";
|
||||
$restrictedLink = $model->create($adminId, null, null, 'tfe');
|
||||
$createdIds[] = $restrictedLink['id'];
|
||||
slAssertEq('tfe', $restrictedLink['objet_restriction'], 'objet_restriction=tfe stored');
|
||||
|
||||
$anyLink = $model->create($adminId, null, null, 'invalid_value');
|
||||
$createdIds[] = $anyLink['id'];
|
||||
slAssertEq(null, $anyLink['objet_restriction'], 'invalid objet_restriction stored as null');
|
||||
echo "\n";
|
||||
|
||||
echo "✅ All ShareLink tests passed!\n";
|
||||
$result = true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo '❌ FAIL: ' . $e->getMessage() . "\n";
|
||||
$result = false;
|
||||
} finally {
|
||||
$pdo = $db->getConnection();
|
||||
foreach ($createdIds as $id) {
|
||||
try {
|
||||
$pdo->prepare('DELETE FROM share_links WHERE id = ?')->execute([$id]);
|
||||
} catch (Exception $e) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
return $result ?? false;
|
||||
Reference in New Issue
Block a user