๐Ÿงช Testing Guide๏ƒ

Testing is crucial for maintaining the reliability and security of your authentication system. This guide covers how to test applications using Daycry Auth and how to contribute to the library itself.

๐Ÿ“‹ Table of Contents๏ƒ

๐Ÿƒโ€โ™‚๏ธ Quick Start๏ƒ

Running Tests๏ƒ

# Run all tests
composer test

# Run specific test class
composer test -- --filter="AuthenticationTest"

# Run tests with coverage
composer test:coverage

# Run tests with verbose output
./vendor/bin/phpunit --verbose

Test Environment Setup๏ƒ

The library ships two ready-to-use base classes under tests/_support/:

Class

Use when

Tests\Support\TestCase

No database needed (unit tests, filter tests)

Tests\Support\DatabaseTestCase

Tests that read/write the SQLite in-memory database

Both base classes automatically:

  • Reset all CI4 services between tests

  • Inject the array settings handler (so setting() calls work)

  • Inject a fixed AES-256 encryption key (so service('encrypter') works โ€” needed for TOTP)

  • Seed with CoreSeeder (groups, permissions, one default user)

<?php

namespace Tests\Authentication;

use Tests\Support\DatabaseTestCase;
use Daycry\Auth\Entities\User;
use Daycry\Auth\Models\UserModel;

class MyAuthTest extends DatabaseTestCase
{
    protected User $user;

    protected function setUp(): void
    {
        parent::setUp();

        // Create a test user via CI4 Fabricator
        $this->user = fake(UserModel::class);
    }
}

๐Ÿงช Test Categories๏ƒ

Unit Tests๏ƒ

  • Authentication Logic: Login, logout, password validation

  • Authorization Logic: Permission checking, group management

  • Models: User operations, data validation

  • Entities: User entity behavior

  • Services: Auth services functionality

Integration Tests๏ƒ

  • Controllers: Full request/response cycles

  • Filters: Request filtering and security

  • Database: Data persistence and retrieval

  • Commands: CLI commands functionality

Feature Tests๏ƒ

  • User Registration: Complete registration flow

  • Login Process: Authentication workflow

  • Password Reset: Reset functionality

  • Access Control: Permission-based access

๐Ÿ”ง Test Setup๏ƒ

Base Test Class๏ƒ

<?php
namespace Tests\Authentication;

use Tests\Support\DatabaseTestCase;
use Daycry\Auth\Entities\User;
use Daycry\Auth\Models\UserModel;

class MySessionTest extends DatabaseTestCase
{
    protected User $user;

    protected function setUp(): void
    {
        parent::setUp();

        // Use CI4 Fabricator to create a user with hashed password + email identity
        $this->user = fake(UserModel::class);
    }

    protected function tearDown(): void
    {
        parent::tearDown();
        auth('session')->logout();
    }
}

Mock Configuration๏ƒ

Use the built-in helpers to override config properties for a single test:

// Override Auth config (authenticators, actions, views, routes, session)
$this->injectMockAttributes(['defaultAuthenticator' => 'jwt']);
$this->injectMockAttributes(['actions' => ['login' => \Daycry\Auth\Authentication\Actions\Totp2FA::class]]);

// Override AuthSecurity config (passwords, lockout, rate-limit, TOTP, token lifetimes)
$this->injectMockAttributesSecurity(['userMaxAttempts' => 3]);
$this->injectMockAttributesSecurity(['minimumPasswordLength' => 12]);

// Override AuthOAuth config (provider definitions)
$this->injectMockAttributesOAuth(['providers' => ['google' => [...]]]);

Each call replaces only the specified keys; unspecified keys keep their defaults.

The previous typoโ€™d names (inkectMockAttributes*) still work as deprecated aliases for backward-compatibility but will be removed in v6 โ€” migrate any custom tests to the spelled-correctly variants.

๐Ÿ›ก๏ธ Testing Authentication๏ƒ

Login Tests๏ƒ

<?php
class SessionAuthenticatorTest extends AuthenticationTestCase
{
    public function testLoginSuccess(): void
    {
        $result = $this->auth->attempt([
            'email'    => 'test@example.com',
            'password' => 'secret123'
        ]);

        $this->assertTrue($result->isOK());
        $this->assertTrue($this->auth->loggedIn());
        $this->assertSame($this->user->id, $this->auth->id());
    }

    public function testLoginWithInvalidCredentials(): void
    {
        $result = $this->auth->attempt([
            'email'    => 'test@example.com',
            'password' => 'wrongpassword'
        ]);

        $this->assertFalse($result->isOK());
        $this->assertFalse($this->auth->loggedIn());
        $this->assertStringContainsString('Invalid credentials', $result->reason());
    }

