Files
xamxam/docs/test-plan.md

7.9 KiB
Raw Permalink Blame History

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 PHPUnitcomposer require --dev phpunit/phpunit ^11
  3. Create phpunit.xml at project root:
    <?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.