* @link https://descomplicar.pt * @since 1.0.0 */ namespace Care_API\Tests\Unit\Endpoints; use Care_API\Endpoints\Auth_Endpoints; use WP_REST_Server; use WP_REST_Request; use WP_REST_Response; use WP_Error; use WP_User; /** * Class AuthEndpointsTest * * Unit tests for Auth_Endpoints class covering: * - Login/logout functionality * - Token management and validation * - User profile operations * - Permission and authorization checks * - Rate limiting and security measures * * @since 1.0.0 */ class AuthEndpointsTest extends \Care_API_Test_Case { /** * Auth_Endpoints instance for testing * * @var Auth_Endpoints */ private $auth_endpoints; /** * Test users for different scenarios * * @var array */ private $test_users = array(); /** * Test setup before each test method * * @since 1.0.0 */ public function setUp(): void { parent::setUp(); // Create test users with specific roles $this->test_users = array( 'admin' => $this->create_test_user('administrator'), 'doctor' => $this->create_test_user('kivicare_doctor'), 'patient' => $this->create_test_user('kivicare_patient'), 'receptionist' => $this->create_test_user('kivicare_receptionist'), 'subscriber' => $this->create_test_user('subscriber') // No API access ); // Mock WordPress functions for testing $this->mock_wordpress_functions(); // Register auth endpoints Auth_Endpoints::register_routes(); } /** * Test 1: Authentication Route Registration * * Verifies that all authentication routes are properly registered: * - Login endpoint with proper methods and validation * - Logout endpoint with authentication requirement * - Token refresh and validation endpoints * - Profile management endpoints * - Password reset endpoints * * @test * @since 1.0.0 */ public function test_authentication_route_registration() { // Get all registered routes $routes = $this->server->get_routes(); // Define expected authentication endpoints $expected_auth_routes = array( '/care/v1/auth/login' => array('POST'), '/care/v1/auth/logout' => array('POST'), '/care/v1/auth/refresh' => array('POST'), '/care/v1/auth/validate' => array('GET'), '/care/v1/auth/profile' => array('GET', 'PUT'), '/care/v1/auth/forgot-password' => array('POST'), '/care/v1/auth/reset-password' => array('POST'), ); foreach ($expected_auth_routes as $route => $methods) { $this->assertArrayHasKey( $route, $routes, "Route {$route} should be registered" ); $route_config = $routes[$route]; $registered_methods = array(); // Extract registered methods from route configuration foreach ($route_config as $handler) { if (isset($handler['methods'])) { $handler_methods = (array) $handler['methods']; $registered_methods = array_merge($registered_methods, $handler_methods); } } foreach ($methods as $method) { $this->assertContains( $method, $registered_methods, "Route {$route} should support {$method} method" ); } } // Test route callbacks are properly set $login_route = $routes['/care/v1/auth/login'][0]; $this->assertArrayHasKey('callback', $login_route, 'Login route should have callback'); $this->assertArrayHasKey('permission_callback', $login_route, 'Login route should have permission callback'); $this->assertArrayHasKey('args', $login_route, 'Login route should have argument validation'); // Test login route arguments $login_args = $login_route['args']; $this->assertArrayHasKey('username', $login_args, 'Login should require username parameter'); $this->assertArrayHasKey('password', $login_args, 'Login should require password parameter'); $this->assertTrue( $login_args['username']['required'], 'Username should be required parameter' ); $this->assertTrue( $login_args['password']['required'], 'Password should be required parameter' ); } /** * Test 2: Login Functionality and Validation * * Tests complete login workflow: * - Valid credential authentication * - Invalid credential rejection * - User permission validation * - Rate limiting enforcement * - Response data structure * * @test * @since 1.0.0 */ public function test_login_functionality_and_validation() { // Test valid login with authorized user (admin) $admin_user = get_userdata($this->test_users['admin']); // Mock wp_authenticate to return valid user add_filter('authenticate', function($user, $username, $password) use ($admin_user) { if ($username === $admin_user->user_login && $password === 'valid_password') { return $admin_user; } return new WP_Error('invalid_credentials', 'Invalid credentials'); }, 10, 3); $login_request = new WP_REST_Request('POST', '/care/v1/auth/login'); $login_request->set_body_params(array( 'username' => $admin_user->user_login, 'password' => 'valid_password' )); $login_response = $this->server->dispatch($login_request); // Test successful login response structure if ($login_response->get_status() === 200) { $response_data = $login_response->get_data(); $this->assertArrayHasKey('success', $response_data); $this->assertTrue($response_data['success']); $this->assertArrayHasKey('data', $response_data); $this->assertArrayHasKey('user', $response_data['data']); $user_data = $response_data['data']['user']; $this->assertArrayHasKey('id', $user_data); $this->assertArrayHasKey('username', $user_data); $this->assertArrayHasKey('email', $user_data); $this->assertArrayHasKey('roles', $user_data); $this->assertArrayHasKey('capabilities', $user_data); } // Test invalid credentials $invalid_login_request = new WP_REST_Request('POST', '/care/v1/auth/login'); $invalid_login_request->set_body_params(array( 'username' => 'invalid_user', 'password' => 'invalid_password' )); $invalid_response = $this->server->dispatch($invalid_login_request); $this->assertEquals( 401, $invalid_response->get_status(), 'Invalid credentials should return 401 status' ); // Test missing parameters $missing_params_request = new WP_REST_Request('POST', '/care/v1/auth/login'); $missing_params_request->set_body_params(array( 'username' => 'testuser' // Missing password )); $missing_response = $this->server->dispatch($missing_params_request); $this->assertEquals( 400, $missing_response->get_status(), 'Missing parameters should return 400 status' ); // Test username validation $this->assertTrue( Auth_Endpoints::validate_username('valid_user'), 'Valid username should pass validation' ); $this->assertTrue( Auth_Endpoints::validate_username('user@example.com'), 'Valid email should pass username validation' ); $this->assertFalse( Auth_Endpoints::validate_username(''), 'Empty username should fail validation' ); // Test password validation $this->assertTrue( Auth_Endpoints::validate_password('validpassword123'), 'Valid password should pass validation' ); $this->assertFalse( Auth_Endpoints::validate_password('short'), 'Short password should fail validation' ); $this->assertFalse( Auth_Endpoints::validate_password(''), 'Empty password should fail validation' ); // Clean up filter remove_all_filters('authenticate'); } /** * Test 3: User Authorization and Permissions * * Validates user permission system: * - Role-based API access control * - Capability-based endpoint access * - User status validation (active/suspended) * - API capability assignment per role * * @test * @since 1.0.0 */ public function test_user_authorization_and_permissions() { // Test different user roles and their API access $role_access_tests = array( 'administrator' => true, 'kivicare_doctor' => true, 'kivicare_patient' => true, 'kivicare_receptionist' => true, 'subscriber' => false // Should not have API access ); foreach ($role_access_tests as $role => $should_have_access) { $user_id = $this->test_users[str_replace('kivicare_', '', $role)] ?? $this->test_users['subscriber']; $user = get_userdata($user_id); // Mock user_can_access_api method behavior $reflection = new \ReflectionClass(Auth_Endpoints::class); $method = $reflection->getMethod('user_can_access_api'); $method->setAccessible(true); $has_access = $method->invokeArgs(null, array($user)); if ($should_have_access) { $this->assertTrue( $has_access, "User with role {$role} should have API access" ); } else { $this->assertFalse( $has_access, "User with role {$role} should not have API access" ); } } // Test user capability assignment $admin_user = get_userdata($this->test_users['admin']); $doctor_user = get_userdata($this->test_users['doctor']); $patient_user = get_userdata($this->test_users['patient']); // Mock get_user_api_capabilities method $reflection = new \ReflectionClass(Auth_Endpoints::class); $method = $reflection->getMethod('get_user_api_capabilities'); $method->setAccessible(true); // Test admin capabilities $admin_caps = $method->invokeArgs(null, array($admin_user)); $this->assertIsArray($admin_caps, 'Admin capabilities should be array'); $this->assertContains('read_clinics', $admin_caps, 'Admin should have read_clinics capability'); $this->assertContains('create_clinics', $admin_caps, 'Admin should have create_clinics capability'); $this->assertContains('manage_settings', $admin_caps, 'Admin should have manage_settings capability'); // Test doctor capabilities $doctor_caps = $method->invokeArgs(null, array($doctor_user)); $this->assertIsArray($doctor_caps, 'Doctor capabilities should be array'); $this->assertContains('read_patients', $doctor_caps, 'Doctor should have read_patients capability'); $this->assertContains('create_encounters', $doctor_caps, 'Doctor should have create_encounters capability'); $this->assertNotContains('delete_clinics', $doctor_caps, 'Doctor should not have delete_clinics capability'); // Test patient capabilities (more limited) $patient_caps = $method->invokeArgs(null, array($patient_user)); $this->assertIsArray($patient_caps, 'Patient capabilities should be array'); $this->assertContains('read_appointments', $patient_caps, 'Patient should have read_appointments capability'); $this->assertNotContains('create_patients', $patient_caps, 'Patient should not have create_patients capability'); // Test user status validation $active_user = get_userdata($this->test_users['admin']); // Mock is_user_active method $is_active_method = $reflection->getMethod('is_user_active'); $is_active_method->setAccessible(true); $this->assertTrue( $is_active_method->invokeArgs(null, array($active_user)), 'User without status meta should be considered active' ); // Test suspended user update_user_meta($this->test_users['admin'], 'account_status', 'suspended'); $this->assertFalse( $is_active_method->invokeArgs(null, array($active_user)), 'User with suspended status should not be active' ); // Clean up delete_user_meta($this->test_users['admin'], 'account_status'); } /** * Test 4: Profile Management Operations * * Tests user profile endpoints: * - Profile data retrieval * - Profile update operations * - Data validation and sanitization * - Permission checks for profile access * * @test * @since 1.0.0 */ public function test_profile_management_operations() { // Set current user for profile operations wp_set_current_user($this->test_users['admin']); // Test profile retrieval $profile_request = new WP_REST_Request('GET', '/care/v1/auth/profile'); $profile_response = $this->server->dispatch($profile_request); if ($profile_response->get_status() === 200) { $profile_data = $profile_response->get_data(); $this->assertArrayHasKey('success', $profile_data); $this->assertTrue($profile_data['success']); $this->assertArrayHasKey('data', $profile_data); $user_data = $profile_data['data']; // Test required profile fields $required_fields = array('id', 'username', 'email', 'first_name', 'last_name', 'roles'); foreach ($required_fields as $field) { $this->assertArrayHasKey( $field, $user_data, "Profile data should contain {$field} field" ); } // Test profile meta fields $this->assertArrayHasKey('profile', $user_data, 'Should contain profile meta data'); } // Test profile update $update_request = new WP_REST_Request('PUT', '/care/v1/auth/profile'); $update_request->set_body_params(array( 'first_name' => 'Updated First', 'last_name' => 'Updated Last', 'email' => 'updated@example.com' )); $update_response = $this->server->dispatch($update_request); if ($update_response->get_status() === 200) { $update_data = $update_response->get_data(); $this->assertArrayHasKey('success', $update_data); $this->assertTrue($update_data['success']); } // Test profile update with invalid data $invalid_update_request = new WP_REST_Request('PUT', '/care/v1/auth/profile'); $invalid_update_request->set_body_params(array( 'email' => 'invalid-email-format' )); $invalid_update_response = $this->server->dispatch($invalid_update_request); $this->assertGreaterThanOrEqual( 400, $invalid_update_response->get_status(), 'Invalid email should return error status' ); // Test profile access without authentication wp_set_current_user(0); $unauth_profile_request = new WP_REST_Request('GET', '/care/v1/auth/profile'); $unauth_response = $this->server->dispatch($unauth_profile_request); $this->assertEquals( 401, $unauth_response->get_status(), 'Unauthenticated profile access should return 401' ); } /** * Test 5: Rate Limiting and Security Measures * * Validates security implementations: * - Rate limiting for authentication attempts * - Token management and validation * - Password reset security * - Request logging and monitoring * - IP-based restrictions * * @test * @since 1.0.0 */ public function test_rate_limiting_and_security_measures() { // Test rate limiting for login attempts $rate_limit_result = Auth_Endpoints::check_rate_limit(); $this->assertTrue( is_bool($rate_limit_result) || is_wp_error($rate_limit_result), 'Rate limit check should return boolean or WP_Error' ); // Simulate multiple failed login attempts to test rate limiting $client_ip = '192.168.1.100'; $_SERVER['REMOTE_ADDR'] = $client_ip; // Mock transient functions for rate limiting $rate_limit_key = 'auth_rate_limit_' . md5($client_ip); set_transient($rate_limit_key, 5, 900); // Set to limit $rate_limited_result = Auth_Endpoints::check_rate_limit(); $this->assertInstanceOf( 'WP_Error', $rate_limited_result, 'Rate limit should return WP_Error when exceeded' ); if (is_wp_error($rate_limited_result)) { $this->assertEquals( 'rate_limit_exceeded', $rate_limited_result->get_error_code(), 'Rate limit error should have correct error code' ); } // Clean up transient delete_transient($rate_limit_key); // Test password reset security $forgot_password_request = new WP_REST_Request('POST', '/care/v1/auth/forgot-password'); $forgot_password_request->set_body_params(array( 'username' => 'nonexistent@example.com' )); $forgot_response = $this->server->dispatch($forgot_password_request); // Should return success even for non-existent users (security measure) if ($forgot_response->get_status() === 200) { $forgot_data = $forgot_response->get_data(); $this->assertArrayHasKey('success', $forgot_data); $this->assertTrue($forgot_data['success']); $this->assertStringContainsString( 'If the user exists', $forgot_data['message'], 'Should not reveal whether user exists' ); } // Test token extraction from request $test_request = new WP_REST_Request('GET', '/care/v1/auth/validate'); $test_request->set_header('authorization', 'Bearer test-jwt-token-here'); // Mock get_token_from_request method $reflection = new \ReflectionClass(Auth_Endpoints::class); $method = $reflection->getMethod('get_token_from_request'); $method->setAccessible(true); $extracted_token = $method->invokeArgs(null, array($test_request)); $this->assertEquals( 'test-jwt-token-here', $extracted_token, 'Should correctly extract JWT token from Authorization header' ); // Test client IP detection $ip_method = $reflection->getMethod('get_client_ip'); $ip_method->setAccessible(true); // Test with various IP headers $_SERVER['HTTP_CF_CONNECTING_IP'] = '203.0.113.1'; $detected_ip = $ip_method->invoke(null); $this->assertEquals( '203.0.113.1', $detected_ip, 'Should detect IP from Cloudflare header' ); // Test fallback to REMOTE_ADDR unset($_SERVER['HTTP_CF_CONNECTING_IP']); $_SERVER['REMOTE_ADDR'] = '192.168.1.50'; $fallback_ip = $ip_method->invoke(null); $this->assertEquals( '192.168.1.50', $fallback_ip, 'Should fallback to REMOTE_ADDR when no proxy headers' ); // Test password reset validation $reset_request = new WP_REST_Request('POST', '/care/v1/auth/reset-password'); $reset_request->set_body_params(array( 'key' => 'invalid-key', 'login' => 'testuser', 'password' => 'newpassword123' )); $reset_response = $this->server->dispatch($reset_request); $this->assertGreaterThanOrEqual( 400, $reset_response->get_status(), 'Invalid reset key should return error status' ); } /** * Test teardown after each test method * * @since 1.0.0 */ public function tearDown(): void { // Clear current user wp_set_current_user(0); // Clear any transients set during testing global $wpdb; $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_auth_rate_limit_%'"); $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_auth_rate_limit_%'"); // Clean up server variables unset($_SERVER['HTTP_CF_CONNECTING_IP']); unset($_SERVER['HTTP_X_FORWARDED_FOR']); $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; parent::tearDown(); } /** * Helper method to create test user with specific role * * @param string $role User role * @return int User ID * @since 1.0.0 */ private function create_test_user($role) { return $this->factory->user->create(array( 'user_login' => 'test_' . $role . '_' . wp_rand(1000, 9999), 'user_email' => 'test' . wp_rand(1000, 9999) . '@example.com', 'user_pass' => 'test_password_123', 'first_name' => 'Test', 'last_name' => 'User', 'role' => $role )); } /** * Mock WordPress functions needed for testing * * @since 1.0.0 */ private function mock_wordpress_functions() { // Mock password reset functions if they don't exist if (!function_exists('get_password_reset_key')) { function get_password_reset_key($user) { return 'mock_reset_key_' . $user->ID . '_' . time(); } } if (!function_exists('check_password_reset_key')) { function check_password_reset_key($key, $login) { if (strpos($key, 'mock_reset_key_') === 0) { return get_user_by('login', $login); } return new \WP_Error('invalid_key', 'Invalid key'); } } // Mock wp_mail function if (!function_exists('wp_mail')) { function wp_mail($to, $subject, $message, $headers = '', $attachments = array()) { return true; // Always succeed for testing } } // Mock sanitization functions if needed if (!function_exists('sanitize_user')) { function sanitize_user($username, $strict = false) { return strip_tags($username); } } if (!function_exists('validate_username')) { function validate_username($username) { return !empty($username) && strlen($username) >= 3; } } } }