fix(perfexcrm module): align version to 3.0.1, unify entrypoint, and harden routes/views
- Bump DESK_MOLONI version to 3.0.1 across module - Normalize hooks to after_client_* and instantiate PerfexHooks safely - Fix OAuthController view path and API client class name - Add missing admin views for webhook config/logs; adjust view loading - Harden client portal routes and admin routes mapping - Make Dashboard/Logs/Queue tolerant to optional model methods - Align log details query with existing schema; avoid broken joins This makes the module operational in Perfex (admin + client), reduces 404s, and avoids fatal errors due to inconsistent tables/methods.
This commit is contained in:
446
modules/desk_moloni/tests/OAuthIntegrationTest.php
Normal file
446
modules/desk_moloni/tests/OAuthIntegrationTest.php
Normal file
@@ -0,0 +1,446 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user