Files
xamxam/docs/test-plan.md

181 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<?xml version="1.0"?>
<phpunit bootstrap="tests/bootstrap.php" colors="true">
<testsuites>
<testsuite name="XAMXAM">
<directory>tests/phpunit</directory>
</testsuite>
</testsuites>
</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 23 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 | ~23h | Nothing |
| 2 — Integration | DB extended, ShareLink extended, RateLimit extended | ~34h | Test DB |
| 3 — Controller validation | Create/Edit validation paths | ~23h | Test DB + HTTP mock |
| 4 — Cleanup | Migrate old tests, CI, coverage report | ~2h | Phase 13 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.