Repository navigation
Conversation
giuscris
force-pushed
the
tests
branch
8 times, most recently
from
October 11, 2026 17:17
ea9e4ac to
75e93d6
Compare
- Use a consistent `Fixtures` directory name (Http, Pages) and fix the `fixtures` references in FileSystemTest and MimeTypeTest, which only worked on case-insensitive filesystems - Make FormTest independent from the execution order by loading the schemes service (which registers `FieldFactory`) in `setUp()` - Cover previously untested behavior: `Request::isLocalhost()` with IPv6, `Request::validateReferer()` negative cases, forwarded protocols, single-byte and clamped ranges in `FileResponse`, and the destruction of the old session on `Session::regenerate()` - Replace weak or misleading tests (page icon, slug validation, publish date windows, content file resolution, lifecycle events, non-GET request content, `Request::fromGlobals()`) with assertions on actual behavior Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
giuscris
force-pushed
the
tests
branch
2 times, most recently
from
October 11, 2026 22:22
29f2ebc to
e2fff8e
Compare
Public tests are grouped by topic (new tests next to the ones they extend, data providers right after their first user) and helpers follow the php-cs-fixer ordered_class_elements order. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
…te UsersController tests Covers GHSA-34p4, GHSA-xh84, GHSA-vf6x, GHSA-5pxr, GHSA-7j46 (server-side rendering). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
RequestMethod semantics, Container::resolveEagerServices(), ServiceDefinition::isLazy(), Header::parseETags(), conditional request edge cases, PageCollection::search() scores, Date with immutable date times. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Covers Methods, SingletonClass, StaticClass, TranslatedException, Event, EventDispatcher, ListenerProvider and the Cms/Plugins events. Failing tests report: singleton subclasses share their parent instance, TranslatedException cannot wrap errors. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Covers Tokenizer, Token, TokenStream, Parser, the nodes, NodeInterpolator and Interpolator. Failing tests report: numbers merged with following commas or spaces, quotes stripped from string contents, null values traversing into top level variables, private members raising errors instead of InterpolationException, calls on closures evaluated after traversal, magic __call shadowing public properties, ImplicitArrayKeyNode::value() uninitialized. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Shared PSR-16 behavior tests for ArrayCache and FilesCache, plus CacheItem and CacheManager. Failing tests report: namespaces accept a trailing newline, zero or negative TTLs do not delete the item, truncated cache files raise ParseError instead of UnexpectedValueException. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Covers Logger, handlers, formatters, Registry and the deprecated Log. Failing tests report: log forging through line breaks in the text formatter, JsonException on invalid UTF-8 or non-finite numbers while logging, TypeError for array levels, null values in Registry, files created when merely reading a Registry. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Failing tests report: language prefixes matched case-insensitively but not resolved, regional Accept-Language codes never matching the base language, unquoted language codes in the prefix regex, loading a missing translation file throwing, translations keeping a stale fallback after new strings are loaded. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Cover CsrfToken, Authenticator (with in-memory session and real rate
limiter), Users (User, UserCollection, UserFactory, Role, Permissions,
ColorScheme, Password, InitialsImageGenerator).
Failing tests intentionally document bugs in the sources:
- usernames are not validated, so `../x` writes the account file outside
the accounts directory
- getters return stale values after email changes through the setter
- setImage accepts sibling directories sharing the accounts prefix
- emails differing only in case are accepted as distinct
- initials are computed from HTML-escaped names ("Tom & Jerry" -> "TA")
- Password: bcrypt silently truncates at 72 bytes, null byte raises
ValueError
- login of a user without a password hash throws
UnexpectedValueException instead of AuthenticationFailedException
- isLoggedIn() is true for the session of a deleted user
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Cover Model (accessor attributes, deprecated implicit access, set/get,
toArray), Schemes (Scheme, Schemes, SchemeFactory, SchemeOptions),
FieldFactory, DynamicFieldValue and the Layout classes (Layout, Section,
Tab and their collections) plus the shared Fields Translations trait.
Failing tests intentionally document bugs in the sources:
- FieldFactory loops forever on circular field type extension
(loop-a extends loop-b extends loop-a)
- DynamicFieldValue stays flagged as "computing" after a failed
computation, so a retry reports a bogus RecursionException
- Scheme::title() throws a TypeError for per-language titles that miss
the current language or contain unknown translation keys
- Section/Tab labels (Fields Translations trait) throw TypeError for
per-language labels missing the current language and
InvalidArgumentException for unknown {{keys}}
- Model::toArray() crashes on uninitialized typed Getter properties
- Model::toArray() exports the whole App instance
- Model::set() does not apply field validation when fields were not
loaded yet, so stored data depends on access order
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Cover Asset, AssetCollection and Assets (including namespaced keys and
path traversal payloads against the resolution roots), Plugin,
PluginManifest, PluginFactory, Plugins, Template, TemplateFactory and the
view Renderer. Plugins are exercised through generated plugin
directories in the temporary path.
Failing tests intentionally document bugs in the sources:
- Asset throws the generic FileNotFoundException for missing files, the
documented AssetNotFoundException is never raised
- Plugin manifest defaults are stored under plugins.<id> while
isEnabled(), the panel and Plugins use plugins.<camelCaseName>, so the
defaults of hyphenated plugins are never found
- Plugin id validation accepts a trailing newline ("demo\n") because of
the `$` anchor
- Plugin::getEventListeners() registers every public method starting
with "on" (onlineUsers() -> event "lineUsers")
- Plugins::initialize() registers the listeners again when called twice
- PluginManifest raises TypeError instead of UnexpectedValueException for
values of the wrong type
- Renderer::load() lets a variable named `_filename` replace the script
being included
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
…sData Cover AbstractParser/AbstractEncoder, the CommonMark extension (FormworkExtension, LinkBaseProcessor, ImageAltProcessor, ImageRenderer) through Markdown::parse with hostile destinations and attribute values, PhpDomParser (malformed XML, XXE and entity expansion payloads) and Html5Parser, policy invariants for HtmlReference and SvgReference, and FilesData. Failing tests intentionally document bugs in the sources: - Markdown links or images with an empty destination (`[x]()`) raise "Uninitialized string offset 0" in LinkBaseProcessor - Markdown::parse() without a `site` option crashes on relative links and on any image (null dereference in LinkBaseProcessor and ImageAltProcessor) although the option is optional in the schema Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Cover PageTraversal, PageStatus, PageUid and PageUri on generated site trees (nesting, siblings boundaries, caching, date handling, URI building), plus PaginationFactory. Failing tests intentionally document bugs in the sources: - PageStatus throws "Invalid publish date" for unquoted YAML dates and numeric timestamps (YAML parses `publishDate: 2024-01-01` as an int) - PageStatus lets the InvalidArgumentException of an unparsable date escape, so a typo in one frontmatter breaks every listing using the page status - PageUri::uri() normalizes paths against the filesystem root instead of the application root, so `../` segments can climb above the request root Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Cover Backupper (archive naming, name validation, pruning, listing),
ZipErrors, SemVer (parsing, normalization, comparison, ~ and ^ ranges)
and Updater with a stubbed HTTP client. The updater tests never let an
update reach the extraction into ROOT_PATH: archives containing
escaping entries are only fed to the extractor to check they are
refused.
Failing tests intentionally document bugs in the sources:
- Backupper builds the archive name from the hostname without
sanitizing it, so IPv6 hosts ("[2001:db8::1]") make web backups fail
- Backupper::getBackups() and deleteOldBackups() key the backups by
modification time, so backups created in the same second hide each
other and are miscounted when pruning
- Backupper::getBackups() creates the backup directory non-recursively
while backup() creates it recursively
- SemVer::fromString() accepts a trailing newline ("1.2.3\n")
- SemVer treats the "pl"/"patch" prerelease as newer than its release
- Updater::latestRelease() throws before the first check although it is
declared nullable
- Updater: GitHub API error payloads (rate limit) end in an ErrorException
for the undefined `published_at` key instead of a clear RuntimeException
- Updater: the ETag comparison is unreachable for the installed version
(isVersionInstallable() is false for it), so a re-released archive of
the same version is never offered
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Cover Debug (dump of scalars, arrays, objects, enums, closures, resources, dump gating on the Accept header) and CodeDumper (context windows, highlighting, escaping, backtrace frame parameters, sensitive parameters). Failing tests intentionally document bugs in the sources: - Debug keeps every dumped object id forever, so dumping the same object twice (or twice inside one structure) wrongly reports a circular reference - Debug does not escape property names, so dumping decoded user input (json_decode objects) injects markup - Debug throws for closed resources and overflows the stack for self-referencing arrays - CodeDumper does not escape class and function names of backtrace frames - CodeDumper cannot dump frames whose file is not readable (evaluated code), which breaks the error page - CodeDumper line numbers are misaligned when the last line is a power of ten Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Cover AssetsController, FilesController, ErrorsController and PageController (rendering, unpublished pages, page files, canonical redirects, pagination/taxonomy gating, maintenance mode, statistics tracking and the page cache) on top of a lightweight controller container, plus the plugin AssetsController. Failing tests intentionally document missing path confinement in the controllers. The router normalizes the request path before matching, so these are not reachable from HTTP today, but each controller trusts its route parameters and joins them to a base directory with a path join that resolves `..`: - AssetsController::asset(): `id` and `name` - FilesController::file(): `name` - Plugins AssetsController::asset(): `type` (the `file` parameter is already confined with Path::resolve()) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Cover the Cache, Logger, Assets, Translations, Languages, Users, Plugins, Schemes, Site, Templates and Authentication service loaders through containers built over temporary configuration and files. Failing tests intentionally document bugs in the sources: - UsersServiceLoader parses every visible file of the roles and accounts directories, so a stray README.md or notes.txt ends in "Undefined array key" and takes the whole site down - PluginsServiceLoader only ignores RuntimeException, a plugin with an invalid id raises InvalidArgumentException and breaks the site Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Cover the application singleton, service accessors and error handler, a smoke test resolving every service declared in services.php, the Site (page lookup, hidden directories, retrieval, error and index pages, aliases, modification time, metadata) and UriGenerator. Failing tests intentionally document bugs in the sources: - Site::findPage() resolves directories starting with an underscore although retrievePages() excludes them from listings, so `_private` pages are routable - UriGenerator::path() resolves `..` against the filesystem root instead of the application root Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Exercise the PanelServiceLoader route action listener on every route defined by the panel: no route is reachable by remote clients before the first user exists, only registration and assets are reachable from localhost, only login, logout and assets are reachable without logging in, logged in users are never redirected and routes outside the panel root are never touched. Also cover the Http enums. All tests pass: the gatekeeper does not leave any panel route open. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
Cover the shared controller helpers (view, redirect, redirect to
referer with hostile referers, forward, route generation), the template
and plugin collections and the deprecated ReadonlyModelProperty
attribute.
Failing tests intentionally document bugs in the sources:
- Request::baseUri() and absoluteUri() contain a double slash when the
application is installed in the document root
("http://host//current/"), which makes redirectToReferer() miss the
"referer is the current page" check
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t49GomL2ddx2h1TeNkLTd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces a comprehensive PHPUnit test suite for Formwork, along with a small set of targeted changes to the core that make the application testable without altering its public behavior or architecture. The changes include adding application path accessors to
App, allowing the application to be unloaded and reloaded, introducing an HTTP transport abstraction for theClient, and wiring PHPUnit into the CI workflow.Testing infrastructure
composer testandcomposer test:coveragescripts, aphpunit.xml.distconfiguration, and aFormwork\TestsPSR-4 autoload-dev namespace. (composer.json,phpunit.xml.dist)site,cacheor configuration directories, and never load locally installed plugins. (tests/bootstrap.php,tests/TestInstallation.php)#[UsesApplication]attribute and aTestCasebase class that loads the application only for the test classes that need it, plus a helper to capture user deprecations. (tests/UsesApplication.php,tests/TestApplication.php,tests/TestCase.php)formwork/src, includingCms,Http,Services,Fields,Pages,Panelcontrollers,Backup,Updater,Cache,LogandUtils. The suite passes in any execution order. (tests/Unit/**)Application changes
App::__construct()now accepts an optional$rootPath, falling back to theROOT_PATHconstant and throwing aRuntimeExceptionif neither is available. (formwork/src/Cms/App.php)App::rootPath()andApp::systemPath()to expose the installation paths.systemPath()usesSYSTEM_PATHwhen defined, otherwise the directory containing Formwork. This is a first step towards removing the path constants. (formwork/src/Cms/App.php)App::unload(), which resets the services container so that the nextload()starts from a clean state. (formwork/src/Cms/App.php)App::load()toApp::run(), so that loading the application no longer modifies global error handling. (formwork/src/Cms/App.php)Services and paths
Backupper,UpdaterandRateLimiternow receive the root path as astring $rootPathconstructor parameter, provided by the container fromApp::rootPath(), instead of readingROOT_PATHdirectly. (formwork/src/Backup/Backupper.php,formwork/src/Updater/Updater.php,formwork/src/Authentication/RateLimiter.php,formwork/config/services/services.php)ConfigServiceLoaderresolves the%ROOT_PATH%and%SYSTEM_PATH%configuration macros from the application, andConfigServiceLoader::clearCache()now takes the root path as an argument. (formwork/src/Services/Loaders/ConfigServiceLoader.php)CacheController,OptionsController,PluginsController,ToolsControllerandCacheCommandto useApp::rootPath()andApp::systemPath()instead of the path constants.HTTP client
HttpTransportInterfaceand its default implementationStreamHttpTransport.Clientnow delegates the actual request to the transport, so responses, redirects and connection errors can be tested without network access. (formwork/src/Http/Client.php,formwork/src/Http/Transport/HttpTransportInterface.php,formwork/src/Http/Transport/StreamHttpTransport.php)Updaternow accepts an optionalClientin its constructor, defaulting to a client configured for the GitHub API. (formwork/src/Updater/Updater.php)Dynamic field values
SchemesServiceLoaderrather than in a static property ofDynamicFieldValue, so each application load gets its own set of variables. (formwork/src/Fields/Dynamic/DynamicFieldValue.php,formwork/src/Services/Loaders/SchemesServiceLoader.php)CI and packaging
.github/workflows/check-php.yaml)tests/andphpunit.xml.distfrom release archives,git archiveexports and backups. (.github/workflows/release.yaml,.gitattributes,formwork/config/system.yaml)tests/from PHPStan and Rector analysis. (formwork/phpstan.neon,formwork/.rector.php)