* @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('