mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
- ErrorHandler tests: 77 assertions covering FK extraction, normalization, dedup, edge cases. Fix FK table map for child tables. - Fix FK violation: (int)null → 0 in createThesis for orientation/ap/finality/license FK columns. Add FK value logging to updateThesis. - Add CURRENT_ISSUES.md with summary of FK violation, dev debugging, and tag dedup status for next conversation
358 lines
14 KiB
PHP
358 lines
14 KiB
PHP
<?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
|
|
* - 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, cc2r (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'],
|
|
'has_annexes' => '',
|
|
'languages' => [(string)$languages[0]['id']],
|
|
'language_autre' => '',
|
|
'formats' => [(string)$formatTypes[0]['id']],
|
|
'tag' => 'art, test, recherche',
|
|
'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 — language_autre creates and links new language
|
|
// =========================================================================
|
|
echo "Test 2: 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 3: 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 4: Edit — language checkboxes round-trip
|
|
// =========================================================================
|
|
echo "Test 3: 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 5: Edit — language_autre adds new language
|
|
// =========================================================================
|
|
echo "Test 4: 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 6: Create — backoffice fields persisted
|
|
// =========================================================================
|
|
echo "Test 5: Create — backoffice fields (remarks, jury_points, exemplaires, cc2r)\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['cc2r'], 'cc2r saved');
|
|
echo "\n";
|
|
|
|
// =========================================================================
|
|
// TEST 7: Edit — backoffice fields updated
|
|
// =========================================================================
|
|
echo "Test 6: 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['cc2r'], 'cc2r cleared');
|
|
echo "\n";
|
|
|
|
// =========================================================================
|
|
// TEST 8: getOrCreateLanguage — idempotent
|
|
// =========================================================================
|
|
echo "Test 7: 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 9: Edit — context_note saved
|
|
// =========================================================================
|
|
echo "Test 8: 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;
|