* @since 1.0.0 */ declare(strict_types=1); namespace Care_API\Tests\Unit\Security; use Care_API\Security\Security_Manager; use WP_REST_Request; use WP_Error; /** * SecurityManagerTest Class * * Unit tests for Security_Manager functionality ensuring robust security controls * and compliance with healthcare data protection requirements. */ class SecurityManagerTest extends \Care_API_Test_Case { /** * Original $_SERVER superglobal backup * * @var array */ private $server_backup; /** * Test transient storage * * @var array */ private $test_transients = []; /** * Set up test environment before each test * * @return void */ public function setUp(): void { parent::setUp(); // Backup original $_SERVER $this->server_backup = $_SERVER; // Clear test transients $this->test_transients = []; // Setup mock transient functions $this->setup_transient_filters(); // Clear WordPress transients wp_cache_flush(); } /** * Clean up test environment after each test * * @return void */ public function tearDown(): void { // Restore original $_SERVER $_SERVER = $this->server_backup; // Clear test transients $this->test_transients = []; // Remove transient filters $this->remove_transient_filters(); // Clear WordPress transients wp_cache_flush(); parent::tearDown(); } /** * Test 1: Validate endpoint permissions functionality * * Tests permission validation for different endpoint types: * - Public endpoints (status, health, version) * - Auth endpoints (login, password reset) * - Protected endpoints (require JWT) * * @covers Security_Manager::check_api_permissions * @covers Security_Manager::is_public_endpoint * @covers Security_Manager::is_auth_endpoint * @covers Security_Manager::validate_public_access * @covers Security_Manager::validate_auth_access * @covers Security_Manager::verify_jwt_authentication */ public function test_validate_endpoint_permissions(): void { // Test Case 1: Invalid request object should return WP_Error $result = Security_Manager::check_api_permissions(null); $this->assertInstanceOf(WP_Error::class, $result); $this->assertEquals('invalid_request', $result->get_error_code()); $this->assertEquals(400, $result->get_error_data()['status']); // Test Case 2: Public endpoint should be allowed with rate limiting $public_request = $this->create_mock_request('GET', '/kivicare/v1/status'); $this->setup_server_vars('127.0.0.1', 'Mozilla/5.0 Test'); // Set rate limit to pass (0 current requests) $this->set_test_transient('care_api_rate_limit_public_127.0.0.1', 0); $result = Security_Manager::check_api_permissions($public_request); $this->assertTrue($result); // Test Case 3: Auth endpoint with valid nonce should pass $auth_request = $this->create_mock_request('POST', '/kivicare/v1/auth/login'); $auth_request->set_header('X-WP-Nonce', 'valid_nonce'); $auth_request->set_header('Content-Type', 'application/json'); // Create nonce that will validate $nonce = wp_create_nonce('wp_rest'); $auth_request->set_header('X-WP-Nonce', $nonce); // Set rate limit to pass $this->set_test_transient('care_api_rate_limit_auth_127.0.0.1', 5); $result = Security_Manager::check_api_permissions($auth_request); $this->assertTrue($result); // Test Case 4: Protected endpoint without JWT should fail $unauth_request = $this->create_mock_request('GET', '/kivicare/v1/patients'); $result = Security_Manager::check_api_permissions($unauth_request); $this->assertInstanceOf(WP_Error::class, $result); $this->assertEquals('missing_authorization', $result->get_error_code()); $this->assertEquals(401, $result->get_error_data()['status']); // Test Case 5: Protected endpoint with invalid Bearer format should fail $invalid_auth_request = $this->create_mock_request('GET', '/kivicare/v1/patients'); $invalid_auth_request->set_header('Authorization', 'Basic dXNlcjpwYXNz'); $result = Security_Manager::check_api_permissions($invalid_auth_request); $this->assertInstanceOf(WP_Error::class, $result); $this->assertEquals('invalid_authorization_format', $result->get_error_code()); $this->assertEquals(401, $result->get_error_data()['status']); } /** * Test 2: Rate limiting enforcement * * Tests rate limiting functionality for different endpoint types: * - Public endpoints: 100 requests/hour * - Auth endpoints: 10 requests/hour * - Protected endpoints: 1000 requests/hour * * @covers Security_Manager::check_rate_limit * @covers Security_Manager::get_client_ip */ public function test_rate_limiting_enforcement(): void { $this->setup_server_vars('192.168.1.100', 'Mozilla/5.0 Test'); // Test Case 1: Public endpoint within rate limit $public_request = $this->create_mock_request('GET', '/kivicare/v1/status'); // Set 50 current requests, limit is 100 $this->set_test_transient('care_api_rate_limit_public_192.168.1.100', 50); $result = Security_Manager::check_api_permissions($public_request); $this->assertTrue($result); // Test Case 2: Public endpoint exceeding rate limit $public_request_exceeded = $this->create_mock_request('GET', '/kivicare/v1/health'); // Set 100 current requests, limit is 100 $this->set_test_transient('care_api_rate_limit_public_192.168.1.100', 100); $result = Security_Manager::check_api_permissions($public_request_exceeded); $this->assertInstanceOf(WP_Error::class, $result); $this->assertEquals('rate_limit_exceeded', $result->get_error_code()); $this->assertEquals(429, $result->get_error_data()['status']); // Test Case 3: Auth endpoint within strict rate limit $auth_request = $this->create_mock_request('POST', '/kivicare/v1/auth/login'); $auth_request->set_header('Content-Type', 'application/json'); // Create valid nonce $nonce = wp_create_nonce('wp_rest'); $auth_request->set_header('X-WP-Nonce', $nonce); // Set 5 current requests, limit is 10 $this->set_test_transient('care_api_rate_limit_auth_192.168.1.100', 5); $result = Security_Manager::check_api_permissions($auth_request); $this->assertTrue($result); // Test Case 4: Auth endpoint exceeding strict rate limit $auth_request_exceeded = $this->create_mock_request('POST', '/kivicare/v1/auth/forgot-password'); // Set 10 current requests, limit is 10 $this->set_test_transient('care_api_rate_limit_auth_192.168.1.100', 10); $result = Security_Manager::check_api_permissions($auth_request_exceeded); $this->assertInstanceOf(WP_Error::class, $result); $this->assertEquals('rate_limit_exceeded', $result->get_error_code()); // Test Case 5: Different IP addresses have separate rate limits $this->setup_server_vars('10.0.0.1', 'Mozilla/5.0 Test'); $different_ip_request = $this->create_mock_request('GET', '/kivicare/v1/status'); // Set 0 current requests for new IP $this->set_test_transient('care_api_rate_limit_public_10.0.0.1', 0); $result = Security_Manager::check_api_permissions($different_ip_request); $this->assertTrue($result); } /** * Test 3: Authentication requirement check * * Tests authentication requirement validation for different scenarios: * - Missing Authorization header * - Invalid Bearer format * - JWT service availability check * * @covers Security_Manager::verify_jwt_authentication */ public function test_authentication_requirement_check(): void { // Test Case 1: Missing Authorization header $request_no_auth = $this->create_mock_request('GET', '/kivicare/v1/patients'); $result = Security_Manager::check_api_permissions($request_no_auth); $this->assertInstanceOf(WP_Error::class, $result); $this->assertEquals('missing_authorization', $result->get_error_code()); $this->assertEquals(401, $result->get_error_data()['status']); // Test Case 2: Invalid Bearer format $request_invalid_format = $this->create_mock_request('GET', '/kivicare/v1/patients'); $request_invalid_format->set_header('Authorization', 'Basic dXNlcjpwYXNz'); $result = Security_Manager::check_api_permissions($request_invalid_format); $this->assertInstanceOf(WP_Error::class, $result); $this->assertEquals('invalid_authorization_format', $result->get_error_code()); $this->assertEquals(401, $result->get_error_data()['status']); // Test Case 3: Valid Bearer format - JWT service will be checked $request_valid_format = $this->create_mock_request('GET', '/kivicare/v1/patients'); $request_valid_format->set_header('Authorization', 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9'); $result = Security_Manager::check_api_permissions($request_valid_format); // Should either succeed (if JWT service available) or fail with JWT service unavailable if (is_wp_error($result)) { $this->assertContains($result->get_error_code(), ['jwt_service_unavailable', 'invalid_token']); } else { $this->assertTrue($result); } // Test Case 4: Empty Bearer token $request_empty_token = $this->create_mock_request('GET', '/kivicare/v1/patients'); $request_empty_token->set_header('Authorization', 'Bearer '); $result = Security_Manager::check_api_permissions($request_empty_token); // Should fail with JWT service unavailable or invalid token $this->assertInstanceOf(WP_Error::class, $result); $this->assertContains($result->get_error_code(), ['jwt_service_unavailable', 'invalid_token']); // Test Case 5: Case insensitive Bearer check $request_lowercase = $this->create_mock_request('GET', '/kivicare/v1/patients'); $request_lowercase->set_header('Authorization', 'bearer valid.token'); $result = Security_Manager::check_api_permissions($request_lowercase); // Should process the token (case insensitive regex) if (is_wp_error($result)) { $this->assertContains($result->get_error_code(), ['jwt_service_unavailable', 'invalid_token']); } else { $this->assertTrue($result); } } /** * Test 4: SQL injection protection * * Tests input validation and sanitization against SQL injection attacks * through the validate_input method with various malicious payloads. * * @covers Security_Manager::validate_input */ public function test_sql_injection_protection(): void { // Test Case 1: SQL injection in text input $sql_injection_text = "'; DROP TABLE users; --"; $result = Security_Manager::validate_input($sql_injection_text, 'text'); // Should be sanitized (WordPress sanitize_text_field removes dangerous characters) $this->assertStringNotContainsString('DROP TABLE', $result); $this->assertStringNotContainsString(';', $result); $this->assertIsString($result); // Test Case 2: SQL injection in email input $sql_injection_email = "user@example.com'; DROP TABLE users; --"; $result = Security_Manager::validate_input($sql_injection_email, 'email'); // Should return WP_Error for invalid email $this->assertInstanceOf(WP_Error::class, $result); $this->assertEquals('invalid_email', $result->get_error_code()); // Test Case 3: SQL injection in integer input $sql_injection_int = "1 OR 1=1"; $result = Security_Manager::validate_input($sql_injection_int, 'int'); // Should return WP_Error for invalid integer $this->assertInstanceOf(WP_Error::class, $result); $this->assertEquals('invalid_integer', $result->get_error_code()); // Test Case 4: SQL injection in URL input $sql_injection_url = "http://example.com'; DROP TABLE users; --"; $result = Security_Manager::validate_input($sql_injection_url, 'url'); // Should return WP_Error for invalid URL $this->assertInstanceOf(WP_Error::class, $result); $this->assertEquals('invalid_url', $result->get_error_code()); // Test Case 5: Valid inputs should pass through $valid_inputs = [ ['john@example.com', 'email'], ['123', 'int'], ['https://example.com', 'url'], ['Clean text input', 'text'], ['username123', 'username'] ]; foreach ($valid_inputs as [$input, $type]) { $result = Security_Manager::validate_input($input, $type); $this->assertNotInstanceOf(WP_Error::class, $result); } // Test Case 6: Complex SQL injection patterns $complex_sql_injections = [ "1' UNION SELECT * FROM wp_users WHERE '1'='1", "admin'/**/UNION/**/SELECT/**/password/**/FROM/**/wp_users/**/WHERE/**/user_login='admin'--", "1'; INSERT INTO wp_users (user_login, user_pass) VALUES ('hacker', 'password'); --" ]; foreach ($complex_sql_injections as $injection) { $result = Security_Manager::validate_input($injection, 'text'); // Should be sanitized and not contain dangerous SQL keywords $this->assertStringNotContainsString('UNION', strtoupper($result)); $this->assertStringNotContainsString('SELECT', strtoupper($result)); $this->assertStringNotContainsString('INSERT', strtoupper($result)); } // Test Case 7: Boolean input validation $this->assertTrue(Security_Manager::validate_input('true', 'boolean')); $this->assertFalse(Security_Manager::validate_input('false', 'boolean')); $this->assertInstanceOf(WP_Error::class, Security_Manager::validate_input('not-boolean', 'boolean')); // Test Case 8: Float input validation $this->assertEquals(123.45, Security_Manager::validate_input('123.45', 'float')); $this->assertInstanceOf(WP_Error::class, Security_Manager::validate_input('not-float', 'float')); } /** * Test 5: XSS (Cross-Site Scripting) prevention * * Tests output sanitization functionality to prevent XSS attacks * through the sanitize_output method with various malicious payloads. * * @covers Security_Manager::sanitize_output */ public function test_xss_prevention(): void { // Test Case 1: Basic XSS script tag $xss_script = ''; $result = Security_Manager::sanitize_output($xss_script, 'text'); // Should be escaped and not contain script tags $this->assertStringNotContainsString('">'; $result = Security_Manager::sanitize_output($nested_xss, 'html'); // Should remove all dangerous elements $this->assertStringNotContainsString('onerror', $result); $this->assertStringNotContainsString('', 'content' => 'Safe content', 'user' => (object) ['name' => ''] ]; $result = Security_Manager::sanitize_output($xss_array, 'text'); // Should recursively sanitize all elements $this->assertIsArray($result); $this->assertStringNotContainsString(''; $result = Security_Manager::sanitize_output($mixed_content, 'default'); $this->assertStringNotContainsString('