๐งช 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 |
|---|---|
|
No database needed (unit tests, filter tests) |
|
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 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๏
Follow naming conventions: Use descriptive test method names
Test one thing: Each test should verify one specific behavior
Use assertions properly: Choose the most specific assertion available
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 teststests/Authorization/- Authorization teststests/Controllers/- Controller teststests/Entities/- Entity teststests/Models/- Model tests (includingOAuthTokenRepositoryTest)tests/Libraries/OauthManagerTest.php- OAuth manager tests (events, scopes, profile, refresh)tests/Libraries/Oauth/ProfileResolver/- Profile resolver tests (factory, Azure, generic)