# ๐Ÿงช 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](#๏ธ-quick-start) - [๐Ÿงช Test Categories](#-test-categories) - [๐Ÿ”ง Test Setup](#-test-setup) - [๐Ÿ›ก๏ธ Testing Authentication](#๏ธ-testing-authentication) - [๐Ÿ‘ฅ Testing Authorization](#-testing-authorization) - [๐ŸŽ›๏ธ Testing Controllers](#๏ธ-testing-controllers) - [๐Ÿ” Testing Filters](#-testing-filters) - [๐Ÿ“Š Testing Models](#-testing-models) - [๐Ÿ—๏ธ Testing Traits](#๏ธ-testing-traits) - [๐ŸŽฏ Testing Best Practices](#-testing-best-practices) - [๐Ÿš€ Contributing Tests](#-contributing-tests) ## ๐Ÿƒโ€โ™‚๏ธ Quick Start ### Running Tests ```bash # 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 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 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: ```php // 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 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 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 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 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 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 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 '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 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 refreshDatabase(); $this->createFreshUser(); } ``` ### 2. Clear Test Names ```php 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 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 ```bash # Generate coverage report composer test:coverage # View coverage in browser open build/coverage/html/index.html ``` ### Pull Request Testing Before submitting a PR: ```bash # 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) ## ๐Ÿ”— Related Documentation - [Quick Start](01-quick-start.md) - Getting started with Daycry Auth - [Authentication](03-authentication.md) - Authentication system details - [Authorization](06-authorization.md) - Permission and role system - [Controllers](05-controllers.md) - Controller implementation - [Filters](04-filters.md) - Security filters --- Remember: Good tests are an investment in your application's reliability and your team's confidence. Write tests that clearly express intent, are easy to maintain, and provide valuable feedback when things break.