- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
451 lines
15 KiB
PHP
451 lines
15 KiB
PHP
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
<?php
|
|
|
|
defined('BASEPATH') or exit('No direct script access allowed');
|
|
|
|
/**
|
|
* OAuth Integration Tests
|
|
*
|
|
* Comprehensive tests for OAuth 2.0 flow with Moloni API
|
|
*
|
|
* @package DeskMoloni
|
|
* @author Descomplicar®
|
|
* @copyright 2025 Descomplicar
|
|
* @version 3.0.0
|
|
*/
|
|
class OAuthIntegrationTest extends PHPUnit\Framework\TestCase
|
|
{
|
|
private $CI;
|
|
private $oauth;
|
|
private $token_manager;
|
|
private $test_client_id;
|
|
private $test_client_secret;
|
|
private $test_redirect_uri;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
// Get CodeIgniter instance
|
|
$this->CI = &get_instance();
|
|
|
|
// Load required libraries
|
|
$this->CI->load->library('desk_moloni/molonioauth');
|
|
$this->CI->load->library('desk_moloni/tokenmanager');
|
|
|
|
$this->oauth = $this->CI->molonioauth;
|
|
$this->token_manager = $this->CI->tokenmanager;
|
|
|
|
// Test credentials (use environment variables or test config)
|
|
$this->test_client_id = getenv('MOLONI_TEST_CLIENT_ID') ?: 'test_client_id';
|
|
$this->test_client_secret = getenv('MOLONI_TEST_CLIENT_SECRET') ?: 'test_client_secret';
|
|
$this->test_redirect_uri = 'https://test.example.com/oauth/callback';
|
|
|
|
// Clear any existing tokens
|
|
$this->token_manager->clear_tokens();
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
// Clean up after tests
|
|
$this->token_manager->clear_tokens();
|
|
|
|
// Reset OAuth configuration
|
|
update_option('desk_moloni_client_id', '');
|
|
update_option('desk_moloni_client_secret', '');
|
|
}
|
|
|
|
/**
|
|
* Test OAuth configuration
|
|
*/
|
|
public function testOAuthConfiguration()
|
|
{
|
|
// Test initial state (not configured)
|
|
$this->assertFalse($this->oauth->is_configured());
|
|
|
|
// Test configuration
|
|
$result = $this->oauth->configure($this->test_client_id, $this->test_client_secret, [
|
|
'redirect_uri' => $this->test_redirect_uri,
|
|
'use_pkce' => true
|
|
]);
|
|
|
|
$this->assertTrue($result);
|
|
$this->assertTrue($this->oauth->is_configured());
|
|
|
|
// Test configuration persistence
|
|
$status = $this->oauth->get_status();
|
|
$this->assertTrue($status['configured']);
|
|
$this->assertTrue($status['use_pkce']);
|
|
$this->assertEquals($this->test_redirect_uri, $status['redirect_uri']);
|
|
}
|
|
|
|
/**
|
|
* Test OAuth configuration validation
|
|
*/
|
|
public function testOAuthConfigurationValidation()
|
|
{
|
|
// Test empty client ID
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->oauth->configure('', $this->test_client_secret);
|
|
}
|
|
|
|
/**
|
|
* Test OAuth configuration with invalid parameters
|
|
*/
|
|
public function testOAuthConfigurationInvalidParameters()
|
|
{
|
|
// Test empty client secret
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->oauth->configure($this->test_client_id, '');
|
|
}
|
|
|
|
/**
|
|
* Test authorization URL generation
|
|
*/
|
|
public function testAuthorizationUrlGeneration()
|
|
{
|
|
// Configure OAuth first
|
|
$this->oauth->configure($this->test_client_id, $this->test_client_secret, [
|
|
'redirect_uri' => $this->test_redirect_uri
|
|
]);
|
|
|
|
// Generate authorization URL
|
|
$state = 'test_state_' . time();
|
|
$auth_url = $this->oauth->get_authorization_url($state);
|
|
|
|
// Verify URL structure
|
|
$this->assertStringContainsString('https://api.moloni.pt/v1/oauth2/authorize', $auth_url);
|
|
$this->assertStringContainsString('client_id=' . urlencode($this->test_client_id), $auth_url);
|
|
$this->assertStringContainsString('redirect_uri=' . urlencode($this->test_redirect_uri), $auth_url);
|
|
$this->assertStringContainsString('state=' . $state, $auth_url);
|
|
$this->assertStringContainsString('response_type=code', $auth_url);
|
|
|
|
// Test PKCE parameters
|
|
$this->assertStringContainsString('code_challenge=', $auth_url);
|
|
$this->assertStringContainsString('code_challenge_method=S256', $auth_url);
|
|
}
|
|
|
|
/**
|
|
* Test authorization URL generation without configuration
|
|
*/
|
|
public function testAuthorizationUrlWithoutConfiguration()
|
|
{
|
|
$this->expectException(Exception::class);
|
|
$this->expectExceptionMessage('OAuth not configured');
|
|
|
|
$this->oauth->get_authorization_url();
|
|
}
|
|
|
|
/**
|
|
* Test OAuth callback handling with mock data
|
|
*/
|
|
public function testOAuthCallbackHandling()
|
|
{
|
|
// Configure OAuth
|
|
$this->oauth->configure($this->test_client_id, $this->test_client_secret);
|
|
|
|
// Mock successful token response
|
|
$mock_response = [
|
|
'access_token' => 'test_access_token_' . time(),
|
|
'refresh_token' => 'test_refresh_token_' . time(),
|
|
'expires_in' => 3600,
|
|
'token_type' => 'Bearer',
|
|
'scope' => 'read write'
|
|
];
|
|
|
|
// Save mock tokens
|
|
$result = $this->token_manager->save_tokens($mock_response);
|
|
$this->assertTrue($result);
|
|
|
|
// Verify token storage
|
|
$this->assertTrue($this->token_manager->are_tokens_valid());
|
|
$this->assertEquals($mock_response['access_token'], $this->token_manager->get_access_token());
|
|
$this->assertEquals($mock_response['refresh_token'], $this->token_manager->get_refresh_token());
|
|
}
|
|
|
|
/**
|
|
* Test token encryption and decryption
|
|
*/
|
|
public function testTokenEncryption()
|
|
{
|
|
$test_token = 'test_access_token_' . uniqid();
|
|
|
|
// Test token save and retrieval
|
|
$token_data = [
|
|
'access_token' => $test_token,
|
|
'refresh_token' => 'test_refresh_' . uniqid(),
|
|
'expires_in' => 3600
|
|
];
|
|
|
|
$result = $this->token_manager->save_tokens($token_data);
|
|
$this->assertTrue($result);
|
|
|
|
// Verify token retrieval
|
|
$retrieved_token = $this->token_manager->get_access_token();
|
|
$this->assertEquals($test_token, $retrieved_token);
|
|
|
|
// Verify encrypted storage (tokens should not be stored in plain text)
|
|
$stored_encrypted = get_option('desk_moloni_access_token_encrypted');
|
|
$this->assertNotEmpty($stored_encrypted);
|
|
$this->assertNotEquals($test_token, $stored_encrypted);
|
|
}
|
|
|
|
/**
|
|
* Test token expiration logic
|
|
*/
|
|
public function testTokenExpiration()
|
|
{
|
|
// Save token that expires in 1 second
|
|
$token_data = [
|
|
'access_token' => 'test_token',
|
|
'expires_in' => 1
|
|
];
|
|
|
|
$this->token_manager->save_tokens($token_data);
|
|
|
|
// Token should be valid initially
|
|
$this->assertTrue($this->token_manager->are_tokens_valid());
|
|
|
|
// Wait for expiration
|
|
sleep(2);
|
|
|
|
// Token should be expired now
|
|
$this->assertFalse($this->token_manager->are_tokens_valid());
|
|
}
|
|
|
|
/**
|
|
* Test token clearing
|
|
*/
|
|
public function testTokenClearing()
|
|
{
|
|
// Save some tokens
|
|
$token_data = [
|
|
'access_token' => 'test_token',
|
|
'refresh_token' => 'test_refresh',
|
|
'expires_in' => 3600
|
|
];
|
|
|
|
$this->token_manager->save_tokens($token_data);
|
|
$this->assertTrue($this->token_manager->are_tokens_valid());
|
|
|
|
// Clear tokens
|
|
$result = $this->token_manager->clear_tokens();
|
|
$this->assertTrue($result);
|
|
|
|
// Verify tokens are cleared
|
|
$this->assertFalse($this->token_manager->are_tokens_valid());
|
|
$this->assertNull($this->token_manager->get_access_token());
|
|
$this->assertNull($this->token_manager->get_refresh_token());
|
|
}
|
|
|
|
/**
|
|
* Test OAuth status reporting
|
|
*/
|
|
public function testOAuthStatus()
|
|
{
|
|
// Test unconfigured status
|
|
$status = $this->oauth->get_status();
|
|
$this->assertFalse($status['configured']);
|
|
$this->assertFalse($status['connected']);
|
|
|
|
// Configure OAuth
|
|
$this->oauth->configure($this->test_client_id, $this->test_client_secret);
|
|
|
|
$status = $this->oauth->get_status();
|
|
$this->assertTrue($status['configured']);
|
|
$this->assertFalse($status['connected']); // No tokens yet
|
|
|
|
// Add tokens
|
|
$this->token_manager->save_tokens([
|
|
'access_token' => 'test_token',
|
|
'expires_in' => 3600
|
|
]);
|
|
|
|
$status = $this->oauth->get_status();
|
|
$this->assertTrue($status['configured']);
|
|
$this->assertTrue($status['connected']);
|
|
}
|
|
|
|
/**
|
|
* Test OAuth configuration testing
|
|
*/
|
|
public function testOAuthConfigurationTesting()
|
|
{
|
|
// Test without configuration
|
|
$test_result = $this->oauth->test_configuration();
|
|
$this->assertFalse($test_result['is_valid']);
|
|
$this->assertContains('OAuth not configured', $test_result['issues']);
|
|
|
|
// Configure OAuth
|
|
$this->oauth->configure($this->test_client_id, $this->test_client_secret);
|
|
|
|
// Test with configuration
|
|
$test_result = $this->oauth->test_configuration();
|
|
|
|
// Should pass basic configuration tests
|
|
$this->assertIsArray($test_result['issues']);
|
|
$this->assertArrayHasKey('is_valid', $test_result);
|
|
$this->assertArrayHasKey('endpoints', $test_result);
|
|
$this->assertArrayHasKey('encryption', $test_result);
|
|
}
|
|
|
|
/**
|
|
* Test token manager encryption validation
|
|
*/
|
|
public function testTokenManagerEncryptionValidation()
|
|
{
|
|
$validation = $this->token_manager->validate_encryption();
|
|
|
|
$this->assertArrayHasKey('is_valid', $validation);
|
|
$this->assertArrayHasKey('issues', $validation);
|
|
$this->assertArrayHasKey('cipher', $validation);
|
|
|
|
// Should pass if OpenSSL is available
|
|
if (extension_loaded('openssl')) {
|
|
$this->assertTrue($validation['is_valid'], 'Encryption validation failed: ' . implode(', ', $validation['issues']));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test token status information
|
|
*/
|
|
public function testTokenStatus()
|
|
{
|
|
// Test empty status
|
|
$status = $this->token_manager->get_token_status();
|
|
$this->assertFalse($status['has_access_token']);
|
|
$this->assertFalse($status['has_refresh_token']);
|
|
$this->assertFalse($status['is_valid']);
|
|
|
|
// Add tokens
|
|
$token_data = [
|
|
'access_token' => 'test_token',
|
|
'refresh_token' => 'test_refresh',
|
|
'expires_in' => 3600,
|
|
'scope' => 'read write'
|
|
];
|
|
|
|
$this->token_manager->save_tokens($token_data);
|
|
|
|
$status = $this->token_manager->get_token_status();
|
|
$this->assertTrue($status['has_access_token']);
|
|
$this->assertTrue($status['has_refresh_token']);
|
|
$this->assertTrue($status['is_valid']);
|
|
$this->assertEquals('read write', $status['scope']);
|
|
$this->assertGreaterThan(0, $status['expires_in']);
|
|
}
|
|
|
|
/**
|
|
* Test PKCE implementation
|
|
*/
|
|
public function testPKCEImplementation()
|
|
{
|
|
// Configure OAuth with PKCE enabled
|
|
$this->oauth->configure($this->test_client_id, $this->test_client_secret, [
|
|
'use_pkce' => true
|
|
]);
|
|
|
|
// Generate authorization URL
|
|
$auth_url = $this->oauth->get_authorization_url('test_state');
|
|
|
|
// Verify PKCE parameters are included
|
|
$this->assertStringContainsString('code_challenge=', $auth_url);
|
|
$this->assertStringContainsString('code_challenge_method=S256', $auth_url);
|
|
|
|
// Verify code verifier is stored in session (would be used in real implementation)
|
|
$this->assertNotEmpty($this->CI->session->userdata('desk_moloni_code_verifier'));
|
|
}
|
|
|
|
/**
|
|
* Test error handling in OAuth flow
|
|
*/
|
|
public function testOAuthErrorHandling()
|
|
{
|
|
// Configure OAuth
|
|
$this->oauth->configure($this->test_client_id, $this->test_client_secret);
|
|
|
|
// Test callback with error
|
|
$this->expectException(Exception::class);
|
|
$this->expectExceptionMessage('OAuth Error');
|
|
|
|
// Simulate error callback (this would normally come from Moloni)
|
|
$_GET['error'] = 'access_denied';
|
|
$_GET['error_description'] = 'User denied access';
|
|
|
|
$this->oauth->handle_callback('', 'test_state');
|
|
}
|
|
|
|
/**
|
|
* Test rate limiting in OAuth requests
|
|
*/
|
|
public function testOAuthRateLimiting()
|
|
{
|
|
// This test would require mocking HTTP requests
|
|
// For now, we test that the rate limiting structure is in place
|
|
$status = $this->oauth->get_status();
|
|
|
|
$this->assertArrayHasKey('rate_limit', $status);
|
|
$this->assertArrayHasKey('max_requests', $status['rate_limit']);
|
|
$this->assertArrayHasKey('current_count', $status['rate_limit']);
|
|
}
|
|
|
|
/**
|
|
* Integration test with mock HTTP responses
|
|
*/
|
|
public function testIntegrationWithMockResponses()
|
|
{
|
|
// This would require a HTTP mocking library like VCR.php or Guzzle Mock
|
|
// For demonstration, we'll test the structure is correct
|
|
|
|
$this->oauth->configure($this->test_client_id, $this->test_client_secret);
|
|
|
|
// Verify OAuth is ready for integration
|
|
$this->assertTrue($this->oauth->is_configured());
|
|
|
|
// Verify we can generate proper authorization URLs
|
|
$auth_url = $this->oauth->get_authorization_url();
|
|
$this->assertStringStartsWith('https://api.moloni.pt/v1/oauth2/authorize', $auth_url);
|
|
}
|
|
|
|
/**
|
|
* Test OAuth connection status
|
|
*/
|
|
public function testOAuthConnectionStatus()
|
|
{
|
|
// Initially not connected
|
|
$this->assertFalse($this->oauth->is_connected());
|
|
|
|
// Configure OAuth
|
|
$this->oauth->configure($this->test_client_id, $this->test_client_secret);
|
|
$this->assertFalse($this->oauth->is_connected()); // Still no tokens
|
|
|
|
// Add valid tokens
|
|
$this->token_manager->save_tokens([
|
|
'access_token' => 'valid_token',
|
|
'expires_in' => 3600
|
|
]);
|
|
|
|
$this->assertTrue($this->oauth->is_connected());
|
|
}
|
|
|
|
/**
|
|
* Test security features
|
|
*/
|
|
public function testSecurityFeatures()
|
|
{
|
|
// Test CSRF protection with state parameter
|
|
$state1 = 'state1';
|
|
$state2 = 'state2';
|
|
|
|
$url1 = $this->oauth->get_authorization_url($state1);
|
|
$url2 = $this->oauth->get_authorization_url($state2);
|
|
|
|
$this->assertStringContainsString('state=' . $state1, $url1);
|
|
$this->assertStringContainsString('state=' . $state2, $url2);
|
|
|
|
// Test that different states produce different URLs
|
|
$this->assertNotEquals($url1, $url2);
|
|
}
|
|
} |