Files
xamxam/tests/TestDatabase.php
Pontoporeia 99125cc8e3 Add autosave draft system for partage form with HTMX-based session persistence
- New fragment endpoint POST/GET /partage/fragments/draft.php:
  saves all form fields to PHP session, excludes file/csrf/slug fields
  GET returns JSON for JS hydration on page load
  rotates both global CSRF and share CSRF tokens in sync

- form.php accepts optional $formExtraAttrs and $showAutosaveStatus:
  allows injecting HTMX attributes and 'Brouillon enregistré' indicator

- renderShareLinkForm adds hx-post with change/input debounce trigger,
  loads autosave-handler.js, hydrate fields from draft on page load

- Draft cleared on successful form submission in handleShareLinkSubmission

- autosave-handler.js now also updates share_link_token hidden input
  when rotating CSRF token (partage form uses both csrf_token and share_link_token)

- Added .autosave-status CSS to form.css (was admin.css-only)

- Updated fragment routing to accept GET requests (needed for draft hydration)
2026-06-11 11:04:49 +02:00

141 lines
4.4 KiB
PHP

<?php
/**
* TestDatabase — helper for integration tests that need a real SQLite database.
*
* Creates an in-memory SQLite database, loads the full schema + seed data,
* and provides a Database instance connected to it. Teardown discards the DB.
*/
/**
* Thin Database subclass that accepts a pre-built PDO connection.
* Bypasses the normal path-based constructor.
*/
class TestDatabaseInstance extends Database
{
public function __construct(PDO $pdo)
{
// Inject PDO directly via reflection, then flag as ready
$ref = new ReflectionProperty(Database::class, 'pdo');
$ref->setValue($this, $pdo);
$pathRef = new ReflectionProperty(Database::class, 'dbPath');
$pathRef->setValue($this, ':memory:');
}
}
class TestDatabase
{
private static ?PDO $pdo = null;
private static ?Database $db = null;
/**
* Get or create the shared test Database instance.
* Uses an in-memory SQLite DB so tests are fast and isolated.
*/
public static function getInstance(): Database
{
if (self::$db === null) {
self::$pdo = new PDO('sqlite::memory:');
self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
self::$pdo->exec('PRAGMA foreign_keys = ON');
self::$pdo->exec('PRAGMA journal_mode = MEMORY');
// Load schema
$schema = file_get_contents(APP_ROOT . '/storage/schema.sql');
if ($schema === false) {
throw new RuntimeException('Failed to read schema.sql');
}
self::$pdo->exec($schema);
// Create a Database wrapper injecting our PDO
self::$db = new TestDatabaseInstance(self::$pdo);
}
return self::$db;
}
/**
* Get the raw PDO connection (for queries that bypass the Database class).
*/
public static function getPDO(): PDO
{
// Ensure the DB is booted
self::getInstance();
return self::$pdo;
}
/**
* Reset all test data between tests.
* Preserves seed data (orientations, access types, etc.) but removes
* any theses, authors, tags, share links etc. created during a test.
*/
public static function resetData(): void
{
$pdo = self::getPDO();
// Order matters due to FK constraints
$tables = [
'file_access_audit',
'file_access_sessions',
'file_access_tokens',
'file_access_requests',
'thesis_files',
'thesis_tags',
'thesis_formats',
'thesis_languages',
'thesis_supervisors',
'thesis_authors',
'theses',
'share_links',
'tags',
'authors',
'supervisors',
'admin_audit_log',
'audit_log',
];
foreach ($tables as $table) {
$pdo->exec("DELETE FROM $table");
}
// Re-seed tags (some tests rely on tags existing)
try {
$pdo->exec('DELETE FROM tags WHERE deleted_at IS NOT NULL');
} catch (Exception $e) {
// tags table already empty
}
}
/**
* Seed some basic test data: an author, a thesis.
* Returns [authorId, thesisId].
*
* @return array{0: int, 1: int}
*/
public static function seedBasicThesis(string $title = 'Test Thesis', string $authorName = 'Test Author', int $year = 2024): array
{
$pdo = self::getPDO();
// Insert author
$pdo->prepare('INSERT INTO authors (name) VALUES (?)')->execute([$authorName]);
$authorId = (int)$pdo->lastInsertId();
// Insert thesis
$pdo->prepare(
"INSERT INTO theses (title, year, identifier, is_published, objet) VALUES (?, ?, ?, 1, 'tfe')"
)->execute([$title, $year, "$year-001"]);
$thesisId = (int)$pdo->lastInsertId();
// Link author
$pdo->prepare('INSERT INTO thesis_authors (thesis_id, author_id) VALUES (?, ?)')
->execute([$thesisId, $authorId]);
// Insert a cover file
$pdo->prepare(
"INSERT INTO thesis_files (thesis_id, file_type, file_path, file_name, file_size, mime_type) VALUES (?, 'cover', ?, ?, 0, 'image/jpeg')"
)->execute([$thesisId, "documents/$year-001/cover.jpg", 'cover.jpg']);
return [$authorId, $thesisId];
}
}