    public function testLoginWithNonExistentUser(): void
    {
        $result = $this->auth->attempt([
            'email'    => 'nonexistent@example.com',
            'password' => 'secret123'
        ]);

        $this->assertFalse($result->isOK());
        $this->assertFalse($this->auth->loggedIn());
    }
}

Logout Tests๏ƒ

<?php
public function testLogout(): void
{
    // Login first
    $this->auth->attempt([
        'email'    => 'test@example.com',
        'password' => 'secret123'
    ]);

    $this->assertTrue($this->auth->loggedIn());

    // Logout
    $this->auth->logout();

    $this->assertFalse($this->auth->loggedIn());
    $this->assertNull($this->auth->user());
    $this->assertNull($this->auth->id());
}

Remember Me Tests๏ƒ

<?php
public function testRememberMeFunctionality(): void
{
    $result = $this->auth->remember()->attempt([
        'email'    => 'test@example.com',
        'password' => 'secret123'
    ]);

    $this->assertTrue($result->isOK());
    
    // Check remember token exists
    $user = $this->auth->user();
    $this->assertNotNull($user->remember_token);
    
    // Simulate new session
    session()->destroy();
    
    // Should still be logged in via remember token
    $this->assertTrue($this->auth->loggedIn());
}

๐Ÿ‘ฅ Testing Authorization๏ƒ

Permission Tests๏ƒ

<?php
class AuthorizationTest extends DatabaseTestCase
{
    public function testUserHasPermission(): void
    {
        $user = $this->createUserWithPermission('posts.create');
        
        $this->auth->login($user);
        
        $this->assertTrue($this->auth->user()->can('posts.create'));
        $this->assertFalse($this->auth->user()->can('posts.delete'));
    }

    public function testGroupPermissions(): void
    {
        $group = $this->createGroup('editors', ['posts.create', 'posts.edit']);
        $user  = $this->createUser();
        
        $user->addToGroup($group);
        $this->auth->login($user);

        $this->assertTrue($this->auth->user()->inGroup('editors'));
        $this->assertTrue($this->auth->user()->can('posts.create'));
        $this->assertTrue($this->auth->user()->can('posts.edit'));
    }

    private function createUserWithPermission(string $permission): User
    {
        $user = $this->createUser();
        $user->addPermission($permission);
        
        return $user;
    }
}

๐ŸŽ›๏ธ Testing Controllers๏ƒ

Controller Test Example๏ƒ

<?php
class BaseAuthControllerTest extends DatabaseTestCase
{
    private MockBaseAuthController $controller;
    private IncomingRequest $request;
    private Response $response;

    protected function setUp(): void
    {
        parent::setUp();

        $this->request  = $this->createMockRequest();
        $this->response = new Response(new App());
        
        $this->controller = new MockBaseAuthController();
        $this->controller->initController($this->request, $this->response, service('logger'));
    }

    public function testControllerInitialization(): void
    {
        $this->assertNotNull($this->controller->publicAuthHandler);
        $this->assertInstanceOf(BaseAuthController::class, $this->controller);
    }

    public function testAjaxResponse(): void
    {
        $this->request->setHeader('X-Requested-With', 'XMLHttpRequest');
        
        $response = $this->controller->testMethod();
        
        $this->assertSame(200, $response->getStatusCode());
        $this->assertJson($response->getBody());
    }

    private function createMockRequest(): IncomingRequest
    {
        $userAgent = $this->createMock(UserAgent::class);
        
        return new IncomingRequest(
            new App(),
            new URI('http://example.com/test'),
            'php://input',
            $userAgent
        );
    }
}

๐Ÿ” Testing Filters๏ƒ

Filter Test Example๏ƒ

<?php
class AuthFilterTest extends DatabaseTestCase
{
    public function testFilterAllowsAuthenticatedUser(): void
    {
        $this->loginAsUser();
        
        $request  = service('request');
        $response = service('response');
        
        $filter = new AuthFilter();
        $result = $filter->before($request);
        
        $this->assertNull($result); // No redirect means allowed
    }

    public function testFilterRedirectsUnauthenticatedUser(): void
    {
        $request  = service('request');
        $response = service('response');
        
        $filter = new AuthFilter();
        $result = $filter->before($request);
        
        $this->assertInstanceOf(RedirectResponse::class, $result);
        $this->assertStringContainsString('/login', $result->getHeaderLine('Location'));
    }

    public function testPermissionFilter(): void
    {
        $user = $this->createUserWithPermission('admin.access');
        $this->auth->login($user);
        
        $filter = new PermissionFilter();
        $result = $filter->before(service('request'), ['admin.access']);
        
        $this->assertNull($result); // Allowed
    }
}

