7.9 KiB
XAMXAM — Test Coverage Plan
Prerequisites (do these first, in order)
- Add Composer — create
composer.jsonif not present, ensurevendor/is gitignored except for committed tools - Install PHPUnit —
composer require --dev phpunit/phpunit ^11 - Create
phpunit.xmlat project root:<?xml version="1.0"?> <phpunit bootstrap="tests/bootstrap.php" colors="true"> <testsuites> <testsuite name="XAMXAM"> <directory>tests/phpunit</directory> </testsuite> </testsuites> </phpunit> - Create
tests/bootstrap.php— autoload classes under test, define any constants the app needs (DB credentials from env, app root path, etc.) - Create
tests/phpunit/directory — all new tests go here. The existingrun-tests.phpand 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@characteremail()renders a working obfuscated mailto linkmailto()builds correct href structureemailText()replaces inline email addresses in a block of textmailtoInText()wraps addresses in mailto linksobfuscateHtml()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 boundariesdiskColor()— thresholds (below warning, warning, critical)logLineClass()— maps log level strings to CSS class namesnginxLineClass()— maps nginx status codes to classesstatusLabel()/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
TestDatabasehelper 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 inbootstrap.php
2.1 DatabaseExtendedTest.php
High-value targets (cover these first):
escapeLikeString()— percent signs, underscores, backslashes in inputbuildSearchConditions()— 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 tablegenerateThesisIdentifier()— format is correct, increments correctly, no collision on concurrent insertsgetCoverPathsForTheses()— returns correct paths for known IDs, returns empty for unknown IDsfindOrCreateAuthor()— idempotent (calling twice with same name returns same ID)deduplicateLanguages()/renameLanguage()/mergeLanguage()— data integrity after mergerenameTag()/mergeTag()— same pattern
2.2 ShareLinkExtendedTest.php
Extend existing ShareLinkTest with:
listActive()— only returns active linkslistArchived()— only returns archivedfindBySlug()— hit and miss casessetPassword()+getDecryptedPassword()round-tripupdate()— fields change, others don't
2.3 RateLimitExtendedTest.php
Extend existing RateLimitTest with:
checkKey()— counts per key, not globallygetRemaining()— decrements correctlygetClientIdentifier()— 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 unknowncollectJuryMembers()handles empty list, single member, duplicateshandleWebsiteUrl()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
CreateControllerfield 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.phpand the old test files - Add
vendor/bin/phpunitto CI pipeline (or aMakefiletarget: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-heavyDispatcher— requires full HTTP contextFilepondHandler— requires$_FILESinjectionSmtpRelay— socket-level, needs mock SMTP serverPeerTubeService— OAuth + HTTP, needs VCR-style mockingParsedown— 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.