# XAMXAM — Test Coverage Plan ## Prerequisites (do these first, in order) 1. **Add Composer** — create `composer.json` if not present, ensure `vendor/` is gitignored except for committed tools 2. **Install PHPUnit** — `composer require --dev phpunit/phpunit ^11` 3. **Create `phpunit.xml`** at project root: ```xml tests/phpunit ``` 4. **Create `tests/bootstrap.php`** — autoload classes under test, define any constants the app needs (DB credentials from env, app root path, etc.) 5. **Create `tests/phpunit/`** directory — all new tests go here. The existing `run-tests.php` and its 8 test files are left untouched for now. --- ## Phase 1 — Pure Logic (no DB, no filesystem, no network) **Goal:** Cover all stateless classes and helper methods. These have zero external dependencies and should take 2–3 hours total. Every test here runs in milliseconds and needs no fixtures. ### 1.1 `CryptoTest.php` — HIGH PRIORITY Cover: - Encrypt → decrypt round-trip returns original plaintext - Different plaintexts produce different ciphertexts (IV randomness) - `isEncrypted()` correctly identifies encrypted vs plain strings - Legacy fallback path decrypts values encrypted with the old scheme - Empty string handling (encrypt/decrypt empty string without error) - Invalid base64 input to decrypt throws or returns false gracefully - Wrong key produces failure, not silent garbage ### 1.2 `EmailObfuscatorTest.php` — MEDIUM PRIORITY Cover: - `encode()` produces output with no literal `@` character - `email()` renders a working obfuscated mailto link - `mailto()` builds correct href structure - `emailText()` replaces inline email addresses in a block of text - `mailtoInText()` wraps addresses in mailto links - `obfuscateHtml()` reconstructs anchor tags correctly - Edge cases: empty string, string with no emails, already-obfuscated content, multiple emails in one string ### 1.3 `SystemControllerHelpersTest.php` — HIGH VALUE, LOW EFFORT (~15 min) Cover the static pure functions (no HTTP context needed): - `humanBytes()` — 0, 1023, 1024, 1MB, 1GB boundaries - `diskColor()` — thresholds (below warning, warning, critical) - `logLineClass()` — maps log level strings to CSS class names - `nginxLineClass()` — maps nginx status codes to classes - `statusLabel()` / `statusClass()` — all defined statuses ### 1.4 `StudentEmailTest.php` — LOW EFFORT Cover `buildHtml()`: - Returns a non-empty string containing key thesis fields (title, author) - HTML-escapes special characters in thesis data - Handles a thesis row with missing/null optional fields without error ### 1.5 `TfeControllerOgTest.php` Cover `buildOgTags()`: - Returns array with all required OG keys (`og:title`, `og:description`, `og:image`, etc.) - Falls back correctly when image is absent - Long descriptions are truncated to meta limits --- ## Phase 2 — Integration (requires test database) **Goal:** Cover DB-layer methods not yet touched. Requires a dedicated test database seeded with fixtures, wiped between test runs. ### Setup required before Phase 2 - Create a `tests/fixtures/` directory with SQL seed files (one per test class or shared base) - Add a `TestDatabase` helper class that boots a PDO connection to the test DB, runs migrations, seeds, and truncates on teardown - Store test DB credentials in `.env.test` (never committed), read in `bootstrap.php` ### 2.1 `DatabaseExtendedTest.php` High-value targets (cover these first): - `escapeLikeString()` — percent signs, underscores, backslashes in input - `buildSearchConditions()` — various combinations of filters produce correct WHERE clauses (check SQL structure, not just that it runs) - `findDuplicateThesis()` — detects exact duplicate, misses near-duplicate, handles empty table - `generateThesisIdentifier()` — format is correct, increments correctly, no collision on concurrent inserts - `getCoverPathsForTheses()` — returns correct paths for known IDs, returns empty for unknown IDs - `findOrCreateAuthor()` — idempotent (calling twice with same name returns same ID) - `deduplicateLanguages()` / `renameLanguage()` / `mergeLanguage()` — data integrity after merge - `renameTag()` / `mergeTag()` — same pattern ### 2.2 `ShareLinkExtendedTest.php` Extend existing ShareLinkTest with: - `listActive()` — only returns active links - `listArchived()` — only returns archived - `findBySlug()` — hit and miss cases - `setPassword()` + `getDecryptedPassword()` round-trip - `update()` — fields change, others don't ### 2.3 `RateLimitExtendedTest.php` Extend existing RateLimitTest with: - `checkKey()` — counts per key, not globally - `getRemaining()` — decrements correctly - `getClientIdentifier()` — produces consistent output for same input, ignores X-Forwarded-For --- ## Phase 3 — Validation & Controller Logic **Goal:** Cover the validation and sanitisation logic that lives inside controllers, tested by driving through the public interface rather than calling private methods directly. ### 3.1 `ThesisCreateValidationTest.php` Drive `ThesisCreateController` through its `handle()` method with a mock HTTP POST: - Valid submission creates a record - Missing required fields (title, author, year) returns error, nothing written to DB - Invalid year format (letters, future year beyond threshold, year 0) rejected - Malformed URL in website field rejected - Tag list with duplicates deduplicated before save - XSS payload in title stored escaped, never executed ### 3.2 `ThesisEditValidationTest.php` Same pattern for `ThesisEditController`: - `load()` returns correct data for known ID, 404 for unknown - `collectJuryMembers()` handles empty list, single member, duplicates - `handleWebsiteUrl()` normalises http/https, rejects non-URLs ### 3.3 `autofocusFieldForErrorTest.php` `ThesisEditController` has its own copy of this helper with different field names from `CreateController`. Verify: - Returns correct field name for each known error key - Returns null/default for unknown error key - Does not leak `CreateController` field names --- ## Phase 4 — Cleanup (no new tests, housekeeping) **Goal:** Consolidate the two test systems. - Migrate the 8 existing custom-runner tests to PHPUnit equivalents in `tests/phpunit/` - Validate they all pass under `vendor/bin/phpunit` - Remove `run-tests.php` and the old test files - Add `vendor/bin/phpunit` to CI pipeline (or a `Makefile` target: `make test`) - Generate a baseline coverage report: `vendor/bin/phpunit --coverage-html coverage/` - Commit the `coverage/` baseline so regressions are visible in future reports --- ## What is explicitly out of scope These classes are noted as hard to test and are **not part of this plan**. Do not attempt to test them without first adding dependency injection or a proper HTTP testing layer: - `App` — session/header-heavy - `Dispatcher` — requires full HTTP context - `FilepondHandler` — requires `$_FILES` injection - `SmtpRelay` — socket-level, needs mock SMTP server - `PeerTubeService` — OAuth + HTTP, needs VCR-style mocking - `Parsedown` — third-party, tested upstream --- ## Summary | Phase | Scope | Effort | Requires | |---|---|---|---| | 0 — Prerequisites | PHPUnit setup | ~1h | Composer | | 1 — Pure logic | Crypto, Obfuscator, SystemController helpers, StudentEmail, OG tags | ~2–3h | Nothing | | 2 — Integration | DB extended, ShareLink extended, RateLimit extended | ~3–4h | Test DB | | 3 — Controller validation | Create/Edit validation paths | ~2–3h | Test DB + HTTP mock | | 4 — Cleanup | Migrate old tests, CI, coverage report | ~2h | Phase 1–3 done | **Start with Phase 0 + Phase 1.** They are fully unblocked and deliver the highest security-relevant coverage (Crypto, EmailObfuscator) with the least setup friction.