๐Ÿ“Š Testing Models๏ƒ

User Model Tests๏ƒ

<?php
class UserModelTest extends DatabaseTestCase
{
    public function testCreateUser(): void
    {
        $userData = [
            'username' => 'newuser',
            'email'    => 'new@example.com',
            'password' => 'password123'
        ];

        $userModel = model('UserModel');
        $userId = $userModel->insert($userData);

        $this->assertIsInt($userId);
        
        $user = $userModel->find($userId);
        $this->assertSame('newuser', $user->username);
        $this->assertSame('new@example.com', $user->email);
    }

    public function testPasswordHashing(): void
    {
        $user = new User([
            'username' => 'testuser',
            'email'    => 'test@example.com',
            'password' => 'plaintext'
        ]);

        $this->assertNotSame('plaintext', $user->password_hash);
        $this->assertTrue(password_verify('plaintext', $user->password_hash));
    }

    public function testEmailValidation(): void
    {
        $userModel = model('UserModel');
        
        $result = $userModel->insert([
            'username' => 'testuser',
            'email'    => 'invalid-email',
            'password' => 'password123'
        ]);

        $this->assertFalse($result);
        $this->assertArrayHasKey('email', $userModel->errors());
    }
}

๐Ÿ—๏ธ Testing Traits๏ƒ

Testing BaseControllerTrait๏ƒ

<?php
class BaseControllerTraitTest extends DatabaseTestCase
{
    use BaseControllerTrait;

    public function testGetToken(): void
    {
        $token = $this->getToken();
        
        $this->assertIsArray($token);
        $this->assertArrayHasKey('name', $token);
        $this->assertArrayHasKey('hash', $token);
        $this->assertNotEmpty($token['name']);
        $this->assertNotEmpty($token['hash']);
    }

    public function testSetRequestUnauthorized(): void
    {
        $this->setRequestUnauthorized();
        
        $this->assertFalse($this->isRequestAuthorized());
    }
}

๐ŸŽฏ Testing Best Practices๏ƒ

1. Test Isolation๏ƒ

<?php
// Each test should be independent
protected function setUp(): void
{
    parent::setUp();
    $this->refreshDatabase();
    $this->createFreshUser();
}

2. Clear Test Names๏ƒ

<?php
// Good: Descriptive test names
public function testUserCannotLoginWithExpiredPassword(): void
public function testAdminCanAccessUserManagement(): void
public function testGuestIsRedirectedToLogin(): void

// Bad: Vague test names
public function testLogin(): void
public function testAccess(): void

3. Test Data Factories๏ƒ

<?php
class UserFactory
{
    public static function create(array $overrides = []): User
    {
        return new User(array_merge([
            'username' => fake()->userName(),
            'email'    => fake()->email(),
            'password' => 'password123',
            'active'   => true,
        ], $overrides));
    }

    public static function createWithPermissions(array $permissions): User
    {
        $user = self::create();
        foreach ($permissions as $permission) {
            $user->addPermission($permission);
        }
        return $user;
    }
}

4. Mock External Dependencies๏ƒ

<?php
public function testEmailNotificationSent(): void
{
    $emailMock = $this->createMock(EmailService::class);
    $emailMock->expects($this->once())
              ->method('send')
              ->with($this->equalTo('user@example.com'));

    Services::injectMock('email', $emailMock);
    
    // Test code that should trigger email
}

๐Ÿš€ Contributing Tests๏ƒ

Writing New Tests๏ƒ

  1. Follow naming conventions: Use descriptive test method names

  2. Test one thing: Each test should verify one specific behavior

  3. Use assertions properly: Choose the most specific assertion available

  4. Clean up: Ensure tests donโ€™t leave side effects

Test Coverage๏ƒ

# Generate coverage report
composer test:coverage

# View coverage in browser
open build/coverage/html/index.html

Pull Request Testing๏ƒ

Before submitting a PR:

# Run full test suite
composer test

# Check code style
composer cs:check

# Run static analysis
composer analyze

# Ensure no deprecation warnings
composer test -- --display-deprecations

Test Examples Repository๏ƒ

For more test examples, check the /tests directory:

  • tests/Authentication/ - Authentication tests

  • tests/Authorization/ - Authorization tests

  • tests/Controllers/ - Controller tests

  • tests/Entities/ - Entity tests

  • tests/Models/ - Model tests (including OAuthTokenRepositoryTest)

  • tests/Libraries/OauthManagerTest.php - OAuth manager tests (events, scopes, profile, refresh)

  • tests/Libraries/Oauth/ProfileResolver/ - Profile resolver tests (factory, Azure, generic)