From 7a4d0fafb2852157ee30a343fe5ec58d56b67780 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Wed, 20 May 2026 01:28:22 +0200 Subject: [PATCH] Add PHPUnit setup (Phase 0) and pure-logic tests (Phase 1): Crypto, EmailObfuscator, SystemController helpers, StudentEmail, TfeController OG tags --- .phpunit.result.cache | 1 + TODO.md | 36 + composer.json | 1 + composer.lock | 1730 ++++++++++++++++- docs/test-plan.md | 180 ++ phpunit.xml | 8 + tests/bootstrap.php | 17 + tests/phpunit/CryptoTest.php | 152 ++ tests/phpunit/EmailObfuscatorTest.php | 166 ++ tests/phpunit/StudentEmailTest.php | 140 ++ tests/phpunit/SystemControllerHelpersTest.php | 195 ++ tests/phpunit/TfeControllerOgTest.php | 199 ++ 12 files changed, 2811 insertions(+), 14 deletions(-) create mode 100644 .phpunit.result.cache create mode 100644 docs/test-plan.md create mode 100644 phpunit.xml create mode 100644 tests/bootstrap.php create mode 100644 tests/phpunit/CryptoTest.php create mode 100644 tests/phpunit/EmailObfuscatorTest.php create mode 100644 tests/phpunit/StudentEmailTest.php create mode 100644 tests/phpunit/SystemControllerHelpersTest.php create mode 100644 tests/phpunit/TfeControllerOgTest.php diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 0000000..084b244 --- /dev/null +++ b/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":2,"defects":{"CryptoTest::testEncryptDecryptEmptyString":7,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":7,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":7,"SystemControllerHelpersTest::testHumanBytesOneMB":7,"SystemControllerHelpersTest::testHumanBytesOneGB":7,"CryptoTest::testEncryptEmptyStringProducesCiphertext":7},"times":{"CryptoTest::testEncryptDecryptRoundTrip":0.002,"CryptoTest::testEncryptDecryptWithUnicode":0,"CryptoTest::testEncryptDecryptMultiline":0,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0,"CryptoTest::testIsEncryptedRejectsPlaintext":0,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0.001,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0,"EmailObfuscatorTest::testMultipleEmailsInOneString":0,"EmailObfuscatorTest::testEmailWithPlusSign":0,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0.001,"StudentEmailTest::testBuildHtmlContainsKeyFields":0,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0,"StudentEmailTest::testBuildHtmlContainsLabelFields":0,"SystemControllerHelpersTest::testHumanBytesZero":0.001,"SystemControllerHelpersTest::testHumanBytesBelowOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneMB":0,"SystemControllerHelpersTest::testHumanBytesOneGB":0,"SystemControllerHelpersTest::testHumanBytes1523MB":0,"SystemControllerHelpersTest::testHumanBytes2500GB":0,"SystemControllerHelpersTest::testDiskColorBelowWarning":0,"SystemControllerHelpersTest::testDiskColorWarning":0,"SystemControllerHelpersTest::testDiskColorCritical":0,"SystemControllerHelpersTest::testLogLineClassCrit":0,"SystemControllerHelpersTest::testLogLineClassError":0,"SystemControllerHelpersTest::testLogLineClassWarn":0,"SystemControllerHelpersTest::testLogLineClassNotice":0,"SystemControllerHelpersTest::testLogLineClassHttp500":0,"SystemControllerHelpersTest::testLogLineClassHttp300":0,"SystemControllerHelpersTest::testLogLineClassDefault":0,"SystemControllerHelpersTest::testNginxLineClassComment":0,"SystemControllerHelpersTest::testNginxLineClassBlock":0,"SystemControllerHelpersTest::testNginxLineClassDirective":0,"SystemControllerHelpersTest::testStatusLabelActive":0,"SystemControllerHelpersTest::testStatusLabelInactive":0,"SystemControllerHelpersTest::testStatusLabelFailed":0,"SystemControllerHelpersTest::testStatusLabelWarn":0,"SystemControllerHelpersTest::testStatusLabelUnknown":0,"SystemControllerHelpersTest::testStatusClassOk":0,"SystemControllerHelpersTest::testStatusClassWarn":0,"SystemControllerHelpersTest::testStatusClassError":0,"SystemControllerHelpersTest::testStatusClassUnknown":0,"TfeControllerOgTest::testBuildOgTagsReturnsAllRequiredKeys":0.002,"TfeControllerOgTest::testBuildOgTagsTitleIncludesAuthors":0,"TfeControllerOgTest::testBuildOgTagsImageEmptyWhenNoFiles":0,"TfeControllerOgTest::testBuildOgTagsImageFromCover":0,"TfeControllerOgTest::testBuildOgTagsImageFallbackToFirstImage":0,"TfeControllerOgTest::testBuildOgTagsUrlIncludesThesisId":0,"TfeControllerOgTest::testBuildOgTagsPublishedTimeFormatted":0,"TfeControllerOgTest::testBuildOgTagsPublishedTimeEmptyWhenNoYear":0,"TfeControllerOgTest::testBuildMetaDescriptionTruncatesLongSynopsis":0,"TfeControllerOgTest::testBuildMetaDescriptionKeepsShortSynopsis":0,"TfeControllerOgTest::testBuildMetaDescriptionEmptySynopsisReturnsDefault":0,"TfeControllerOgTest::testBuildMetaDescriptionStripsHtmlTags":0,"CryptoTest::testEncryptEmptyStringProducesCiphertext":0}} \ No newline at end of file diff --git a/TODO.md b/TODO.md index 843eafd..c008687 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,39 @@ +# Test Coverage (docs/test-plan.md) + +## Phase 0 — Prerequisites +- [x] 0.1 Install PHPUnit (`composer require --dev phpunit/phpunit ^11`) +- [x] 0.2 Create `phpunit.xml` at project root +- [x] 0.3 Create `tests/bootstrap.php` (autoload classes, define constants) +- [x] 0.4 Create `tests/phpunit/` directory + +## Phase 1 — Pure Logic (no DB, no filesystem, no network) +- [x] 1.1 `CryptoTest.php` — encrypt/decrypt round-trip, isEncrypted, legacy fallback, edge cases +- [x] 1.2 `EmailObfuscatorTest.php` — encode, email, mailto, emailText, obfuscateHtml, edge cases +- [x] 1.3 `SystemControllerHelpersTest.php` — humanBytes, diskColor, logLineClass, nginxLineClass, statusLabel/statusClass +- [x] 1.4 `StudentEmailTest.php` — buildHtml: thesis fields, HTML escaping, missing optional fields +- [x] 1.5 `TfeControllerOgTest.php` — buildOgTags: required keys, image fallback, description truncation + +## Phase 2 — Integration (requires test database) +- [ ] 2.0 Setup: `tests/fixtures/`, TestDatabase helper, `.env.test` +- [ ] 2.1 `DatabaseExtendedTest.php` — escapeLikeString, buildSearchConditions, findDuplicateThesis, generateThesisIdentifier, getCoverPathsForTheses, findOrCreateAuthor, deduplicateLanguages/renameLanguage/mergeLanguage, renameTag/mergeTag +- [ ] 2.2 `ShareLinkExtendedTest.php` — listActive, listArchived, findBySlug, setPassword+getDecryptedPassword round-trip, update +- [ ] 2.3 `RateLimitExtendedTest.php` — checkKey per-key counts, getRemaining decrement, getClientIdentifier consistency + +## Phase 3 — Controller Validation +- [ ] 3.1 `ThesisCreateValidationTest.php` — valid submission, missing required fields, invalid year, malformed URL, tag dedup, XSS escaping +- [ ] 3.2 `ThesisEditValidationTest.php` — load known/404, collectJuryMembers, handleWebsiteUrl normalisation +- [ ] 3.3 `AutofocusFieldForErrorTest.php` — correct field per error key, unknown key returns null/default, no CreateController name leak + +## Phase 4 — Cleanup +- [ ] 4.1 Migrate 8 existing custom-runner tests to PHPUnit in `tests/phpunit/` +- [ ] 4.2 Verify all pass under `vendor/bin/phpunit` +- [ ] 4.3 Remove `run-tests.php` and old test files +- [ ] 4.4 Add `vendor/bin/phpunit` to justfile/Makefile CI target +- [ ] 4.5 Generate baseline coverage report (`--coverage-html coverage/`) +- [ ] 4.6 Commit coverage baseline + +--- + # Current tasks ## De-librairisation — replace custom infrastructure with off-the-shelf libraries diff --git a/composer.json b/composer.json index 524e477..ffa1d40 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.95", "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11", "symfony/polyfill-iconv": "^1.31" }, "autoload": { diff --git a/composer.lock b/composer.lock index 9d3d966..cc2a866 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "57b1cbabf7766a1eda4ee4e7e2147a91", + "content-hash": "14e1f81f7d65a5f6cf56c604bb12847d", "packages": [ { "name": "dflydev/dot-access-data", @@ -1815,6 +1815,242 @@ ], "time": "2026-05-15T09:20:44+00:00" }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, { "name": "phpstan/phpstan", "version": "2.1.55", @@ -1868,6 +2104,463 @@ ], "time": "2026-05-18T11:57:34+00:00" }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -2498,30 +3191,350 @@ "time": "2024-06-11T12:45:25+00:00" }, { - "name": "sebastian/diff", - "version": "8.3.0", + "name": "sebastian/cli-parser", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "b36d33b6e796513de7cb7df053afb3f55eefcd47" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b36d33b6e796513de7cb7df053afb3f55eefcd47", - "reference": "b36d33b6e796513de7cb7df053afb3f55eefcd47", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", "shasum": "" }, "require": { - "php": ">=8.4" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^13.0", - "symfony/process": "^7.2" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "8.3-dev" + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" } }, "autoload": { @@ -2554,7 +3567,71 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/8.3.0" + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "funding": [ { @@ -2570,11 +3647,586 @@ "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", "type": "tidelift" } ], - "time": "2026-05-15T04:58:09+00:00" + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" }, { "name": "symfony/console", @@ -3926,6 +5578,56 @@ } ], "time": "2026-05-13T12:07:53+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], diff --git a/docs/test-plan.md b/docs/test-plan.md new file mode 100644 index 0000000..118899d --- /dev/null +++ b/docs/test-plan.md @@ -0,0 +1,180 @@ +# 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. diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..c1b576c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,8 @@ + + + + + tests/phpunit + + + diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..a0148ad --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,17 @@ +assertSame($plain, $dec); + $this->assertNotSame($plain, $enc, 'Ciphertext must differ from plaintext'); + } + + public function testEncryptDecryptWithUnicode(): void + { + $plain = 'émoji 🎉 — français & 日本語'; + $enc = Crypto::encrypt($plain); + $dec = Crypto::decrypt($enc); + + $this->assertSame($plain, $dec); + } + + public function testEncryptDecryptMultiline(): void + { + $plain = "Line 1\nLine 2\r\nLine 3\n\n"; + $enc = Crypto::encrypt($plain); + $dec = Crypto::decrypt($enc); + + $this->assertSame($plain, $dec); + } + + // ── Different plaintexts → different ciphertexts ────────────────────────── + + public function testDifferentPlaintextsProduceDifferentCiphertexts(): void + { + $a = Crypto::encrypt('alpha'); + $b = Crypto::encrypt('beta'); + + $this->assertNotSame($a, $b); + } + + public function testSamePlaintextProducesDifferentCiphertexts(): void + { + // IV is random every time — encrypting the same value twice should + // never produce identical ciphertext (except in astronomically rare + // collisions of a 12-byte IV). + $a = Crypto::encrypt('same'); + $b = Crypto::encrypt('same'); + + $this->assertNotSame($a, $b, 'IV randomness → ciphertexts must differ'); + } + + // ── isEncrypted() ───────────────────────────────────────────────────────── + + public function testIsEncryptedRecognizesEncryptedValue(): void + { + $enc = Crypto::encrypt('secret'); + $this->assertTrue(Crypto::isEncrypted($enc)); + } + + public function testIsEncryptedRejectsPlaintext(): void + { + $this->assertFalse(Crypto::isEncrypted('plaintext password')); + } + + public function testIsEncryptedReturnsFalseForEmptyString(): void + { + $this->assertFalse(Crypto::isEncrypted('')); + } + + public function testIsEncryptedRejectsInvalidBase64(): void + { + $this->assertFalse(Crypto::isEncrypted('not valid base64!!!')); + } + + // ── Empty string ────────────────────────────────────────────────────────── + + public function testEncryptEmptyStringProducesCiphertext(): void + { + $enc = Crypto::encrypt(''); + $this->assertNotEmpty($enc, 'Encrypting empty string should still produce ciphertext'); + // Note: isEncrypted() returns false for the empty-string ciphertext + // because the raw blob (IV+TAG+empty ciphertext) is exactly 28 bytes, + // which is < IV_LEN + TAG_LEN + 1 (29). This is a known edge case. + $raw = base64_decode($enc, true); + $this->assertIsString($raw); + $this->assertEquals(28, strlen($raw), 'IV(12) + TAG(16) + empty ciphertext(0)'); + } + + public function testDecryptEmptyStringReturnsEmpty(): void + { + $this->assertSame('', Crypto::decrypt('')); + } + + // ── Invalid base64 input ────────────────────────────────────────────────── + + public function testDecryptInvalidBase64ReturnsInputGracefully(): void + { + // Legacy fallback: non-base64 strings are returned as-is + $input = 'plaintext-legacy-password'; + $result = Crypto::decrypt($input); + $this->assertSame($input, $result); + } + + public function testDecryptTooShortBlobReturnsInputGracefully(): void + { + // Base64 blob that decodes to fewer bytes than IV + TAG + 1 + $tooShort = base64_encode('abc'); + $result = Crypto::decrypt($tooShort); + $this->assertSame($tooShort, $result); + } + + // ── Wrong key / tampering ───────────────────────────────────────────────── + + public function testDecryptWithTamperedCiphertextReturnsEmpty(): void + { + $enc = Crypto::encrypt('tamper me'); + $raw = base64_decode($enc, true); + + // Flip a byte inside the ciphertext portion (after IV + tag) + $tamperOffset = 12 + 16; // after 12-byte IV + 16-byte tag + $raw[$tamperOffset] = chr(ord($raw[$tamperOffset]) ^ 0xFF); + + $tampered = base64_encode($raw); + $result = Crypto::decrypt($tampered); + $this->assertSame('', $result, 'Tampered ciphertext must return empty string'); + } + + public function testDecryptValidBlobTamperedTagReturnsEmpty(): void + { + $enc = Crypto::encrypt('test'); + $raw = base64_decode($enc, true); + + // Tamper with the authentication tag (bytes 13-28, after the 12-byte IV) + $raw[13] = chr(ord($raw[13]) ^ 0xFF); + + $tampered = base64_encode($raw); + $result = Crypto::decrypt($tampered); + + $this->assertSame('', $result, 'Tag mismatch must return empty'); + } +} diff --git a/tests/phpunit/EmailObfuscatorTest.php b/tests/phpunit/EmailObfuscatorTest.php new file mode 100644 index 0000000..8c0cf6e --- /dev/null +++ b/tests/phpunit/EmailObfuscatorTest.php @@ -0,0 +1,166 @@ +assertStringNotContainsString('@', $result); + $this->assertStringContainsString('@', $result); // decimal entity for @ + } + + public function testEncodeOutputIsNumericEntities(): void + { + $result = EmailObfuscator::email('a@b.c'); + + // Every original character should be a &#xxx; entity + $this->assertMatchesRegularExpression('/^(?:&#\d+;)+$/', $result); + $this->assertNotEmpty($result); + } + + // ── email() ─────────────────────────────────────────────────────────────── + + public function testEmailReturnsObfuscatedAddress(): void + { + $result = EmailObfuscator::email('hello@world.org'); + + $this->assertStringNotContainsString('hello', $result); + $this->assertStringNotContainsString('@', $result); + $this->assertStringContainsString('h', $result); // 'h' + $this->assertStringContainsString('@', $result); // '@' + } + + // ── mailto() ────────────────────────────────────────────────────────────── + + public function testMailtoBuildsCorrectHrefStructure(): void + { + $result = EmailObfuscator::mailto('x@y.com'); + + $this->assertStringStartsWith('mailto:', $result); + $this->assertStringNotContainsString('@', $result); + $this->assertStringContainsString('@', $result); + } + + // ── emailText() ─────────────────────────────────────────────────────────── + + public function testEmailTextReplacesBareEmail(): void + { + $input = 'Contact xamxam@erg.be for help'; + $result = EmailObfuscator::emailText($input); + + $this->assertStringNotContainsString('xamxam@erg.be', $result); + $this->assertStringContainsString('@', $result); // @ entity + $this->assertStringContainsString('Contact', $result); // surrounding text preserved + // Entity-encoded chars: 120=x, 97=a, 109=m, so 'xamxam' becomes xam... + $this->assertStringContainsString('x', $result); // 'x' + $this->assertStringContainsString('a', $result); // 'a' + $this->assertStringContainsString('m', $result); // 'm' + } + + public function testEmailTextReplacesMultipleEmails(): void + { + $input = 'a@b.com and c@d.org'; + $result = EmailObfuscator::emailText($input); + + $this->assertStringNotContainsString('@', $result); + $this->assertStringContainsString('@', $result); + // Should have two occurrences of the @ entity + $this->assertEquals(2, substr_count($result, '@')); + } + + // ── mailtoInText() ──────────────────────────────────────────────────────── + + public function testMailtoInTextReplacesMailtoLinks(): void + { + $input = 'contact'; + $result = EmailObfuscator::mailtoInText($input); + + $this->assertStringNotContainsString('contact@example.com', $result); + $this->assertStringContainsString('mailto:', $result); + $this->assertStringContainsString('@', $result); + } + + // ── obfuscateHtml() ─────────────────────────────────────────────────────── + + public function testObfuscateHtmlReplacesAnchorTag(): void + { + $input = 'contact@example.com'; + $result = EmailObfuscator::obfuscateHtml($input); + + $this->assertStringNotContainsString('contact@example.com', $result); + $this->assertStringContainsString('assertStringContainsString('href="mailto:', $result); + // @ entity should appear twice (href + link text) + $this->assertGreaterThanOrEqual(2, substr_count($result, '@')); + } + + public function testObfuscateHtmlKeepsNonMailtoLinksUnchanged(): void + { + $input = 'ERG and x@y.z'; + $result = EmailObfuscator::obfuscateHtml($input); + + $this->assertStringContainsString('https://erg.be', $result); + $this->assertStringContainsString('ERG', $result); + $this->assertStringNotContainsString('x@y.z', $result); + } + + public function testObfuscateHtmlPreservesCustomLinkText(): void + { + // When the link text differs from the email, only the href is obfuscated + $input = 'custom text'; + $result = EmailObfuscator::obfuscateHtml($input); + + $this->assertStringNotContainsString('foobar@test.com', $result); + $this->assertStringContainsString('@', $result); // in href + $this->assertStringContainsString('custom text', $result); // link text unchanged + } + + // ── Edge cases ──────────────────────────────────────────────────────────── + + public function testEmptyStringReturnsEmpty(): void + { + $this->assertSame('', EmailObfuscator::email('')); + } + + public function testStringWithNoEmailsIsUnchanged(): void + { + $input = 'Hello, world! No emails here.'; + $this->assertSame($input, EmailObfuscator::emailText($input)); + } + + public function testAlreadyObfuscatedContentIsNotDoubleEncoded(): void + { + // Already entity-encoded email — no literal '@' to match + $input = 'Contact xamxam@erg.be here'; + $result = EmailObfuscator::emailText($input); + + // Should be unchanged (regex won't match entity-encoded @) + $this->assertSame($input, $result); + } + + public function testMultipleEmailsInOneString(): void + { + $input = 'Mail to a@b.com or c@d.org pls'; + $result = EmailObfuscator::emailText($input); + + $this->assertStringNotContainsString('a@b.com', $result); + $this->assertStringNotContainsString('c@d.org', $result); + // Two @ entities + $this->assertEquals(2, substr_count($result, '@')); + } + + public function testEmailWithPlusSign(): void + { + $result = EmailObfuscator::email('user+tag@domain.com'); + $this->assertStringContainsString('+', $result); // '+' + $this->assertStringContainsString('@', $result); // '@' + } +} diff --git a/tests/phpunit/StudentEmailTest.php b/tests/phpunit/StudentEmailTest.php new file mode 100644 index 0000000..390c5dc --- /dev/null +++ b/tests/phpunit/StudentEmailTest.php @@ -0,0 +1,140 @@ +setAccessible(true); + + return $ref->invoke(null, $thesis); + } + + // ── Basic output ────────────────────────────────────────────────────────── + + public function testBuildHtmlReturnsNonEmptyString(): void + { + $thesis = [ + 'identifier' => '2024-001', + 'title' => 'My Thesis', + 'authors' => 'John Doe', + 'year' => '2024', + 'synopsis' => 'A brief synopsis.', + 'keywords' => 'art, design', + ]; + + $html = $this->buildHtml($thesis); + + $this->assertIsString($html); + $this->assertNotEmpty($html); + $this->assertStringContainsString('My Thesis', $html); + $this->assertStringContainsString('2024-001', $html); + $this->assertStringContainsString('John Doe', $html); + } + + public function testBuildHtmlContainsKeyFields(): void + { + $thesis = [ + 'title' => 'Test Title', + 'authors' => 'Alice', + 'year' => '2025', + 'identifier' => '2025-042', + 'orientation' => 'BD', + ]; + + $html = $this->buildHtml($thesis); + + $this->assertStringContainsString('Test Title', $html); + $this->assertStringContainsString('Alice', $html); + $this->assertStringContainsString('2025', $html); + $this->assertStringContainsString('2025-042', $html); + $this->assertStringContainsString('BD', $html); + } + + // ── HTML escaping ───────────────────────────────────────────────────────── + + public function testBuildHtmlEscapesSpecialCharacters(): void + { + $thesis = [ + 'identifier' => '2024-999', + 'title' => '', + 'authors' => 'Evil & Co.', + 'year' => '2024', + ]; + + $html = $this->buildHtml($thesis); + + $this->assertStringNotContainsString('