Search by

yahyaerturan / auth-testing

yahyaerturan

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.

Package info

github.com/yahyaerturan/auth-testing

pkg:composer/yahyaerturan/auth-testing

Statistics

Installs: 20

Dependents: 4

Suggesters: 1

Stars: 0

Open Issues: 0

v1.0.1 2026-09-08 19:57 UTC

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 a require. The doubles themselves have no test-framework dependency at all — only the contract suites in contracts/ extend TestCase.

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 NativeCryptographicRandom is 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.