yahyaerturan / auth-testing
Testing utilities for yahyaerturan/auth: in-memory adapters, deterministic clock and randomness, fixtures, and the reusable repository contract suites adapter authors run against their own storage.
Requires
- php: ^8.5
- psr/clock: ^1.0
- psr/event-dispatcher: ^1.0
- yahyaerturan/auth: ^1.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.90
- phpstan/phpstan: ^2.2
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^13.3
- yahyaerturan/auth-authorization: ^1.0
Suggests
- phpunit/phpunit: Required only to use the reusable repository contract suites under the YahyaErturan\Auth\Testing\Contract namespace; the test doubles themselves need nothing.
- yahyaerturan/auth-authorization: Required only to use the in-memory authorization adapters, the role/permission fixtures and the authorization contract suites; the authentication doubles need nothing.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-08 19:59:17 UTC
README
Testing utilities for yahyaerturan/auth:
in-memory adapters for every repository port, a frozen clock and a deterministic
random source, fixtures, and the reusable repository contract suites that
define what any adapter — including one you write yourself — has to do.
Install it as a development dependency. It is not required in production, and nothing in it belongs in a running application.
composer require --dev yahyaerturan/auth-testing
Requires PHP 8.5 and yahyaerturan/auth.
Why this package exists
Two reasons, and the second is the one that made it a published package rather
than a tests/ directory.
1. Testing an application that uses this library should not require a
database. Every port yahyaerturan/auth defines has an in-memory
implementation here, so a use case can be composed and driven in a unit test:
$users = new InMemoryUserRepository(); $sessions = new InMemorySessionRepository($users); $clock = new FrozenClock(new DateTimeImmutable('2031-01-01T00:00:00+00:00'));
The doubles are not stubs. They enforce the same invariants the relational adapters do — uniqueness, compare-and-set semantics, first-write-wins revocation — because a double that is more permissive than production turns a passing test into a false negative.
2. Adapter authors need something to be measured against. The contract
suites in contracts/ are the definition of a repository port's behaviour, and
they are executable. yahyaerturan/auth-pdo runs them against SQLite, MariaDB
and PostgreSQL; the in-memory doubles here run them too; and if you write a
Mongo, Redis or DynamoDB adapter, you run the same suite against yours. That is
what makes "implements the port" a claim with evidence behind it (ADR-033).
What is in here
| In-memory repositories | InMemoryUserRepository, InMemoryCredentialRepository, InMemorySessionRepository, InMemoryOneTimeTokenRepository, InMemoryApiTokenRepository, InMemoryAuditRepository, InMemoryTransactionManager |
| In-memory authorization | InMemoryRoleRepository, InMemoryPermissionRepository, InMemoryAuthorizationGrantRepository — need yahyaerturan/auth-authorization |
| Deterministic sources | FrozenClock, DeterministicCryptographicRandom, DeterministicIdGenerator |
| Failure injection | FailingUserRepository, FailingSessionRepository, FailingCredentialRepository, FailingOneTimeTokenRepository, FailingApiTokenRepository, FailingAuditRepository |
| Race reproduction | InterleavingSessionRepository, InterleavingCredentialRepository, InterleavingIdGenerator, InterleavingPasswordHasher |
| Recording doubles | RecordingEventDispatcher, RecordingPasswordHasher, RecordingAuditSinkFailureObserver, ScriptedRateLimiter |
| Fixtures | Fixtures — builders for users, credentials, sessions and role graphs |
| Contracts | contracts/ — the reusable behavioural suites, under YahyaErturan\Auth\Testing\Contract\ |
Minimal usage
Driving a core use case with nothing on disk:
<?php declare(strict_types=1); use YahyaErturan\Auth\Testing\FrozenClock; use YahyaErturan\Auth\Testing\Fixtures; use YahyaErturan\Auth\Testing\InMemoryCredentialRepository; use YahyaErturan\Auth\Testing\InMemorySessionRepository; use YahyaErturan\Auth\Testing\InMemoryUserRepository; $clock = new FrozenClock(Fixtures::instant('2031-01-01 00:00:00.000000')); $users = new InMemoryUserRepository(); $credentials = new InMemoryCredentialRepository($users); $sessions = new InMemorySessionRepository($users); $users->save(Fixtures::activeUser('u1', $clock->now())); $credentials->save(Fixtures::passwordCredential( id: 'cred-1', userId: 'u1', identifier: 'ada@example.com', at: $clock->now(), ));
Every fixture takes its instant rather than reading one, so the objects a test builds and the clock a use case is handed cannot disagree.
FrozenClock never advances on its own, which is the point: a token's expiry is
decided by a value your test sets, so an assertion about expiry is an assertion
about the library rather than about how long the test took to run.
Running the contract suites against your own adapter
A contract suite is an abstract TestCase. Extend it, return your adapter, and
the suite does the rest:
<?php declare(strict_types=1); namespace Acme\Auth\Mongo\Tests; use YahyaErturan\Auth\Contract\SessionRepository; use YahyaErturan\Auth\Contract\UserRepository; use YahyaErturan\Auth\Testing\Contract\SessionRepositoryContract; final class MongoSessionRepositoryTest extends SessionRepositoryContract { private UserRepository $users; protected function createRepository(): SessionRepository { return new MongoSessionRepository($this->connection()); } /** * Sessions reference identities, so the suite needs somewhere to put the * users it creates. It may be your adapter or an in-memory one — the * contract does not care, which is why it asks rather than assumes. */ protected function identities(): UserRepository { return $this->users ??= new MongoUserRepository($this->connection()); } }
The suites available are UserRepositoryContract, CredentialRepositoryContract,
SessionRepositoryContract, OneTimeTokenRepositoryContract,
ApiTokenRepositoryContract, AuditRepositoryContract, and — with
yahyaerturan/auth-authorization installed — RoleRepositoryContract,
PermissionRepositoryContract and AuthorizationGrantRepositoryContract.
See docs/CONTRACT_TESTING.md for what each suite
asserts, what it deliberately does not, and how to satisfy the ones with
concurrency requirements.
What this package does not own
- Production behaviour. Nothing here is an implementation you should deploy. The in-memory repositories lose everything when the process ends, and that is the entire design.
- Persistence. Relational adapters live in
yahyaerturan/auth-pdo. - Authorization semantics. The role and permission model is defined by
yahyaerturan/auth-authorization; this package only stores what that model hands it. - A test framework. PHPUnit is a
suggest, not arequire. The doubles themselves have no test-framework dependency at all — only the contract suites incontracts/extendTestCase.
Security-relevant defaults
DeterministicCryptographicRandom returns bytes you queued. That is exactly what
you want in a test and catastrophic anywhere else, so:
- it is never wired by default — you construct it explicitly;
- it lives in a package installed with
--dev, so a production autoloader does not contain it; - the core's own
NativeCryptographicRandomis the only implementation shipped in a runtime package.
FrozenClock is the same shape of hazard, and gets the same treatment.
If you find yourself needing either of these in production code, the thing that is wrong is the production code.
Relationship to the rest of the ecosystem
| Package | Repository |
|---|---|
yahyaerturan/auth |
https://github.com/yahyaerturan/auth |
yahyaerturan/auth-pdo |
https://github.com/yahyaerturan/auth-pdo |
yahyaerturan/auth-psr15 |
https://github.com/yahyaerturan/auth-psr15 |
yahyaerturan/auth-authorization |
https://github.com/yahyaerturan/auth-authorization |
yahyaerturan/auth-testing |
https://github.com/yahyaerturan/auth-testing |
This package requires yahyaerturan/auth and nothing else at runtime.
yahyaerturan/auth-authorization is a suggest: install it to use the
authorization doubles, contracts and fixtures. Without it, the authentication
doubles work exactly as documented and the authorization classes are simply
never loaded.
Development
git clone https://github.com/yahyaerturan/auth-testing cd auth-testing composer install composer qa # platform, coding standard, PHPStan, PHPUnit
Individual gates: composer test, composer stan, composer cs, composer cs:fix.
Working across several packages at once
The packages are separate repositories. Clone the ones you need as siblings:
your-workspace/
├── auth/
├── auth-authorization/
└── auth-testing/
then point Composer at the checkouts without committing anything:
cp composer.json composer.dev.json
composer config --file composer.dev.json repositories.siblings \
'{"type":"path","url":"../auth*","options":{"symlink":true}}'
composer config --file composer.dev.json minimum-stability dev
COMPOSER=composer.dev.json composer update
composer.dev.json and composer.dev.lock are git-ignored. The committed
manifest resolves everything from Packagist, which is what a consumer gets.
Releasing
See docs/RELEASING.md.
Contributing
Issues and pull requests are welcome — see CONTRIBUTING.md
for the setup, the quality gate, and the conventions this project expects.
Participation is governed by the Code of Conduct.
Found a security vulnerability? Do not open an issue or a pull request.
Security
See SECURITY.md.
License
MIT — see LICENSE.