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:
221
modules/desk_moloni/tests/contract/ConfigTableTest.php
Normal file
221
modules/desk_moloni/tests/contract/ConfigTableTest.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
/**
|
||||
* Contract Test for desk_moloni_config table
|
||||
*
|
||||
* This test MUST FAIL until the Config_model is properly implemented
|
||||
* Following TDD RED-GREEN-REFACTOR cycle
|
||||
*
|
||||
* @package DeskMoloni\Tests\Contract
|
||||
*/
|
||||
|
||||
namespace DeskMoloni\Tests\Contract;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ConfigTableTest extends TestCase
|
||||
{
|
||||
private $CI;
|
||||
private $db;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
// Initialize CodeIgniter instance
|
||||
$this->CI = &get_instance();
|
||||
$this->CI->load->database();
|
||||
$this->db = $this->CI->db;
|
||||
|
||||
// Ensure we're in test environment
|
||||
if (ENVIRONMENT !== 'testing') {
|
||||
$this->markTestSkipped('Contract tests should only run in testing environment');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: desk_moloni_config table must exist with correct structure
|
||||
*/
|
||||
public function config_table_exists_with_required_structure()
|
||||
{
|
||||
// ARRANGE: Test database table existence and structure
|
||||
|
||||
// ACT: Query table structure
|
||||
$table_exists = $this->db->table_exists('desk_moloni_config');
|
||||
|
||||
// ASSERT: Table must exist
|
||||
$this->assertTrue($table_exists, 'desk_moloni_config table must exist');
|
||||
|
||||
// ASSERT: Required columns exist with correct types
|
||||
$fields = $this->db->list_fields('desk_moloni_config');
|
||||
|
||||
$required_fields = ['id', 'setting_key', 'setting_value', 'encrypted', 'created_at', 'updated_at'];
|
||||
foreach ($required_fields as $field) {
|
||||
$this->assertContains($field, $fields, "Required field '{$field}' must exist in desk_moloni_config table");
|
||||
}
|
||||
|
||||
// ASSERT: Check field types and constraints
|
||||
$field_data = $this->db->field_data('desk_moloni_config');
|
||||
$field_info = [];
|
||||
foreach ($field_data as $field) {
|
||||
$field_info[$field->name] = $field;
|
||||
}
|
||||
|
||||
// Verify setting_key is unique
|
||||
$this->assertEquals('varchar', strtolower($field_info['setting_key']->type), 'setting_key must be varchar type');
|
||||
$this->assertEquals(255, $field_info['setting_key']->max_length, 'setting_key must have max_length of 255');
|
||||
|
||||
// Verify encrypted is boolean (tinyint in MySQL)
|
||||
$this->assertEquals('tinyint', strtolower($field_info['encrypted']->type), 'encrypted must be tinyint type');
|
||||
$this->assertEquals(1, $field_info['encrypted']->default_value, 'encrypted must have default value of 0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Config table must enforce unique constraint on setting_key
|
||||
*/
|
||||
public function config_table_enforces_unique_setting_key()
|
||||
{
|
||||
// ARRANGE: Clean table and insert test data
|
||||
$this->db->truncate('desk_moloni_config');
|
||||
|
||||
$test_data = [
|
||||
'setting_key' => 'test_unique_key',
|
||||
'setting_value' => 'test_value',
|
||||
'encrypted' => 0
|
||||
];
|
||||
|
||||
// ACT & ASSERT: First insert should succeed
|
||||
$first_insert = $this->db->insert('desk_moloni_config', $test_data);
|
||||
$this->assertTrue($first_insert, 'First insert with unique key should succeed');
|
||||
|
||||
// ACT & ASSERT: Second insert with same key should fail
|
||||
$this->expectException(\Exception::class);
|
||||
$this->db->insert('desk_moloni_config', $test_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Config table must have proper indexes for performance
|
||||
*/
|
||||
public function config_table_has_required_indexes()
|
||||
{
|
||||
// ACT: Get table indexes
|
||||
$indexes = $this->db->query("SHOW INDEX FROM desk_moloni_config")->result_array();
|
||||
|
||||
// ASSERT: Primary key exists
|
||||
$has_primary = false;
|
||||
$has_setting_key_index = false;
|
||||
|
||||
foreach ($indexes as $index) {
|
||||
if ($index['Key_name'] === 'PRIMARY') {
|
||||
$has_primary = true;
|
||||
}
|
||||
if ($index['Key_name'] === 'idx_setting_key') {
|
||||
$has_setting_key_index = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue($has_primary, 'Table must have PRIMARY KEY');
|
||||
$this->assertTrue($has_setting_key_index, 'Table must have idx_setting_key index for performance');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Config table must support encrypted and non-encrypted values
|
||||
*/
|
||||
public function config_table_supports_encryption_flag()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_config');
|
||||
|
||||
// ACT: Insert encrypted and non-encrypted test data
|
||||
$encrypted_data = [
|
||||
'setting_key' => 'oauth_access_token',
|
||||
'setting_value' => 'encrypted_token_value',
|
||||
'encrypted' => 1
|
||||
];
|
||||
|
||||
$plain_data = [
|
||||
'setting_key' => 'api_base_url',
|
||||
'setting_value' => 'https://api.moloni.pt/v1',
|
||||
'encrypted' => 0
|
||||
];
|
||||
|
||||
$this->db->insert('desk_moloni_config', $encrypted_data);
|
||||
$this->db->insert('desk_moloni_config', $plain_data);
|
||||
|
||||
// ASSERT: Data inserted correctly with proper encryption flags
|
||||
$encrypted_row = $this->db->get_where('desk_moloni_config', ['setting_key' => 'oauth_access_token'])->row();
|
||||
$plain_row = $this->db->get_where('desk_moloni_config', ['setting_key' => 'api_base_url'])->row();
|
||||
|
||||
$this->assertEquals(1, $encrypted_row->encrypted, 'Encrypted flag must be set for sensitive data');
|
||||
$this->assertEquals(0, $plain_row->encrypted, 'Encrypted flag must be false for plain data');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Config table must have automatic timestamps
|
||||
*/
|
||||
public function config_table_has_automatic_timestamps()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_config');
|
||||
|
||||
// ACT: Insert test record
|
||||
$test_data = [
|
||||
'setting_key' => 'timestamp_test',
|
||||
'setting_value' => 'test_value',
|
||||
'encrypted' => 0
|
||||
];
|
||||
|
||||
$this->db->insert('desk_moloni_config', $test_data);
|
||||
|
||||
// ASSERT: Timestamps are automatically set
|
||||
$row = $this->db->get_where('desk_moloni_config', ['setting_key' => 'timestamp_test'])->row();
|
||||
|
||||
$this->assertNotNull($row->created_at, 'created_at must be automatically set');
|
||||
$this->assertNotNull($row->updated_at, 'updated_at must be automatically set');
|
||||
|
||||
// ASSERT: Timestamps are recent (within last 5 seconds)
|
||||
$created_time = strtotime($row->created_at);
|
||||
$current_time = time();
|
||||
$this->assertLessThan(5, abs($current_time - $created_time), 'created_at must be recent');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Config table must support TEXT values for large configurations
|
||||
*/
|
||||
public function config_table_supports_large_text_values()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_config');
|
||||
|
||||
// ACT: Insert large value (simulate large JSON configuration)
|
||||
$large_value = str_repeat('{"large_config":' . str_repeat('"test"', 1000) . '}', 10);
|
||||
|
||||
$test_data = [
|
||||
'setting_key' => 'large_config_test',
|
||||
'setting_value' => $large_value,
|
||||
'encrypted' => 0
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_config', $test_data);
|
||||
|
||||
// ASSERT: Large values can be stored
|
||||
$this->assertTrue($insert_success, 'Table must support large TEXT values');
|
||||
|
||||
// ASSERT: Large value is retrieved correctly
|
||||
$row = $this->db->get_where('desk_moloni_config', ['setting_key' => 'large_config_test'])->row();
|
||||
$this->assertEquals($large_value, $row->setting_value, 'Large values must be stored and retrieved correctly');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Clean up test data
|
||||
if ($this->db) {
|
||||
$this->db->where('setting_key LIKE', 'test_%');
|
||||
$this->db->or_where('setting_key LIKE', '%_test');
|
||||
$this->db->delete('desk_moloni_config');
|
||||
}
|
||||
}
|
||||
}
|
||||
399
modules/desk_moloni/tests/contract/LogTableTest.php
Normal file
399
modules/desk_moloni/tests/contract/LogTableTest.php
Normal file
@@ -0,0 +1,399 @@
|
||||
<?php
|
||||
/**
|
||||
* Contract Test for desk_moloni_sync_log table
|
||||
*
|
||||
* This test MUST FAIL until the Sync_log_model is properly implemented
|
||||
* Following TDD RED-GREEN-REFACTOR cycle
|
||||
*
|
||||
* @package DeskMoloni\Tests\Contract
|
||||
*/
|
||||
|
||||
namespace DeskMoloni\Tests\Contract;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class LogTableTest extends TestCase
|
||||
{
|
||||
private $CI;
|
||||
private $db;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
$this->CI->load->database();
|
||||
$this->db = $this->CI->db;
|
||||
|
||||
if (ENVIRONMENT !== 'testing') {
|
||||
$this->markTestSkipped('Contract tests should only run in testing environment');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: desk_moloni_sync_log table must exist with correct structure
|
||||
*/
|
||||
public function log_table_exists_with_required_structure()
|
||||
{
|
||||
// ACT: Check table existence
|
||||
$table_exists = $this->db->table_exists('desk_moloni_sync_log');
|
||||
|
||||
// ASSERT: Table must exist
|
||||
$this->assertTrue($table_exists, 'desk_moloni_sync_log table must exist');
|
||||
|
||||
// ASSERT: Required columns exist
|
||||
$fields = $this->db->list_fields('desk_moloni_sync_log');
|
||||
$required_fields = [
|
||||
'id', 'operation_type', 'entity_type', 'perfex_id', 'moloni_id',
|
||||
'direction', 'status', 'request_data', 'response_data',
|
||||
'error_message', 'execution_time_ms', 'created_at'
|
||||
];
|
||||
|
||||
foreach ($required_fields as $field) {
|
||||
$this->assertContains($field, $fields, "Required field '{$field}' must exist in desk_moloni_sync_log table");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Log table must enforce operation_type ENUM values
|
||||
*/
|
||||
public function log_table_enforces_operation_type_enum()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_sync_log');
|
||||
|
||||
// ACT & ASSERT: Valid operation types should work
|
||||
$valid_operations = ['create', 'update', 'delete', 'status_change'];
|
||||
|
||||
foreach ($valid_operations as $operation) {
|
||||
$test_data = [
|
||||
'operation_type' => $operation,
|
||||
'entity_type' => 'client',
|
||||
'perfex_id' => 1,
|
||||
'moloni_id' => 1,
|
||||
'direction' => 'perfex_to_moloni',
|
||||
'status' => 'success'
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_sync_log', $test_data);
|
||||
$this->assertTrue($insert_success, "Operation type '{$operation}' must be valid");
|
||||
|
||||
// Clean up
|
||||
$this->db->delete('desk_moloni_sync_log', ['operation_type' => $operation]);
|
||||
}
|
||||
|
||||
// ACT & ASSERT: Invalid operation type should fail
|
||||
$invalid_data = [
|
||||
'operation_type' => 'invalid_operation',
|
||||
'entity_type' => 'client',
|
||||
'perfex_id' => 1,
|
||||
'moloni_id' => 1,
|
||||
'direction' => 'perfex_to_moloni',
|
||||
'status' => 'success'
|
||||
];
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->db->insert('desk_moloni_sync_log', $invalid_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Log table must enforce direction ENUM values
|
||||
*/
|
||||
public function log_table_enforces_direction_enum()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_sync_log');
|
||||
|
||||
// ACT & ASSERT: Valid directions should work
|
||||
$valid_directions = ['perfex_to_moloni', 'moloni_to_perfex'];
|
||||
|
||||
foreach ($valid_directions as $direction) {
|
||||
$test_data = [
|
||||
'operation_type' => 'create',
|
||||
'entity_type' => 'invoice',
|
||||
'perfex_id' => 10,
|
||||
'moloni_id' => 20,
|
||||
'direction' => $direction,
|
||||
'status' => 'success'
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_sync_log', $test_data);
|
||||
$this->assertTrue($insert_success, "Direction '{$direction}' must be valid");
|
||||
|
||||
// Clean up
|
||||
$this->db->delete('desk_moloni_sync_log', ['direction' => $direction]);
|
||||
}
|
||||
|
||||
// ACT & ASSERT: Invalid direction should fail
|
||||
$invalid_data = [
|
||||
'operation_type' => 'create',
|
||||
'entity_type' => 'invoice',
|
||||
'perfex_id' => 10,
|
||||
'moloni_id' => 20,
|
||||
'direction' => 'invalid_direction',
|
||||
'status' => 'success'
|
||||
];
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->db->insert('desk_moloni_sync_log', $invalid_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Log table must enforce status ENUM values
|
||||
*/
|
||||
public function log_table_enforces_status_enum()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_sync_log');
|
||||
|
||||
// ACT & ASSERT: Valid status values should work
|
||||
$valid_statuses = ['success', 'error', 'warning'];
|
||||
|
||||
foreach ($valid_statuses as $status) {
|
||||
$test_data = [
|
||||
'operation_type' => 'update',
|
||||
'entity_type' => 'product',
|
||||
'perfex_id' => 30,
|
||||
'moloni_id' => 40,
|
||||
'direction' => 'moloni_to_perfex',
|
||||
'status' => $status
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_sync_log', $test_data);
|
||||
$this->assertTrue($insert_success, "Status '{$status}' must be valid");
|
||||
|
||||
// Clean up
|
||||
$this->db->delete('desk_moloni_sync_log', ['status' => $status]);
|
||||
}
|
||||
|
||||
// ACT & ASSERT: Invalid status should fail
|
||||
$invalid_data = [
|
||||
'operation_type' => 'update',
|
||||
'entity_type' => 'product',
|
||||
'perfex_id' => 30,
|
||||
'moloni_id' => 40,
|
||||
'direction' => 'moloni_to_perfex',
|
||||
'status' => 'invalid_status'
|
||||
];
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->db->insert('desk_moloni_sync_log', $invalid_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Log table must support JSON storage for request and response data
|
||||
*/
|
||||
public function log_table_supports_json_request_response_data()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_sync_log');
|
||||
|
||||
// ACT: Insert log entry with JSON request/response data
|
||||
$request_data = [
|
||||
'method' => 'POST',
|
||||
'endpoint' => '/customers',
|
||||
'headers' => ['Authorization' => 'Bearer token123'],
|
||||
'body' => [
|
||||
'name' => 'Test Company',
|
||||
'vat' => '123456789',
|
||||
'email' => 'test@company.com'
|
||||
]
|
||||
];
|
||||
|
||||
$response_data = [
|
||||
'status_code' => 201,
|
||||
'headers' => ['Content-Type' => 'application/json'],
|
||||
'body' => [
|
||||
'customer_id' => 456,
|
||||
'message' => 'Customer created successfully'
|
||||
]
|
||||
];
|
||||
|
||||
$log_data = [
|
||||
'operation_type' => 'create',
|
||||
'entity_type' => 'client',
|
||||
'perfex_id' => 100,
|
||||
'moloni_id' => 456,
|
||||
'direction' => 'perfex_to_moloni',
|
||||
'status' => 'success',
|
||||
'request_data' => json_encode($request_data),
|
||||
'response_data' => json_encode($response_data),
|
||||
'execution_time_ms' => 1500
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_sync_log', $log_data);
|
||||
$this->assertTrue($insert_success, 'Log entry with JSON data must be inserted successfully');
|
||||
|
||||
// ASSERT: JSON data is stored and retrieved correctly
|
||||
$row = $this->db->get_where('desk_moloni_sync_log', ['perfex_id' => 100, 'moloni_id' => 456])->row();
|
||||
|
||||
$retrieved_request = json_decode($row->request_data, true);
|
||||
$retrieved_response = json_decode($row->response_data, true);
|
||||
|
||||
$this->assertEquals($request_data, $retrieved_request, 'Request data JSON must be stored and retrieved correctly');
|
||||
$this->assertEquals($response_data, $retrieved_response, 'Response data JSON must be stored and retrieved correctly');
|
||||
$this->assertEquals('Test Company', $retrieved_request['body']['name'], 'Nested request data must be accessible');
|
||||
$this->assertEquals(456, $retrieved_response['body']['customer_id'], 'Nested response data must be accessible');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Log table must support performance monitoring with execution time
|
||||
*/
|
||||
public function log_table_supports_performance_monitoring()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_sync_log');
|
||||
|
||||
// ACT: Insert log entries with different execution times
|
||||
$performance_logs = [
|
||||
['execution_time_ms' => 50, 'entity_id' => 501], // Fast operation
|
||||
['execution_time_ms' => 2500, 'entity_id' => 502], // Slow operation
|
||||
['execution_time_ms' => 15000, 'entity_id' => 503], // Very slow operation
|
||||
];
|
||||
|
||||
foreach ($performance_logs as $log) {
|
||||
$log_data = [
|
||||
'operation_type' => 'create',
|
||||
'entity_type' => 'invoice',
|
||||
'perfex_id' => $log['entity_id'],
|
||||
'moloni_id' => $log['entity_id'] + 1000,
|
||||
'direction' => 'perfex_to_moloni',
|
||||
'status' => 'success',
|
||||
'execution_time_ms' => $log['execution_time_ms']
|
||||
];
|
||||
|
||||
$this->db->insert('desk_moloni_sync_log', $log_data);
|
||||
}
|
||||
|
||||
// ASSERT: Performance data can be queried and analyzed
|
||||
$this->db->select('AVG(execution_time_ms) as avg_time, MAX(execution_time_ms) as max_time, MIN(execution_time_ms) as min_time');
|
||||
$this->db->where('entity_type', 'invoice');
|
||||
$performance_stats = $this->db->get('desk_moloni_sync_log')->row();
|
||||
|
||||
$this->assertEquals(5850, $performance_stats->avg_time, 'Average execution time must be calculable');
|
||||
$this->assertEquals(15000, $performance_stats->max_time, 'Maximum execution time must be retrievable');
|
||||
$this->assertEquals(50, $performance_stats->min_time, 'Minimum execution time must be retrievable');
|
||||
|
||||
// ASSERT: Slow operations can be identified
|
||||
$slow_operations = $this->db->get_where('desk_moloni_sync_log', 'execution_time_ms > 10000')->result();
|
||||
$this->assertCount(1, $slow_operations, 'Slow operations must be identifiable for optimization');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Log table must support NULL perfex_id or moloni_id for failed operations
|
||||
*/
|
||||
public function log_table_supports_null_entity_ids()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_sync_log');
|
||||
|
||||
// ACT: Insert log entry with NULL perfex_id (creation failed before getting Perfex ID)
|
||||
$failed_creation = [
|
||||
'operation_type' => 'create',
|
||||
'entity_type' => 'client',
|
||||
'perfex_id' => null,
|
||||
'moloni_id' => 789,
|
||||
'direction' => 'moloni_to_perfex',
|
||||
'status' => 'error',
|
||||
'error_message' => 'Perfex client creation failed due to validation error'
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_sync_log', $failed_creation);
|
||||
$this->assertTrue($insert_success, 'Log entry with NULL perfex_id must be allowed');
|
||||
|
||||
// ACT: Insert log entry with NULL moloni_id (Moloni creation failed)
|
||||
$failed_moloni_creation = [
|
||||
'operation_type' => 'create',
|
||||
'entity_type' => 'product',
|
||||
'perfex_id' => 123,
|
||||
'moloni_id' => null,
|
||||
'direction' => 'perfex_to_moloni',
|
||||
'status' => 'error',
|
||||
'error_message' => 'Moloni product creation failed due to API error'
|
||||
];
|
||||
|
||||
$insert_success2 = $this->db->insert('desk_moloni_sync_log', $failed_moloni_creation);
|
||||
$this->assertTrue($insert_success2, 'Log entry with NULL moloni_id must be allowed');
|
||||
|
||||
// ASSERT: NULL values are handled correctly
|
||||
$null_perfex = $this->db->get_where('desk_moloni_sync_log', ['moloni_id' => 789])->row();
|
||||
$null_moloni = $this->db->get_where('desk_moloni_sync_log', ['perfex_id' => 123])->row();
|
||||
|
||||
$this->assertNull($null_perfex->perfex_id, 'perfex_id must be NULL when creation fails');
|
||||
$this->assertNull($null_moloni->moloni_id, 'moloni_id must be NULL when Moloni creation fails');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Log table must have required indexes for analytics and performance
|
||||
*/
|
||||
public function log_table_has_required_indexes()
|
||||
{
|
||||
// ACT: Get table indexes
|
||||
$indexes = $this->db->query("SHOW INDEX FROM desk_moloni_sync_log")->result_array();
|
||||
|
||||
// ASSERT: Required indexes exist for analytics and performance
|
||||
$required_indexes = [
|
||||
'PRIMARY',
|
||||
'idx_entity_status',
|
||||
'idx_perfex_entity',
|
||||
'idx_moloni_entity',
|
||||
'idx_created_at',
|
||||
'idx_status_direction',
|
||||
'idx_log_analytics'
|
||||
];
|
||||
|
||||
$index_names = array_column($indexes, 'Key_name');
|
||||
|
||||
foreach ($required_indexes as $required_index) {
|
||||
$this->assertContains($required_index, $index_names, "Required index '{$required_index}' must exist for log analytics");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Log table must support automatic created_at timestamp
|
||||
*/
|
||||
public function log_table_has_automatic_created_at()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_sync_log');
|
||||
|
||||
// ACT: Insert log entry without specifying created_at
|
||||
$log_data = [
|
||||
'operation_type' => 'update',
|
||||
'entity_type' => 'estimate',
|
||||
'perfex_id' => 999,
|
||||
'moloni_id' => 888,
|
||||
'direction' => 'bidirectional',
|
||||
'status' => 'success',
|
||||
'execution_time_ms' => 750
|
||||
];
|
||||
|
||||
$this->db->insert('desk_moloni_sync_log', $log_data);
|
||||
|
||||
// ASSERT: created_at is automatically set and is recent
|
||||
$row = $this->db->get_where('desk_moloni_sync_log', ['perfex_id' => 999])->row();
|
||||
|
||||
$this->assertNotNull($row->created_at, 'created_at must be automatically set');
|
||||
|
||||
$created_time = strtotime($row->created_at);
|
||||
$current_time = time();
|
||||
$this->assertLessThan(5, abs($current_time - $created_time), 'created_at must be recent timestamp');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Clean up test data
|
||||
if ($this->db) {
|
||||
$this->db->where('perfex_id IS NOT NULL OR moloni_id IS NOT NULL');
|
||||
$this->db->where('(perfex_id <= 1000 OR moloni_id <= 1000)');
|
||||
$this->db->delete('desk_moloni_sync_log');
|
||||
}
|
||||
}
|
||||
}
|
||||
283
modules/desk_moloni/tests/contract/MappingTableTest.php
Normal file
283
modules/desk_moloni/tests/contract/MappingTableTest.php
Normal file
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
/**
|
||||
* Contract Test for desk_moloni_mapping table
|
||||
*
|
||||
* This test MUST FAIL until the Mapping_model is properly implemented
|
||||
* Following TDD RED-GREEN-REFACTOR cycle
|
||||
*
|
||||
* @package DeskMoloni\Tests\Contract
|
||||
*/
|
||||
|
||||
namespace DeskMoloni\Tests\Contract;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MappingTableTest extends TestCase
|
||||
{
|
||||
private $CI;
|
||||
private $db;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
$this->CI->load->database();
|
||||
$this->db = $this->CI->db;
|
||||
|
||||
if (ENVIRONMENT !== 'testing') {
|
||||
$this->markTestSkipped('Contract tests should only run in testing environment');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: desk_moloni_mapping table must exist with correct structure
|
||||
*/
|
||||
public function mapping_table_exists_with_required_structure()
|
||||
{
|
||||
// ACT: Check table existence
|
||||
$table_exists = $this->db->table_exists('desk_moloni_mapping');
|
||||
|
||||
// ASSERT: Table must exist
|
||||
$this->assertTrue($table_exists, 'desk_moloni_mapping table must exist');
|
||||
|
||||
// ASSERT: Required columns exist
|
||||
$fields = $this->db->list_fields('desk_moloni_mapping');
|
||||
$required_fields = ['id', 'entity_type', 'perfex_id', 'moloni_id', 'sync_direction', 'last_sync_at', 'created_at', 'updated_at'];
|
||||
|
||||
foreach ($required_fields as $field) {
|
||||
$this->assertContains($field, $fields, "Required field '{$field}' must exist in desk_moloni_mapping table");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Mapping table must enforce entity_type ENUM values
|
||||
*/
|
||||
public function mapping_table_enforces_entity_type_enum()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_mapping');
|
||||
|
||||
// ACT & ASSERT: Valid entity types should work
|
||||
$valid_types = ['client', 'product', 'invoice', 'estimate', 'credit_note'];
|
||||
|
||||
foreach ($valid_types as $type) {
|
||||
$test_data = [
|
||||
'entity_type' => $type,
|
||||
'perfex_id' => 1,
|
||||
'moloni_id' => 1,
|
||||
'sync_direction' => 'bidirectional'
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_mapping', $test_data);
|
||||
$this->assertTrue($insert_success, "Entity type '{$type}' must be valid");
|
||||
|
||||
// Clean up for next iteration
|
||||
$this->db->delete('desk_moloni_mapping', ['entity_type' => $type]);
|
||||
}
|
||||
|
||||
// ACT & ASSERT: Invalid entity type should fail
|
||||
$invalid_data = [
|
||||
'entity_type' => 'invalid_type',
|
||||
'perfex_id' => 1,
|
||||
'moloni_id' => 1,
|
||||
'sync_direction' => 'bidirectional'
|
||||
];
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->db->insert('desk_moloni_mapping', $invalid_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Mapping table must enforce sync_direction ENUM values
|
||||
*/
|
||||
public function mapping_table_enforces_sync_direction_enum()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_mapping');
|
||||
|
||||
// ACT & ASSERT: Valid sync directions should work
|
||||
$valid_directions = ['perfex_to_moloni', 'moloni_to_perfex', 'bidirectional'];
|
||||
|
||||
foreach ($valid_directions as $direction) {
|
||||
$test_data = [
|
||||
'entity_type' => 'client',
|
||||
'perfex_id' => 1,
|
||||
'moloni_id' => 1,
|
||||
'sync_direction' => $direction
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_mapping', $test_data);
|
||||
$this->assertTrue($insert_success, "Sync direction '{$direction}' must be valid");
|
||||
|
||||
// Clean up for next iteration
|
||||
$this->db->delete('desk_moloni_mapping', ['sync_direction' => $direction]);
|
||||
}
|
||||
|
||||
// ACT & ASSERT: Invalid sync direction should fail
|
||||
$invalid_data = [
|
||||
'entity_type' => 'client',
|
||||
'perfex_id' => 1,
|
||||
'moloni_id' => 1,
|
||||
'sync_direction' => 'invalid_direction'
|
||||
];
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->db->insert('desk_moloni_mapping', $invalid_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Mapping table must enforce unique constraints
|
||||
*/
|
||||
public function mapping_table_enforces_unique_constraints()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_mapping');
|
||||
|
||||
$test_data = [
|
||||
'entity_type' => 'client',
|
||||
'perfex_id' => 123,
|
||||
'moloni_id' => 456,
|
||||
'sync_direction' => 'bidirectional'
|
||||
];
|
||||
|
||||
// ACT & ASSERT: First insert should succeed
|
||||
$first_insert = $this->db->insert('desk_moloni_mapping', $test_data);
|
||||
$this->assertTrue($first_insert, 'First insert with unique mapping should succeed');
|
||||
|
||||
// ACT & ASSERT: Duplicate perfex_id for same entity_type should fail
|
||||
$duplicate_perfex = [
|
||||
'entity_type' => 'client',
|
||||
'perfex_id' => 123,
|
||||
'moloni_id' => 789,
|
||||
'sync_direction' => 'bidirectional'
|
||||
];
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->db->insert('desk_moloni_mapping', $duplicate_perfex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Mapping table must have required indexes for performance
|
||||
*/
|
||||
public function mapping_table_has_required_indexes()
|
||||
{
|
||||
// ACT: Get table indexes
|
||||
$indexes = $this->db->query("SHOW INDEX FROM desk_moloni_mapping")->result_array();
|
||||
|
||||
// ASSERT: Required indexes exist
|
||||
$required_indexes = [
|
||||
'PRIMARY',
|
||||
'unique_perfex_mapping',
|
||||
'unique_moloni_mapping',
|
||||
'idx_entity_perfex',
|
||||
'idx_entity_moloni',
|
||||
'idx_last_sync'
|
||||
];
|
||||
|
||||
$index_names = array_column($indexes, 'Key_name');
|
||||
|
||||
foreach ($required_indexes as $required_index) {
|
||||
$this->assertContains($required_index, $index_names, "Required index '{$required_index}' must exist");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Mapping table must support bidirectional relationships
|
||||
*/
|
||||
public function mapping_table_supports_bidirectional_relationships()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_mapping');
|
||||
|
||||
// ACT: Insert bidirectional mapping
|
||||
$bidirectional_data = [
|
||||
'entity_type' => 'invoice',
|
||||
'perfex_id' => 100,
|
||||
'moloni_id' => 200,
|
||||
'sync_direction' => 'bidirectional',
|
||||
'last_sync_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_mapping', $bidirectional_data);
|
||||
$this->assertTrue($insert_success, 'Bidirectional mapping must be supported');
|
||||
|
||||
// ASSERT: Data retrieved correctly
|
||||
$row = $this->db->get_where('desk_moloni_mapping', ['perfex_id' => 100, 'entity_type' => 'invoice'])->row();
|
||||
|
||||
$this->assertEquals('bidirectional', $row->sync_direction, 'Bidirectional sync direction must be stored');
|
||||
$this->assertEquals(100, $row->perfex_id, 'Perfex ID must be stored correctly');
|
||||
$this->assertEquals(200, $row->moloni_id, 'Moloni ID must be stored correctly');
|
||||
$this->assertNotNull($row->last_sync_at, 'last_sync_at must support timestamp values');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Mapping table must allow NULL last_sync_at for new mappings
|
||||
*/
|
||||
public function mapping_table_allows_null_last_sync_at()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_mapping');
|
||||
|
||||
// ACT: Insert mapping without last_sync_at
|
||||
$new_mapping_data = [
|
||||
'entity_type' => 'product',
|
||||
'perfex_id' => 50,
|
||||
'moloni_id' => 75,
|
||||
'sync_direction' => 'perfex_to_moloni'
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_mapping', $new_mapping_data);
|
||||
$this->assertTrue($insert_success, 'New mapping without last_sync_at must be allowed');
|
||||
|
||||
// ASSERT: last_sync_at is NULL for new mappings
|
||||
$row = $this->db->get_where('desk_moloni_mapping', ['perfex_id' => 50, 'entity_type' => 'product'])->row();
|
||||
$this->assertNull($row->last_sync_at, 'last_sync_at must be NULL for new mappings');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Mapping table must have automatic created_at and updated_at timestamps
|
||||
*/
|
||||
public function mapping_table_has_automatic_timestamps()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_mapping');
|
||||
|
||||
// ACT: Insert mapping
|
||||
$test_data = [
|
||||
'entity_type' => 'estimate',
|
||||
'perfex_id' => 25,
|
||||
'moloni_id' => 35,
|
||||
'sync_direction' => 'moloni_to_perfex'
|
||||
];
|
||||
|
||||
$this->db->insert('desk_moloni_mapping', $test_data);
|
||||
|
||||
// ASSERT: Timestamps are automatically set
|
||||
$row = $this->db->get_where('desk_moloni_mapping', ['perfex_id' => 25, 'entity_type' => 'estimate'])->row();
|
||||
|
||||
$this->assertNotNull($row->created_at, 'created_at must be automatically set');
|
||||
$this->assertNotNull($row->updated_at, 'updated_at must be automatically set');
|
||||
|
||||
// ASSERT: Timestamps are recent
|
||||
$created_time = strtotime($row->created_at);
|
||||
$current_time = time();
|
||||
$this->assertLessThan(5, abs($current_time - $created_time), 'created_at must be recent');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Clean up test data
|
||||
if ($this->db) {
|
||||
$this->db->where('perfex_id >=', 1);
|
||||
$this->db->where('perfex_id <=', 200);
|
||||
$this->db->delete('desk_moloni_mapping');
|
||||
}
|
||||
}
|
||||
}
|
||||
464
modules/desk_moloni/tests/contract/MoloniApiContractTest.php
Normal file
464
modules/desk_moloni/tests/contract/MoloniApiContractTest.php
Normal file
@@ -0,0 +1,464 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DeskMoloni\Tests\Contract;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
|
||||
/**
|
||||
* Contract Test: Moloni API Endpoint Validation
|
||||
*
|
||||
* This test MUST FAIL initially as part of TDD methodology.
|
||||
* Tests validate API contracts with real Moloni sandbox environment.
|
||||
*
|
||||
* @group contract
|
||||
* @group moloni-api
|
||||
*/
|
||||
class MoloniApiContractTest extends TestCase
|
||||
{
|
||||
private Client $httpClient;
|
||||
private array $config;
|
||||
private ?string $accessToken = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
global $testConfig;
|
||||
$this->config = $testConfig['moloni'];
|
||||
|
||||
$this->httpClient = new Client([
|
||||
'base_uri' => $this->config['sandbox'] ? MOLONI_SANDBOX_URL : MOLONI_PRODUCTION_URL,
|
||||
'timeout' => 30,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
'User-Agent' => 'Desk-Moloni/3.0.0 PHPUnit-Test'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test OAuth 2.0 token endpoint contract
|
||||
* This test will initially fail until OAuth implementation exists
|
||||
*/
|
||||
public function testOAuthTokenEndpointContract(): void
|
||||
{
|
||||
$response = $this->httpClient->post('v1/grant', [
|
||||
'json' => [
|
||||
'grant_type' => 'client_credentials',
|
||||
'client_id' => $this->config['client_id'],
|
||||
'client_secret' => $this->config['client_secret'],
|
||||
'scope' => ''
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
// Validate response structure
|
||||
$this->assertArrayHasKey('access_token', $data);
|
||||
$this->assertArrayHasKey('token_type', $data);
|
||||
$this->assertArrayHasKey('expires_in', $data);
|
||||
$this->assertEquals('Bearer', $data['token_type']);
|
||||
$this->assertIsString($data['access_token']);
|
||||
$this->assertIsInt($data['expires_in']);
|
||||
$this->assertGreaterThan(0, $data['expires_in']);
|
||||
|
||||
// Store token for subsequent tests
|
||||
$this->accessToken = $data['access_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test company list endpoint contract
|
||||
* @depends testOAuthTokenEndpointContract
|
||||
*/
|
||||
public function testCompanyListEndpointContract(): void
|
||||
{
|
||||
if (!$this->accessToken) {
|
||||
$this->markTestSkipped('Access token not available');
|
||||
}
|
||||
|
||||
$response = $this->httpClient->post('v1/companies/getAll', [
|
||||
'json' => [
|
||||
'access_token' => $this->accessToken
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
// Validate Moloni response structure
|
||||
$this->assertArrayHasKey('valid', $data);
|
||||
$this->assertArrayHasKey('data', $data);
|
||||
$this->assertEquals(1, $data['valid']);
|
||||
$this->assertIsArray($data['data']);
|
||||
|
||||
if (!empty($data['data'])) {
|
||||
$company = $data['data'][0];
|
||||
$this->assertArrayHasKey('company_id', $company);
|
||||
$this->assertArrayHasKey('name', $company);
|
||||
$this->assertIsInt($company['company_id']);
|
||||
$this->assertIsString($company['name']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test customer creation endpoint contract
|
||||
* @depends testOAuthTokenEndpointContract
|
||||
*/
|
||||
public function testCustomerCreateEndpointContract(): void
|
||||
{
|
||||
if (!$this->accessToken) {
|
||||
$this->markTestSkipped('Access token not available');
|
||||
}
|
||||
|
||||
$testCustomer = [
|
||||
'access_token' => $this->accessToken,
|
||||
'company_id' => 1, // Test company ID
|
||||
'vat' => '999999990', // Test VAT number
|
||||
'number' => 'TEST-' . time(),
|
||||
'name' => 'Test Customer Contract',
|
||||
'email' => 'test@contract-test.com',
|
||||
'phone' => '+351910000000',
|
||||
'address' => 'Test Address',
|
||||
'zip_code' => '1000-001',
|
||||
'city' => 'Lisboa',
|
||||
'country_id' => 1 // Portugal
|
||||
];
|
||||
|
||||
$response = $this->httpClient->post('v1/customers/insert', [
|
||||
'json' => $testCustomer
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
// Validate response structure
|
||||
$this->assertArrayHasKey('valid', $data);
|
||||
|
||||
if ($data['valid'] === 1) {
|
||||
$this->assertArrayHasKey('data', $data);
|
||||
$this->assertArrayHasKey('customer_id', $data['data']);
|
||||
$this->assertIsInt($data['data']['customer_id']);
|
||||
$this->assertGreaterThan(0, $data['data']['customer_id']);
|
||||
} else {
|
||||
// Validate error structure
|
||||
$this->assertArrayHasKey('errors', $data);
|
||||
$this->assertIsArray($data['errors']);
|
||||
$this->assertNotEmpty($data['errors']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test customer update endpoint contract
|
||||
* @depends testOAuthTokenEndpointContract
|
||||
*/
|
||||
public function testCustomerUpdateEndpointContract(): void
|
||||
{
|
||||
if (!$this->accessToken) {
|
||||
$this->markTestSkipped('Access token not available');
|
||||
}
|
||||
|
||||
// First create a customer to update
|
||||
$createResponse = $this->httpClient->post('v1/customers/insert', [
|
||||
'json' => [
|
||||
'access_token' => $this->accessToken,
|
||||
'company_id' => 1,
|
||||
'vat' => '999999991',
|
||||
'number' => 'TEST-UPDATE-' . time(),
|
||||
'name' => 'Test Customer Update',
|
||||
'email' => 'test-update@contract-test.com'
|
||||
]
|
||||
]);
|
||||
|
||||
$createData = json_decode($createResponse->getBody()->getContents(), true);
|
||||
|
||||
if ($createData['valid'] !== 1) {
|
||||
$this->markTestSkipped('Could not create test customer for update test');
|
||||
}
|
||||
|
||||
$customerId = $createData['data']['customer_id'];
|
||||
|
||||
// Now test update
|
||||
$updateResponse = $this->httpClient->post('v1/customers/update', [
|
||||
'json' => [
|
||||
'access_token' => $this->accessToken,
|
||||
'company_id' => 1,
|
||||
'customer_id' => $customerId,
|
||||
'name' => 'Updated Test Customer',
|
||||
'email' => 'updated@contract-test.com'
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $updateResponse->getStatusCode());
|
||||
|
||||
$updateData = json_decode($updateResponse->getBody()->getContents(), true);
|
||||
|
||||
// Validate response structure
|
||||
$this->assertArrayHasKey('valid', $updateData);
|
||||
$this->assertArrayHasKey('data', $updateData);
|
||||
|
||||
if ($updateData['valid'] === 1) {
|
||||
$this->assertEquals($customerId, $updateData['data']['customer_id']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test product creation endpoint contract
|
||||
* @depends testOAuthTokenEndpointContract
|
||||
*/
|
||||
public function testProductCreateEndpointContract(): void
|
||||
{
|
||||
if (!$this->accessToken) {
|
||||
$this->markTestSkipped('Access token not available');
|
||||
}
|
||||
|
||||
$testProduct = [
|
||||
'access_token' => $this->accessToken,
|
||||
'company_id' => 1,
|
||||
'category_id' => 1,
|
||||
'type' => 1, // Product type
|
||||
'name' => 'Test Product Contract',
|
||||
'summary' => 'Test product for contract validation',
|
||||
'reference' => 'TEST-PROD-' . time(),
|
||||
'price' => 100.00,
|
||||
'unit_id' => 1, // Units
|
||||
'has_stock' => 1,
|
||||
'stock' => 10,
|
||||
'pos_favorite' => 0
|
||||
];
|
||||
|
||||
$response = $this->httpClient->post('v1/products/insert', [
|
||||
'json' => $testProduct
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
// Validate response structure
|
||||
$this->assertArrayHasKey('valid', $data);
|
||||
|
||||
if ($data['valid'] === 1) {
|
||||
$this->assertArrayHasKey('data', $data);
|
||||
$this->assertArrayHasKey('product_id', $data['data']);
|
||||
$this->assertIsInt($data['data']['product_id']);
|
||||
$this->assertGreaterThan(0, $data['data']['product_id']);
|
||||
} else {
|
||||
$this->assertArrayHasKey('errors', $data);
|
||||
$this->assertIsArray($data['errors']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invoice creation endpoint contract
|
||||
* @depends testOAuthTokenEndpointContract
|
||||
*/
|
||||
public function testInvoiceCreateEndpointContract(): void
|
||||
{
|
||||
if (!$this->accessToken) {
|
||||
$this->markTestSkipped('Access token not available');
|
||||
}
|
||||
|
||||
$testInvoice = [
|
||||
'access_token' => $this->accessToken,
|
||||
'company_id' => 1,
|
||||
'document_set_id' => 1,
|
||||
'customer_id' => 1, // Use existing customer
|
||||
'date' => date('Y-m-d'),
|
||||
'products' => [
|
||||
[
|
||||
'product_id' => 1, // Use existing product
|
||||
'name' => 'Test Product Line',
|
||||
'qty' => 1,
|
||||
'price' => 100.00,
|
||||
'discount' => 0,
|
||||
'order' => 0,
|
||||
'exemption_reason' => 'M99',
|
||||
'taxes' => [
|
||||
[
|
||||
'tax_id' => 1,
|
||||
'value' => 23,
|
||||
'order' => 0,
|
||||
'cumulative' => 0
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'payment_method_id' => 1,
|
||||
'notes' => 'Test invoice for contract validation'
|
||||
];
|
||||
|
||||
$response = $this->httpClient->post('v1/invoices/insert', [
|
||||
'json' => $testInvoice
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
// Validate response structure
|
||||
$this->assertArrayHasKey('valid', $data);
|
||||
|
||||
if ($data['valid'] === 1) {
|
||||
$this->assertArrayHasKey('data', $data);
|
||||
$this->assertArrayHasKey('document_id', $data['data']);
|
||||
$this->assertIsInt($data['data']['document_id']);
|
||||
$this->assertGreaterThan(0, $data['data']['document_id']);
|
||||
} else {
|
||||
$this->assertArrayHasKey('errors', $data);
|
||||
$this->assertIsArray($data['errors']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test rate limiting endpoint behavior
|
||||
* @depends testOAuthTokenEndpointContract
|
||||
*/
|
||||
public function testApiRateLimitingBehavior(): void
|
||||
{
|
||||
if (!$this->accessToken) {
|
||||
$this->markTestSkipped('Access token not available');
|
||||
}
|
||||
|
||||
$requestCount = 0;
|
||||
$rateLimitHit = false;
|
||||
|
||||
// Make rapid requests to test rate limiting
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
try {
|
||||
$response = $this->httpClient->post('v1/companies/getAll', [
|
||||
'json' => [
|
||||
'access_token' => $this->accessToken
|
||||
]
|
||||
]);
|
||||
|
||||
$requestCount++;
|
||||
|
||||
// Check for rate limit headers if present
|
||||
if ($response->hasHeader('X-RateLimit-Remaining')) {
|
||||
$remaining = (int)$response->getHeaderLine('X-RateLimit-Remaining');
|
||||
if ($remaining <= 0) {
|
||||
$rateLimitHit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay to avoid overwhelming the API
|
||||
usleep(100000); // 100ms
|
||||
|
||||
} catch (GuzzleException $e) {
|
||||
if (strpos($e->getMessage(), '429') !== false) {
|
||||
$rateLimitHit = true;
|
||||
break;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate rate limiting behavior
|
||||
$this->assertGreaterThan(0, $requestCount, 'Should be able to make some requests');
|
||||
|
||||
// Note: Rate limiting test is informational - Moloni's exact rate limits may vary
|
||||
// The test documents the API's rate limiting behavior for our implementation
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error handling contract
|
||||
* @depends testOAuthTokenEndpointContract
|
||||
*/
|
||||
public function testErrorHandlingContract(): void
|
||||
{
|
||||
// Test with invalid access token
|
||||
$response = $this->httpClient->post('v1/companies/getAll', [
|
||||
'json' => [
|
||||
'access_token' => 'invalid_token'
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode()); // Moloni returns 200 even for errors
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
// Validate error response structure
|
||||
$this->assertArrayHasKey('valid', $data);
|
||||
$this->assertEquals(0, $data['valid']);
|
||||
$this->assertArrayHasKey('errors', $data);
|
||||
$this->assertIsArray($data['errors']);
|
||||
$this->assertNotEmpty($data['errors']);
|
||||
|
||||
// Check error format
|
||||
$error = $data['errors'][0];
|
||||
$this->assertIsArray($error);
|
||||
$this->assertArrayHasKey('field', $error);
|
||||
$this->assertArrayHasKey('message', $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test required fields validation contract
|
||||
*/
|
||||
public function testRequiredFieldsValidationContract(): void
|
||||
{
|
||||
// Test customer creation with missing required fields
|
||||
$response = $this->httpClient->post('v1/customers/insert', [
|
||||
'json' => [
|
||||
'access_token' => 'test_token',
|
||||
'company_id' => 1
|
||||
// Missing required fields like name, vat, etc.
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
// Should return validation errors
|
||||
$this->assertArrayHasKey('valid', $data);
|
||||
$this->assertEquals(0, $data['valid']);
|
||||
$this->assertArrayHasKey('errors', $data);
|
||||
$this->assertIsArray($data['errors']);
|
||||
$this->assertNotEmpty($data['errors']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test field length limits contract
|
||||
* @depends testOAuthTokenEndpointContract
|
||||
*/
|
||||
public function testFieldLengthLimitsContract(): void
|
||||
{
|
||||
if (!$this->accessToken) {
|
||||
$this->markTestSkipped('Access token not available');
|
||||
}
|
||||
|
||||
// Test with excessively long field values
|
||||
$longString = str_repeat('A', 1000);
|
||||
|
||||
$response = $this->httpClient->post('v1/customers/insert', [
|
||||
'json' => [
|
||||
'access_token' => $this->accessToken,
|
||||
'company_id' => 1,
|
||||
'vat' => '999999992',
|
||||
'number' => 'TEST-LONG-' . time(),
|
||||
'name' => $longString, // Excessively long name
|
||||
'email' => 'test@example.com'
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
// Should either succeed with truncated data or fail with validation error
|
||||
$this->assertArrayHasKey('valid', $data);
|
||||
|
||||
if ($data['valid'] === 0) {
|
||||
$this->assertArrayHasKey('errors', $data);
|
||||
$this->assertNotEmpty($data['errors']);
|
||||
}
|
||||
}
|
||||
}
|
||||
340
modules/desk_moloni/tests/contract/QueueTableTest.php
Normal file
340
modules/desk_moloni/tests/contract/QueueTableTest.php
Normal file
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
/**
|
||||
* Contract Test for desk_moloni_sync_queue table
|
||||
*
|
||||
* This test MUST FAIL until the Sync_queue_model is properly implemented
|
||||
* Following TDD RED-GREEN-REFACTOR cycle
|
||||
*
|
||||
* @package DeskMoloni\Tests\Contract
|
||||
*/
|
||||
|
||||
namespace DeskMoloni\Tests\Contract;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class QueueTableTest extends TestCase
|
||||
{
|
||||
private $CI;
|
||||
private $db;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
$this->CI->load->database();
|
||||
$this->db = $this->CI->db;
|
||||
|
||||
if (ENVIRONMENT !== 'testing') {
|
||||
$this->markTestSkipped('Contract tests should only run in testing environment');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: desk_moloni_sync_queue table must exist with correct structure
|
||||
*/
|
||||
public function queue_table_exists_with_required_structure()
|
||||
{
|
||||
// ACT: Check table existence
|
||||
$table_exists = $this->db->table_exists('desk_moloni_sync_queue');
|
||||
|
||||
// ASSERT: Table must exist
|
||||
$this->assertTrue($table_exists, 'desk_moloni_sync_queue table must exist');
|
||||
|
||||
// ASSERT: Required columns exist
|
||||
$fields = $this->db->list_fields('desk_moloni_sync_queue');
|
||||
$required_fields = [
|
||||
'id', 'task_type', 'entity_type', 'entity_id', 'priority', 'payload',
|
||||
'status', 'attempts', 'max_attempts', 'scheduled_at', 'started_at',
|
||||
'completed_at', 'error_message', 'created_at', 'updated_at'
|
||||
];
|
||||
|
||||
foreach ($required_fields as $field) {
|
||||
$this->assertContains($field, $fields, "Required field '{$field}' must exist in desk_moloni_sync_queue table");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Queue table must enforce task_type ENUM values
|
||||
*/
|
||||
public function queue_table_enforces_task_type_enum()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_sync_queue');
|
||||
|
||||
// ACT & ASSERT: Valid task types should work
|
||||
$valid_task_types = [
|
||||
'sync_client', 'sync_product', 'sync_invoice',
|
||||
'sync_estimate', 'sync_credit_note', 'status_update'
|
||||
];
|
||||
|
||||
foreach ($valid_task_types as $task_type) {
|
||||
$test_data = [
|
||||
'task_type' => $task_type,
|
||||
'entity_type' => 'client',
|
||||
'entity_id' => 1,
|
||||
'priority' => 5
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_sync_queue', $test_data);
|
||||
$this->assertTrue($insert_success, "Task type '{$task_type}' must be valid");
|
||||
|
||||
// Clean up
|
||||
$this->db->delete('desk_moloni_sync_queue', ['task_type' => $task_type]);
|
||||
}
|
||||
|
||||
// ACT & ASSERT: Invalid task type should fail
|
||||
$invalid_data = [
|
||||
'task_type' => 'invalid_task',
|
||||
'entity_type' => 'client',
|
||||
'entity_id' => 1,
|
||||
'priority' => 5
|
||||
];
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->db->insert('desk_moloni_sync_queue', $invalid_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Queue table must enforce status ENUM values
|
||||
*/
|
||||
public function queue_table_enforces_status_enum()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_sync_queue');
|
||||
|
||||
// ACT & ASSERT: Valid status values should work
|
||||
$valid_statuses = ['pending', 'processing', 'completed', 'failed', 'retry'];
|
||||
|
||||
foreach ($valid_statuses as $status) {
|
||||
$test_data = [
|
||||
'task_type' => 'sync_client',
|
||||
'entity_type' => 'client',
|
||||
'entity_id' => 1,
|
||||
'priority' => 5,
|
||||
'status' => $status
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_sync_queue', $test_data);
|
||||
$this->assertTrue($insert_success, "Status '{$status}' must be valid");
|
||||
|
||||
// Clean up
|
||||
$this->db->delete('desk_moloni_sync_queue', ['status' => $status]);
|
||||
}
|
||||
|
||||
// ACT & ASSERT: Invalid status should fail
|
||||
$invalid_data = [
|
||||
'task_type' => 'sync_client',
|
||||
'entity_type' => 'client',
|
||||
'entity_id' => 1,
|
||||
'priority' => 5,
|
||||
'status' => 'invalid_status'
|
||||
];
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->db->insert('desk_moloni_sync_queue', $invalid_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Queue table must support priority-based ordering
|
||||
*/
|
||||
public function queue_table_supports_priority_ordering()
|
||||
{
|
||||
// ARRANGE: Clean table and insert tasks with different priorities
|
||||
$this->db->truncate('desk_moloni_sync_queue');
|
||||
|
||||
$tasks = [
|
||||
['priority' => 9, 'entity_id' => 1], // Lowest priority
|
||||
['priority' => 1, 'entity_id' => 2], // Highest priority
|
||||
['priority' => 5, 'entity_id' => 3], // Medium priority
|
||||
];
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
$task_data = [
|
||||
'task_type' => 'sync_client',
|
||||
'entity_type' => 'client',
|
||||
'entity_id' => $task['entity_id'],
|
||||
'priority' => $task['priority'],
|
||||
'status' => 'pending'
|
||||
];
|
||||
$this->db->insert('desk_moloni_sync_queue', $task_data);
|
||||
}
|
||||
|
||||
// ACT: Query tasks ordered by priority (ascending = highest priority first)
|
||||
$this->db->select('entity_id, priority');
|
||||
$this->db->where('status', 'pending');
|
||||
$this->db->order_by('priority', 'ASC');
|
||||
$ordered_tasks = $this->db->get('desk_moloni_sync_queue')->result();
|
||||
|
||||
// ASSERT: Tasks are ordered by priority (1 = highest, 9 = lowest)
|
||||
$this->assertEquals(2, $ordered_tasks[0]->entity_id, 'Highest priority task (1) should be first');
|
||||
$this->assertEquals(3, $ordered_tasks[1]->entity_id, 'Medium priority task (5) should be second');
|
||||
$this->assertEquals(1, $ordered_tasks[2]->entity_id, 'Lowest priority task (9) should be last');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Queue table must support JSON payload for task data
|
||||
*/
|
||||
public function queue_table_supports_json_payload()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_sync_queue');
|
||||
|
||||
// ACT: Insert task with JSON payload
|
||||
$json_payload = [
|
||||
'sync_fields' => ['name', 'email', 'vat'],
|
||||
'force_update' => true,
|
||||
'retry_count' => 0,
|
||||
'metadata' => [
|
||||
'source' => 'perfex',
|
||||
'trigger' => 'after_client_updated'
|
||||
]
|
||||
];
|
||||
|
||||
$task_data = [
|
||||
'task_type' => 'sync_client',
|
||||
'entity_type' => 'client',
|
||||
'entity_id' => 100,
|
||||
'priority' => 3,
|
||||
'payload' => json_encode($json_payload),
|
||||
'status' => 'pending'
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_sync_queue', $task_data);
|
||||
$this->assertTrue($insert_success, 'Task with JSON payload must be inserted successfully');
|
||||
|
||||
// ASSERT: JSON payload is stored and retrieved correctly
|
||||
$row = $this->db->get_where('desk_moloni_sync_queue', ['entity_id' => 100])->row();
|
||||
$retrieved_payload = json_decode($row->payload, true);
|
||||
|
||||
$this->assertEquals($json_payload, $retrieved_payload, 'JSON payload must be stored and retrieved correctly');
|
||||
$this->assertTrue(is_array($retrieved_payload), 'Payload must be retrievable as array');
|
||||
$this->assertEquals('perfex', $retrieved_payload['metadata']['source'], 'Nested JSON data must be accessible');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Queue table must support retry mechanism with attempts tracking
|
||||
*/
|
||||
public function queue_table_supports_retry_mechanism()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_sync_queue');
|
||||
|
||||
// ACT: Insert task with retry configuration
|
||||
$retry_task = [
|
||||
'task_type' => 'sync_invoice',
|
||||
'entity_type' => 'invoice',
|
||||
'entity_id' => 200,
|
||||
'priority' => 1,
|
||||
'status' => 'failed',
|
||||
'attempts' => 2,
|
||||
'max_attempts' => 3,
|
||||
'error_message' => 'API rate limit exceeded'
|
||||
];
|
||||
|
||||
$insert_success = $this->db->insert('desk_moloni_sync_queue', $retry_task);
|
||||
$this->assertTrue($insert_success, 'Task with retry configuration must be inserted');
|
||||
|
||||
// ASSERT: Retry data is stored correctly
|
||||
$row = $this->db->get_where('desk_moloni_sync_queue', ['entity_id' => 200])->row();
|
||||
|
||||
$this->assertEquals(2, $row->attempts, 'Attempts counter must be stored');
|
||||
$this->assertEquals(3, $row->max_attempts, 'Max attempts limit must be stored');
|
||||
$this->assertEquals('failed', $row->status, 'Failed status must be stored');
|
||||
$this->assertEquals('API rate limit exceeded', $row->error_message, 'Error message must be stored');
|
||||
|
||||
// ASSERT: Task can be updated for retry
|
||||
$this->db->set('status', 'retry');
|
||||
$this->db->set('attempts', $row->attempts + 1);
|
||||
$this->db->where('id', $row->id);
|
||||
$update_success = $this->db->update('desk_moloni_sync_queue');
|
||||
|
||||
$this->assertTrue($update_success, 'Task must be updatable for retry');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Queue table must have required indexes for performance
|
||||
*/
|
||||
public function queue_table_has_required_indexes()
|
||||
{
|
||||
// ACT: Get table indexes
|
||||
$indexes = $this->db->query("SHOW INDEX FROM desk_moloni_sync_queue")->result_array();
|
||||
|
||||
// ASSERT: Required indexes exist
|
||||
$required_indexes = [
|
||||
'PRIMARY',
|
||||
'idx_status_priority',
|
||||
'idx_entity',
|
||||
'idx_scheduled',
|
||||
'idx_status_attempts',
|
||||
'idx_queue_processing'
|
||||
];
|
||||
|
||||
$index_names = array_column($indexes, 'Key_name');
|
||||
|
||||
foreach ($required_indexes as $required_index) {
|
||||
$this->assertContains($required_index, $index_names, "Required index '{$required_index}' must exist for queue performance");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* Contract: Queue table must support scheduled execution times
|
||||
*/
|
||||
public function queue_table_supports_scheduled_execution()
|
||||
{
|
||||
// ARRANGE: Clean table
|
||||
$this->db->truncate('desk_moloni_sync_queue');
|
||||
|
||||
// ACT: Insert tasks with different scheduled times
|
||||
$future_time = date('Y-m-d H:i:s', time() + 3600); // 1 hour from now
|
||||
$past_time = date('Y-m-d H:i:s', time() - 3600); // 1 hour ago
|
||||
|
||||
$scheduled_tasks = [
|
||||
[
|
||||
'entity_id' => 301,
|
||||
'scheduled_at' => $future_time,
|
||||
'status' => 'pending'
|
||||
],
|
||||
[
|
||||
'entity_id' => 302,
|
||||
'scheduled_at' => $past_time,
|
||||
'status' => 'pending'
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($scheduled_tasks as $task) {
|
||||
$task_data = array_merge([
|
||||
'task_type' => 'sync_product',
|
||||
'entity_type' => 'product',
|
||||
'priority' => 5
|
||||
], $task);
|
||||
|
||||
$this->db->insert('desk_moloni_sync_queue', $task_data);
|
||||
}
|
||||
|
||||
// ASSERT: Tasks can be filtered by scheduled time
|
||||
$ready_tasks = $this->db->get_where('desk_moloni_sync_queue', [
|
||||
'scheduled_at <=' => date('Y-m-d H:i:s'),
|
||||
'status' => 'pending'
|
||||
])->result();
|
||||
|
||||
$this->assertCount(1, $ready_tasks, 'Only past/current scheduled tasks should be ready');
|
||||
$this->assertEquals(302, $ready_tasks[0]->entity_id, 'Past scheduled task should be ready for processing');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Clean up test data
|
||||
if ($this->db) {
|
||||
$this->db->where('entity_id >=', 1);
|
||||
$this->db->where('entity_id <=', 400);
|
||||
$this->db->delete('desk_moloni_sync_queue');
|
||||
}
|
||||
}
|
||||
}
|
||||
376
modules/desk_moloni/tests/contract/test_admin_api.php
Normal file
376
modules/desk_moloni/tests/contract/test_admin_api.php
Normal file
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Contract Test: Admin API Endpoints
|
||||
*
|
||||
* Tests the Admin API endpoints contract
|
||||
* These tests MUST FAIL initially (TDD) before implementing the Admin controller endpoints
|
||||
*
|
||||
* @package DeskMoloni
|
||||
* @subpackage Tests\Contract
|
||||
* @version 3.0.0
|
||||
* @author Descomplicar®
|
||||
*/
|
||||
|
||||
define('BASEPATH', true);
|
||||
define('ENVIRONMENT', 'testing');
|
||||
|
||||
echo "\n" . str_repeat("=", 80) . "\n";
|
||||
echo "ADMIN API ENDPOINTS CONTRACT TESTS\n";
|
||||
echo "TDD: These tests MUST FAIL before implementation\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
$test_results = [];
|
||||
$start_time = microtime(true);
|
||||
|
||||
// Test 1: Admin Controller File Existence
|
||||
echo "1. 🧪 Testing Admin Controller File Existence...\n";
|
||||
$admin_file = __DIR__ . '/../../controllers/Admin.php';
|
||||
|
||||
if (file_exists($admin_file)) {
|
||||
echo " ✅ Admin.php controller exists\n";
|
||||
$test_results['controller_exists'] = true;
|
||||
} else {
|
||||
echo " ❌ EXPECTED FAILURE: Admin.php controller does not exist\n";
|
||||
echo " 📝 TODO: Create controllers/Admin.php\n";
|
||||
$test_results['controller_exists'] = false;
|
||||
}
|
||||
|
||||
// Test 2: Required Admin API Endpoints
|
||||
echo "\n2. 🧪 Testing Required Admin API Endpoints...\n";
|
||||
|
||||
$required_endpoints = [
|
||||
// OAuth Management
|
||||
'oauth_configure' => 'OAuth configuration endpoint',
|
||||
'oauth_callback' => 'OAuth callback handler',
|
||||
'oauth_status' => 'OAuth status check',
|
||||
'oauth_test' => 'OAuth connection test',
|
||||
|
||||
// Configuration Management
|
||||
'save_config' => 'Save module configuration',
|
||||
'get_config' => 'Get module configuration',
|
||||
'test_connection' => 'Test API connection',
|
||||
'reset_config' => 'Reset configuration',
|
||||
|
||||
// Sync Management
|
||||
'manual_sync' => 'Manual synchronization trigger',
|
||||
'bulk_sync' => 'Bulk synchronization',
|
||||
'sync_status' => 'Synchronization status',
|
||||
'cancel_sync' => 'Cancel synchronization',
|
||||
|
||||
// Queue Management
|
||||
'queue_status' => 'Queue status check',
|
||||
'queue_clear' => 'Clear queue',
|
||||
'queue_retry' => 'Retry failed tasks',
|
||||
'queue_stats' => 'Queue statistics',
|
||||
|
||||
// Mapping Management
|
||||
'mapping_create' => 'Create entity mapping',
|
||||
'mapping_update' => 'Update entity mapping',
|
||||
'mapping_delete' => 'Delete entity mapping',
|
||||
'mapping_discover' => 'Auto-discover mappings',
|
||||
|
||||
// Monitoring & Logs
|
||||
'get_logs' => 'Get synchronization logs',
|
||||
'clear_logs' => 'Clear logs',
|
||||
'get_stats' => 'Get module statistics',
|
||||
'health_check' => 'System health check'
|
||||
];
|
||||
|
||||
if (file_exists($admin_file)) {
|
||||
$content = file_get_contents($admin_file);
|
||||
$endpoints_found = 0;
|
||||
|
||||
foreach ($required_endpoints as $endpoint => $description) {
|
||||
// Check for method definition
|
||||
if (strpos($content, "function {$endpoint}") !== false ||
|
||||
strpos($content, "public function {$endpoint}") !== false) {
|
||||
echo " ✅ Endpoint {$endpoint}() found - {$description}\n";
|
||||
$endpoints_found++;
|
||||
} else {
|
||||
echo " ❌ Endpoint {$endpoint}() missing - {$description}\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['endpoints_complete'] = ($endpoints_found === count($required_endpoints));
|
||||
echo " 📊 Endpoints found: {$endpoints_found}/" . count($required_endpoints) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test endpoints - controller file does not exist\n";
|
||||
$test_results['endpoints_complete'] = false;
|
||||
}
|
||||
|
||||
// Test 3: HTTP Methods Support
|
||||
echo "\n3. 🧪 Testing HTTP Methods Support...\n";
|
||||
|
||||
$http_methods = [
|
||||
'GET' => ['oauth_status', 'get_config', 'queue_status', 'get_logs'],
|
||||
'POST' => ['oauth_configure', 'save_config', 'manual_sync', 'mapping_create'],
|
||||
'PUT' => ['mapping_update', 'oauth_callback'],
|
||||
'DELETE' => ['mapping_delete', 'queue_clear']
|
||||
];
|
||||
|
||||
if (file_exists($admin_file)) {
|
||||
$content = file_get_contents($admin_file);
|
||||
$methods_supported = 0;
|
||||
|
||||
foreach ($http_methods as $method => $endpoints) {
|
||||
$method_found = false;
|
||||
foreach ($endpoints as $endpoint) {
|
||||
// Check if method restriction is implemented
|
||||
if (strpos($content, '$this->input->method()') !== false ||
|
||||
strpos($content, "'{$method}'") !== false ||
|
||||
strpos($content, "\"{$method}\"") !== false) {
|
||||
$method_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($method_found) {
|
||||
echo " ✅ {$method} method support found\n";
|
||||
$methods_supported++;
|
||||
} else {
|
||||
echo " ❌ {$method} method support missing\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['http_methods'] = ($methods_supported >= 2);
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test HTTP methods - controller file does not exist\n";
|
||||
$test_results['http_methods'] = false;
|
||||
}
|
||||
|
||||
// Test 4: Response Format Contract
|
||||
echo "\n4. 🧪 Testing Response Format Contract...\n";
|
||||
|
||||
$response_patterns = [
|
||||
'JSON responses' => 'set_content_type.*application/json',
|
||||
'Status codes' => 'set_status_header',
|
||||
'Error handling' => 'try.*catch',
|
||||
'Success responses' => 'success.*true',
|
||||
'Error responses' => 'error.*message'
|
||||
];
|
||||
|
||||
if (file_exists($admin_file)) {
|
||||
$content = file_get_contents($admin_file);
|
||||
$patterns_found = 0;
|
||||
|
||||
foreach ($response_patterns as $feature => $pattern) {
|
||||
if (preg_match("/{$pattern}/i", $content)) {
|
||||
echo " ✅ {$feature} implementation found\n";
|
||||
$patterns_found++;
|
||||
} else {
|
||||
echo " ❌ {$feature} implementation missing\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['response_format'] = ($patterns_found >= 3);
|
||||
echo " 📊 Response patterns: {$patterns_found}/" . count($response_patterns) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test response format - controller file does not exist\n";
|
||||
$test_results['response_format'] = false;
|
||||
}
|
||||
|
||||
// Test 5: Security & Authentication
|
||||
echo "\n5. 🧪 Testing Security & Authentication...\n";
|
||||
|
||||
$security_features = [
|
||||
'Permission checks' => 'has_permission',
|
||||
'CSRF protection' => 'csrf',
|
||||
'Input validation' => 'xss_clean|htmlspecialchars',
|
||||
'Admin authentication' => 'is_admin|admin_logged_in',
|
||||
'Rate limiting' => 'rate_limit'
|
||||
];
|
||||
|
||||
if (file_exists($admin_file)) {
|
||||
$content = file_get_contents($admin_file);
|
||||
$security_found = 0;
|
||||
|
||||
foreach ($security_features as $feature => $pattern) {
|
||||
if (preg_match("/{$pattern}/i", $content)) {
|
||||
echo " ✅ {$feature} found\n";
|
||||
$security_found++;
|
||||
} else {
|
||||
echo " ❌ {$feature} missing\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['security_features'] = ($security_found >= 3);
|
||||
echo " 📊 Security features: {$security_found}/" . count($security_features) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test security - controller file does not exist\n";
|
||||
$test_results['security_features'] = false;
|
||||
}
|
||||
|
||||
// Test 6: Model Integration
|
||||
echo "\n6. 🧪 Testing Model Integration...\n";
|
||||
|
||||
$required_models = [
|
||||
'config_model' => 'Configuration management',
|
||||
'sync_queue_model' => 'Queue management',
|
||||
'sync_log_model' => 'Logging',
|
||||
'mapping_model' => 'Entity mapping'
|
||||
];
|
||||
|
||||
if (file_exists($admin_file)) {
|
||||
$content = file_get_contents($admin_file);
|
||||
$models_found = 0;
|
||||
|
||||
foreach ($required_models as $model => $description) {
|
||||
if (strpos($content, $model) !== false) {
|
||||
echo " ✅ {$model} integration found - {$description}\n";
|
||||
$models_found++;
|
||||
} else {
|
||||
echo " ❌ {$model} integration missing - {$description}\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['model_integration'] = ($models_found === count($required_models));
|
||||
echo " 📊 Models integrated: {$models_found}/" . count($required_models) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test model integration - controller file does not exist\n";
|
||||
$test_results['model_integration'] = false;
|
||||
}
|
||||
|
||||
// Test 7: Error Handling Contract
|
||||
echo "\n7. 🧪 Testing Error Handling Contract...\n";
|
||||
|
||||
$error_handling_patterns = [
|
||||
'Exception handling' => 'try\s*{.*}.*catch',
|
||||
'Error logging' => 'log_message.*error',
|
||||
'User feedback' => 'set_alert|alert_float',
|
||||
'Validation errors' => 'form_validation|validate',
|
||||
'API error handling' => 'api.*error|error.*response'
|
||||
];
|
||||
|
||||
if (file_exists($admin_file)) {
|
||||
$content = file_get_contents($admin_file);
|
||||
$error_patterns_found = 0;
|
||||
|
||||
foreach ($error_handling_patterns as $feature => $pattern) {
|
||||
if (preg_match("/{$pattern}/i", $content)) {
|
||||
echo " ✅ {$feature} found\n";
|
||||
$error_patterns_found++;
|
||||
} else {
|
||||
echo " ❌ {$feature} missing\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['error_handling'] = ($error_patterns_found >= 3);
|
||||
echo " 📊 Error handling patterns: {$error_patterns_found}/" . count($error_handling_patterns) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test error handling - controller file does not exist\n";
|
||||
$test_results['error_handling'] = false;
|
||||
}
|
||||
|
||||
// Test 8: Documentation & Comments
|
||||
echo "\n8. 🧪 Testing Documentation Contract...\n";
|
||||
|
||||
if (file_exists($admin_file)) {
|
||||
$content = file_get_contents($admin_file);
|
||||
$doc_features = 0;
|
||||
|
||||
// Check for proper documentation
|
||||
if (strpos($content, '/**') !== false) {
|
||||
echo " ✅ PHPDoc comments found\n";
|
||||
$doc_features++;
|
||||
} else {
|
||||
echo " ❌ PHPDoc comments missing\n";
|
||||
}
|
||||
|
||||
if (strpos($content, '@param') !== false) {
|
||||
echo " ✅ Parameter documentation found\n";
|
||||
$doc_features++;
|
||||
} else {
|
||||
echo " ❌ Parameter documentation missing\n";
|
||||
}
|
||||
|
||||
if (strpos($content, '@return') !== false) {
|
||||
echo " ✅ Return value documentation found\n";
|
||||
$doc_features++;
|
||||
} else {
|
||||
echo " ❌ Return value documentation missing\n";
|
||||
}
|
||||
|
||||
$test_results['documentation'] = ($doc_features >= 2);
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test documentation - controller file does not exist\n";
|
||||
$test_results['documentation'] = false;
|
||||
}
|
||||
|
||||
// Generate Final Report
|
||||
$execution_time = microtime(true) - $start_time;
|
||||
|
||||
echo "\n" . str_repeat("=", 80) . "\n";
|
||||
echo "ADMIN API CONTRACT TEST REPORT\n";
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
|
||||
$passed_tests = array_filter($test_results, function($result) {
|
||||
return $result === true;
|
||||
});
|
||||
|
||||
$failed_tests = array_filter($test_results, function($result) {
|
||||
return $result === false;
|
||||
});
|
||||
|
||||
echo "Execution Time: " . number_format($execution_time, 2) . "s\n";
|
||||
echo "Tests Passed: " . count($passed_tests) . "\n";
|
||||
echo "Tests Failed: " . count($failed_tests) . " (EXPECTED in TDD)\n";
|
||||
|
||||
if (count($failed_tests) > 0) {
|
||||
echo "\n🔴 TDD STATUS: TESTS FAILING AS EXPECTED\n";
|
||||
echo "Next Step: Implement Admin controller endpoints to make tests pass\n";
|
||||
|
||||
echo "\nFailed Test Categories:\n";
|
||||
foreach ($test_results as $test => $result) {
|
||||
if ($result === false) {
|
||||
echo " ❌ " . ucwords(str_replace('_', ' ', $test)) . "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo "\n🟢 ALL TESTS PASSING\n";
|
||||
echo "Admin API implementation appears to be complete\n";
|
||||
}
|
||||
|
||||
echo "\n📋 IMPLEMENTATION REQUIREMENTS:\n";
|
||||
echo " 1. Complete all missing API endpoints in Admin controller\n";
|
||||
echo " 2. Implement proper HTTP method handling (GET/POST/PUT/DELETE)\n";
|
||||
echo " 3. Add comprehensive security and authentication\n";
|
||||
echo " 4. Ensure proper JSON response format\n";
|
||||
echo " 5. Integrate with all required models\n";
|
||||
echo " 6. Add robust error handling throughout\n";
|
||||
echo " 7. Document all methods with PHPDoc\n";
|
||||
|
||||
echo "\n🎯 SUCCESS CRITERIA:\n";
|
||||
echo " - All " . count($required_endpoints) . " API endpoints implemented\n";
|
||||
echo " - Proper HTTP method support\n";
|
||||
echo " - Security measures in place\n";
|
||||
echo " - Consistent JSON response format\n";
|
||||
echo " - Full model integration\n";
|
||||
echo " - Comprehensive error handling\n";
|
||||
|
||||
// Save results
|
||||
$reports_dir = __DIR__ . '/../reports';
|
||||
if (!is_dir($reports_dir)) {
|
||||
mkdir($reports_dir, 0755, true);
|
||||
}
|
||||
|
||||
$report_file = $reports_dir . '/admin_api_contract_test_' . date('Y-m-d_H-i-s') . '.json';
|
||||
file_put_contents($report_file, json_encode([
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'test_type' => 'admin_api_contract',
|
||||
'status' => count($failed_tests) > 0 ? 'failing' : 'passing',
|
||||
'results' => $test_results,
|
||||
'execution_time' => $execution_time,
|
||||
'endpoints_required' => count($required_endpoints),
|
||||
'tdd_status' => 'Tests failing as expected - ready for implementation'
|
||||
], JSON_PRETTY_PRINT));
|
||||
|
||||
echo "\n📄 Contract test results saved to: {$report_file}\n";
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
361
modules/desk_moloni/tests/contract/test_client_portal_api.php
Normal file
361
modules/desk_moloni/tests/contract/test_client_portal_api.php
Normal file
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Contract Test: Client Portal API Endpoints
|
||||
*
|
||||
* Tests the Client Portal API endpoints contract
|
||||
* These tests MUST FAIL initially (TDD) before implementing the Client Portal endpoints
|
||||
*
|
||||
* @package DeskMoloni
|
||||
* @subpackage Tests\Contract
|
||||
* @version 3.0.0
|
||||
* @author Descomplicar®
|
||||
*/
|
||||
|
||||
define('BASEPATH', true);
|
||||
define('ENVIRONMENT', 'testing');
|
||||
|
||||
echo "\n" . str_repeat("=", 80) . "\n";
|
||||
echo "CLIENT PORTAL API ENDPOINTS CONTRACT TESTS\n";
|
||||
echo "TDD: These tests MUST FAIL before implementation\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
$test_results = [];
|
||||
$start_time = microtime(true);
|
||||
|
||||
// Test 1: Client Portal Controller File Existence
|
||||
echo "1. 🧪 Testing Client Portal Controller File Existence...\n";
|
||||
$client_portal_file = __DIR__ . '/../../controllers/ClientPortal.php';
|
||||
|
||||
if (file_exists($client_portal_file)) {
|
||||
echo " ✅ ClientPortal.php controller exists\n";
|
||||
$test_results['controller_exists'] = true;
|
||||
} else {
|
||||
echo " ❌ EXPECTED FAILURE: ClientPortal.php controller does not exist\n";
|
||||
echo " 📝 TODO: Create controllers/ClientPortal.php\n";
|
||||
$test_results['controller_exists'] = false;
|
||||
}
|
||||
|
||||
// Test 2: Required Client Portal API Endpoints
|
||||
echo "\n2. 🧪 Testing Required Client Portal API Endpoints...\n";
|
||||
|
||||
$required_endpoints = [
|
||||
// Authentication & Session
|
||||
'client_login' => 'Client authentication endpoint',
|
||||
'client_logout' => 'Client logout endpoint',
|
||||
'client_session_check' => 'Session validation endpoint',
|
||||
'client_password_reset' => 'Password reset endpoint',
|
||||
|
||||
// Dashboard & Overview
|
||||
'dashboard' => 'Client dashboard data',
|
||||
'sync_status' => 'Current sync status for client',
|
||||
'recent_activity' => 'Recent sync activity log',
|
||||
'error_summary' => 'Summary of sync errors',
|
||||
|
||||
// Invoice Management
|
||||
'get_invoices' => 'Get client invoices list',
|
||||
'get_invoice_details' => 'Get specific invoice details',
|
||||
'download_invoice' => 'Download invoice PDF',
|
||||
'sync_invoice' => 'Manual invoice sync trigger',
|
||||
|
||||
// Client Data Management
|
||||
'get_client_data' => 'Get client profile data',
|
||||
'update_client_data' => 'Update client information',
|
||||
'get_sync_preferences' => 'Get sync preferences',
|
||||
'update_sync_preferences' => 'Update sync preferences',
|
||||
|
||||
// Reports & Analytics
|
||||
'get_sync_report' => 'Get synchronization report',
|
||||
'get_revenue_report' => 'Get revenue analytics',
|
||||
'export_data' => 'Export client data',
|
||||
'get_invoice_stats' => 'Get invoice statistics',
|
||||
|
||||
// Support & Help
|
||||
'submit_support_ticket' => 'Submit support request',
|
||||
'get_support_tickets' => 'Get client support tickets',
|
||||
'get_help_resources' => 'Get help documentation',
|
||||
'contact_support' => 'Contact support form'
|
||||
];
|
||||
|
||||
if (file_exists($client_portal_file)) {
|
||||
$content = file_get_contents($client_portal_file);
|
||||
$endpoints_found = 0;
|
||||
|
||||
foreach ($required_endpoints as $endpoint => $description) {
|
||||
// Check for method definition
|
||||
if (strpos($content, "function {$endpoint}") !== false ||
|
||||
strpos($content, "public function {$endpoint}") !== false) {
|
||||
echo " ✅ Endpoint {$endpoint}() found - {$description}\n";
|
||||
$endpoints_found++;
|
||||
} else {
|
||||
echo " ❌ Endpoint {$endpoint}() missing - {$description}\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['endpoints_complete'] = ($endpoints_found === count($required_endpoints));
|
||||
echo " 📊 Endpoints found: {$endpoints_found}/" . count($required_endpoints) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test endpoints - controller file does not exist\n";
|
||||
$test_results['endpoints_complete'] = false;
|
||||
}
|
||||
|
||||
// Test 3: Client Authentication System
|
||||
echo "\n3. 🧪 Testing Client Authentication System...\n";
|
||||
|
||||
$auth_features = [
|
||||
'Session management' => ['session_start', 'session_destroy', 'session_check'],
|
||||
'Client validation' => ['client_id', 'validate_client', 'check_permissions'],
|
||||
'Security features' => ['csrf_token', 'xss_clean', 'rate_limit'],
|
||||
'Password handling' => ['password_hash', 'password_verify', 'reset_token']
|
||||
];
|
||||
|
||||
if (file_exists($client_portal_file)) {
|
||||
$content = file_get_contents($client_portal_file);
|
||||
$auth_score = 0;
|
||||
|
||||
foreach ($auth_features as $feature => $keywords) {
|
||||
$feature_found = false;
|
||||
foreach ($keywords as $keyword) {
|
||||
if (stripos($content, $keyword) !== false) {
|
||||
$feature_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($feature_found) {
|
||||
echo " ✅ {$feature} implementation found\n";
|
||||
$auth_score++;
|
||||
} else {
|
||||
echo " ❌ {$feature} implementation missing\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['authentication_system'] = ($auth_score >= 3);
|
||||
echo " 📊 Auth features: {$auth_score}/" . count($auth_features) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test authentication - controller file does not exist\n";
|
||||
$test_results['authentication_system'] = false;
|
||||
}
|
||||
|
||||
// Test 4: Response Format & Standards
|
||||
echo "\n4. 🧪 Testing Response Format & Standards...\n";
|
||||
|
||||
$response_standards = [
|
||||
'JSON responses' => 'set_content_type.*application/json',
|
||||
'HTTP status codes' => 'set_status_header|http_response_code',
|
||||
'Success format' => 'success.*true|status.*success',
|
||||
'Error format' => 'error.*message|status.*error',
|
||||
'Data structure' => 'data.*array|payload.*data',
|
||||
'Pagination support' => 'page|limit|offset|total'
|
||||
];
|
||||
|
||||
if (file_exists($client_portal_file)) {
|
||||
$content = file_get_contents($client_portal_file);
|
||||
$standards_found = 0;
|
||||
|
||||
foreach ($response_standards as $standard => $pattern) {
|
||||
if (preg_match("/{$pattern}/i", $content)) {
|
||||
echo " ✅ {$standard} implementation found\n";
|
||||
$standards_found++;
|
||||
} else {
|
||||
echo " ❌ {$standard} implementation missing\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['response_standards'] = ($standards_found >= 4);
|
||||
echo " 📊 Standards found: {$standards_found}/" . count($response_standards) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test response standards - controller file does not exist\n";
|
||||
$test_results['response_standards'] = false;
|
||||
}
|
||||
|
||||
// Test 5: Data Access & Permissions
|
||||
echo "\n5. 🧪 Testing Data Access & Permissions...\n";
|
||||
|
||||
$permission_features = [
|
||||
'Client data isolation' => 'client_id.*WHERE|WHERE.*client_id',
|
||||
'Permission checks' => 'check_permission|has_access|can_access',
|
||||
'Data filtering' => 'filter_client_data|client_only',
|
||||
'Access logging' => 'log_access|audit_trail'
|
||||
];
|
||||
|
||||
if (file_exists($client_portal_file)) {
|
||||
$content = file_get_contents($client_portal_file);
|
||||
$permission_score = 0;
|
||||
|
||||
foreach ($permission_features as $feature => $pattern) {
|
||||
if (preg_match("/{$pattern}/i", $content)) {
|
||||
echo " ✅ {$feature} found\n";
|
||||
$permission_score++;
|
||||
} else {
|
||||
echo " ❌ {$feature} missing\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['data_permissions'] = ($permission_score >= 2);
|
||||
echo " 📊 Permission features: {$permission_score}/" . count($permission_features) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test permissions - controller file does not exist\n";
|
||||
$test_results['data_permissions'] = false;
|
||||
}
|
||||
|
||||
// Test 6: Frontend Integration
|
||||
echo "\n6. 🧪 Testing Frontend Integration...\n";
|
||||
|
||||
$frontend_files = [
|
||||
'client_portal_view' => __DIR__ . '/../../views/client_portal/dashboard.php',
|
||||
'client_assets' => __DIR__ . '/../../assets/client_portal',
|
||||
'client_config' => __DIR__ . '/../../config/client_portal.php'
|
||||
];
|
||||
|
||||
$frontend_score = 0;
|
||||
foreach ($frontend_files as $component => $path) {
|
||||
if (file_exists($path) || is_dir($path)) {
|
||||
echo " ✅ {$component} exists\n";
|
||||
$frontend_score++;
|
||||
} else {
|
||||
echo " ❌ {$component} missing at {$path}\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['frontend_integration'] = ($frontend_score >= 2);
|
||||
|
||||
// Test 7: Model Integration
|
||||
echo "\n7. 🧪 Testing Model Integration...\n";
|
||||
|
||||
$required_models = [
|
||||
'client_model' => 'Client data management',
|
||||
'invoice_model' => 'Invoice operations',
|
||||
'sync_log_model' => 'Activity logging',
|
||||
'config_model' => 'Client preferences'
|
||||
];
|
||||
|
||||
if (file_exists($client_portal_file)) {
|
||||
$content = file_get_contents($client_portal_file);
|
||||
$models_found = 0;
|
||||
|
||||
foreach ($required_models as $model => $description) {
|
||||
if (strpos($content, $model) !== false) {
|
||||
echo " ✅ {$model} integration found - {$description}\n";
|
||||
$models_found++;
|
||||
} else {
|
||||
echo " ❌ {$model} integration missing - {$description}\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['model_integration'] = ($models_found >= 3);
|
||||
echo " 📊 Models integrated: {$models_found}/" . count($required_models) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test model integration - controller file does not exist\n";
|
||||
$test_results['model_integration'] = false;
|
||||
}
|
||||
|
||||
// Test 8: Error Handling & Logging
|
||||
echo "\n8. 🧪 Testing Error Handling & Logging...\n";
|
||||
|
||||
$error_handling = [
|
||||
'Exception handling' => 'try\\s*{.*}.*catch',
|
||||
'Input validation' => 'validate_input|form_validation',
|
||||
'Error logging' => 'log_message.*error|error_log',
|
||||
'User feedback' => 'flash_message|alert|notification',
|
||||
'Graceful degradation' => 'fallback|default_response'
|
||||
];
|
||||
|
||||
if (file_exists($client_portal_file)) {
|
||||
$content = file_get_contents($client_portal_file);
|
||||
$error_features = 0;
|
||||
|
||||
foreach ($error_handling as $feature => $pattern) {
|
||||
if (preg_match("/{$pattern}/i", $content)) {
|
||||
echo " ✅ {$feature} found\n";
|
||||
$error_features++;
|
||||
} else {
|
||||
echo " ❌ {$feature} missing\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['error_handling'] = ($error_features >= 3);
|
||||
echo " 📊 Error handling features: {$error_features}/" . count($error_handling) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test error handling - controller file does not exist\n";
|
||||
$test_results['error_handling'] = false;
|
||||
}
|
||||
|
||||
// Generate Final Report
|
||||
$execution_time = microtime(true) - $start_time;
|
||||
|
||||
echo "\n" . str_repeat("=", 80) . "\n";
|
||||
echo "CLIENT PORTAL API CONTRACT TEST REPORT\n";
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
|
||||
$passed_tests = array_filter($test_results, function($result) {
|
||||
return $result === true;
|
||||
});
|
||||
|
||||
$failed_tests = array_filter($test_results, function($result) {
|
||||
return $result === false;
|
||||
});
|
||||
|
||||
echo "Execution Time: " . number_format($execution_time, 2) . "s\n";
|
||||
echo "Tests Passed: " . count($passed_tests) . "\n";
|
||||
echo "Tests Failed: " . count($failed_tests) . " (EXPECTED in TDD)\n";
|
||||
|
||||
if (count($failed_tests) > 0) {
|
||||
echo "\n🔴 TDD STATUS: TESTS FAILING AS EXPECTED\n";
|
||||
echo "Next Step: Implement Client Portal controller to make tests pass\n";
|
||||
|
||||
echo "\nFailed Test Categories:\n";
|
||||
foreach ($test_results as $test => $result) {
|
||||
if ($result === false) {
|
||||
echo " ❌ " . ucwords(str_replace('_', ' ', $test)) . "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo "\n🟢 ALL TESTS PASSING\n";
|
||||
echo "Client Portal implementation appears to be complete\n";
|
||||
}
|
||||
|
||||
echo "\n📋 IMPLEMENTATION REQUIREMENTS:\n";
|
||||
echo " 1. Create controllers/ClientPortal.php with all " . count($required_endpoints) . " endpoints\n";
|
||||
echo " 2. Implement secure client authentication system\n";
|
||||
echo " 3. Add proper data access controls and permissions\n";
|
||||
echo " 4. Create consistent JSON response format\n";
|
||||
echo " 5. Integrate with all required models\n";
|
||||
echo " 6. Build responsive frontend interface\n";
|
||||
echo " 7. Add comprehensive error handling and logging\n";
|
||||
echo " 8. Implement input validation and security measures\n";
|
||||
|
||||
echo "\n🎯 SUCCESS CRITERIA:\n";
|
||||
echo " - All " . count($required_endpoints) . " API endpoints functional\n";
|
||||
echo " - Secure client authentication and session management\n";
|
||||
echo " - Proper data isolation and access controls\n";
|
||||
echo " - Consistent API response format\n";
|
||||
echo " - Responsive and user-friendly interface\n";
|
||||
echo " - Comprehensive error handling\n";
|
||||
echo " - Full model integration\n";
|
||||
|
||||
// Save results
|
||||
$reports_dir = __DIR__ . '/../reports';
|
||||
if (!is_dir($reports_dir)) {
|
||||
mkdir($reports_dir, 0755, true);
|
||||
}
|
||||
|
||||
$report_file = $reports_dir . '/client_portal_contract_test_' . date('Y-m-d_H-i-s') . '.json';
|
||||
file_put_contents($report_file, json_encode([
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'test_type' => 'client_portal_contract',
|
||||
'status' => count($failed_tests) > 0 ? 'failing' : 'passing',
|
||||
'results' => $test_results,
|
||||
'execution_time' => $execution_time,
|
||||
'endpoints_required' => count($required_endpoints),
|
||||
'tdd_status' => 'Tests failing as expected - ready for implementation'
|
||||
], JSON_PRETTY_PRINT));
|
||||
|
||||
echo "\n📄 Contract test results saved to: {$report_file}\n";
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
534
modules/desk_moloni/tests/contract/test_moloni_oauth.php
Normal file
534
modules/desk_moloni/tests/contract/test_moloni_oauth.php
Normal file
@@ -0,0 +1,534 @@
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/**
|
||||
* Contract Test: Moloni OAuth API
|
||||
*
|
||||
* Tests the OAuth 2.0 integration contract with Moloni API
|
||||
* These tests MUST FAIL initially (TDD) before implementing the OAuth library
|
||||
*
|
||||
* @package DeskMoloni
|
||||
* @subpackage Tests\Contract
|
||||
* @version 3.0.0
|
||||
* @author Descomplicar®
|
||||
*/
|
||||
|
||||
class Test_Moloni_OAuth extends CI_Controller
|
||||
{
|
||||
private $CI;
|
||||
private $oauth_lib;
|
||||
private $test_results = [];
|
||||
private $start_time;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->CI = &get_instance();
|
||||
$this->start_time = microtime(true);
|
||||
|
||||
// Load testing framework
|
||||
$this->load->helper('url');
|
||||
$this->load->database();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all OAuth contract tests
|
||||
*/
|
||||
public function run_all_tests()
|
||||
{
|
||||
echo "\n" . str_repeat("=", 80) . "\n";
|
||||
echo "MOLONI OAUTH API CONTRACT TESTS\n";
|
||||
echo "TDD: These tests MUST FAIL before implementation\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
try {
|
||||
// Test OAuth library loading
|
||||
$this->test_oauth_library_loading();
|
||||
|
||||
// Test OAuth configuration
|
||||
$this->test_oauth_configuration_contract();
|
||||
|
||||
// Test authorization URL generation
|
||||
$this->test_authorization_url_contract();
|
||||
|
||||
// Test callback handling
|
||||
$this->test_callback_handling_contract();
|
||||
|
||||
// Test token management
|
||||
$this->test_token_management_contract();
|
||||
|
||||
// Test token refresh
|
||||
$this->test_token_refresh_contract();
|
||||
|
||||
// Test API authentication
|
||||
$this->test_api_authentication_contract();
|
||||
|
||||
// Test error handling
|
||||
$this->test_error_handling_contract();
|
||||
|
||||
// Test security features
|
||||
$this->test_security_features_contract();
|
||||
|
||||
// Generate final report
|
||||
$this->generate_contract_report();
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ CRITICAL ERROR: " . $e->getMessage() . "\n";
|
||||
echo " This is EXPECTED in TDD - implement the OAuth library\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 1: OAuth Library Loading Contract
|
||||
*/
|
||||
private function test_oauth_library_loading()
|
||||
{
|
||||
echo "1. 🧪 Testing OAuth Library Loading Contract...\n";
|
||||
|
||||
try {
|
||||
// EXPECTED TO FAIL: Library should not exist yet
|
||||
$this->CI->load->library('desk_moloni/moloni_oauth');
|
||||
$this->oauth_lib = $this->CI->moloni_oauth;
|
||||
|
||||
$this->assert_true(
|
||||
is_object($this->oauth_lib),
|
||||
"OAuth library must be an object"
|
||||
);
|
||||
|
||||
$this->assert_true(
|
||||
method_exists($this->oauth_lib, 'configure'),
|
||||
"OAuth library must have configure() method"
|
||||
);
|
||||
|
||||
echo " ✅ OAuth library loads correctly\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ EXPECTED FAILURE: " . $e->getMessage() . "\n";
|
||||
echo " 📝 TODO: Implement Moloni_oauth library\n";
|
||||
$this->test_results['oauth_loading'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 2: OAuth Configuration Contract
|
||||
*/
|
||||
private function test_oauth_configuration_contract()
|
||||
{
|
||||
echo "\n2. 🧪 Testing OAuth Configuration Contract...\n";
|
||||
|
||||
$test_config = [
|
||||
'client_id' => 'test_client_id_12345',
|
||||
'client_secret' => 'test_client_secret_67890',
|
||||
'redirect_uri' => 'https://test.descomplicar.pt/oauth/callback',
|
||||
'use_pkce' => true
|
||||
];
|
||||
|
||||
try {
|
||||
// EXPECTED TO FAIL: Method doesn't exist yet
|
||||
$result = $this->oauth_lib->configure(
|
||||
$test_config['client_id'],
|
||||
$test_config['client_secret'],
|
||||
$test_config
|
||||
);
|
||||
|
||||
$this->assert_true(
|
||||
$result === true,
|
||||
"configure() must return true on success"
|
||||
);
|
||||
|
||||
// Test configuration retrieval
|
||||
$stored_config = $this->oauth_lib->get_configuration();
|
||||
|
||||
$this->assert_equals(
|
||||
$test_config['client_id'],
|
||||
$stored_config['client_id'],
|
||||
"Client ID must be stored correctly"
|
||||
);
|
||||
|
||||
echo " ✅ OAuth configuration contract satisfied\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ EXPECTED FAILURE: " . $e->getMessage() . "\n";
|
||||
echo " 📝 TODO: Implement configure() and get_configuration() methods\n";
|
||||
$this->test_results['oauth_config'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 3: Authorization URL Generation Contract
|
||||
*/
|
||||
private function test_authorization_url_contract()
|
||||
{
|
||||
echo "\n3. 🧪 Testing Authorization URL Generation Contract...\n";
|
||||
|
||||
try {
|
||||
// EXPECTED TO FAIL: Method doesn't exist yet
|
||||
$state = 'test_state_' . uniqid();
|
||||
$auth_url = $this->oauth_lib->get_authorization_url($state);
|
||||
|
||||
$this->assert_true(
|
||||
is_string($auth_url),
|
||||
"Authorization URL must be a string"
|
||||
);
|
||||
|
||||
$this->assert_true(
|
||||
filter_var($auth_url, FILTER_VALIDATE_URL) !== false,
|
||||
"Authorization URL must be a valid URL"
|
||||
);
|
||||
|
||||
$this->assert_true(
|
||||
strpos($auth_url, 'https://www.moloni.pt') === 0,
|
||||
"Authorization URL must be from Moloni domain"
|
||||
);
|
||||
|
||||
$this->assert_true(
|
||||
strpos($auth_url, 'client_id=') !== false,
|
||||
"Authorization URL must contain client_id parameter"
|
||||
);
|
||||
|
||||
$this->assert_true(
|
||||
strpos($auth_url, 'state=' . $state) !== false,
|
||||
"Authorization URL must contain correct state parameter"
|
||||
);
|
||||
|
||||
echo " ✅ Authorization URL generation contract satisfied\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ EXPECTED FAILURE: " . $e->getMessage() . "\n";
|
||||
echo " 📝 TODO: Implement get_authorization_url() method\n";
|
||||
$this->test_results['auth_url'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 4: Callback Handling Contract
|
||||
*/
|
||||
private function test_callback_handling_contract()
|
||||
{
|
||||
echo "\n4. 🧪 Testing Callback Handling Contract...\n";
|
||||
|
||||
try {
|
||||
// EXPECTED TO FAIL: Method doesn't exist yet
|
||||
$test_code = 'test_authorization_code_12345';
|
||||
$test_state = 'test_state_67890';
|
||||
|
||||
$result = $this->oauth_lib->handle_callback($test_code, $test_state);
|
||||
|
||||
$this->assert_true(
|
||||
is_array($result) || is_bool($result),
|
||||
"Callback handling must return array or boolean"
|
||||
);
|
||||
|
||||
if (is_array($result)) {
|
||||
$this->assert_true(
|
||||
isset($result['access_token']),
|
||||
"Callback result must contain access_token"
|
||||
);
|
||||
|
||||
$this->assert_true(
|
||||
isset($result['expires_in']),
|
||||
"Callback result must contain expires_in"
|
||||
);
|
||||
}
|
||||
|
||||
echo " ✅ Callback handling contract satisfied\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ EXPECTED FAILURE: " . $e->getMessage() . "\n";
|
||||
echo " 📝 TODO: Implement handle_callback() method\n";
|
||||
$this->test_results['callback'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 5: Token Management Contract
|
||||
*/
|
||||
private function test_token_management_contract()
|
||||
{
|
||||
echo "\n5. 🧪 Testing Token Management Contract...\n";
|
||||
|
||||
try {
|
||||
// EXPECTED TO FAIL: Methods don't exist yet
|
||||
$test_tokens = [
|
||||
'access_token' => 'test_access_token_12345',
|
||||
'refresh_token' => 'test_refresh_token_67890',
|
||||
'expires_in' => 3600,
|
||||
'token_type' => 'Bearer'
|
||||
];
|
||||
|
||||
// Test token storage
|
||||
$save_result = $this->oauth_lib->save_tokens($test_tokens);
|
||||
|
||||
$this->assert_true(
|
||||
$save_result === true,
|
||||
"save_tokens() must return true on success"
|
||||
);
|
||||
|
||||
// Test token retrieval
|
||||
$stored_token = $this->oauth_lib->get_access_token();
|
||||
|
||||
$this->assert_equals(
|
||||
$test_tokens['access_token'],
|
||||
$stored_token,
|
||||
"Access token must be retrieved correctly"
|
||||
);
|
||||
|
||||
// Test token validation
|
||||
$is_valid = $this->oauth_lib->is_token_valid();
|
||||
|
||||
$this->assert_true(
|
||||
is_bool($is_valid),
|
||||
"is_token_valid() must return boolean"
|
||||
);
|
||||
|
||||
echo " ✅ Token management contract satisfied\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ EXPECTED FAILURE: " . $e->getMessage() . "\n";
|
||||
echo " 📝 TODO: Implement token management methods\n";
|
||||
$this->test_results['token_mgmt'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 6: Token Refresh Contract
|
||||
*/
|
||||
private function test_token_refresh_contract()
|
||||
{
|
||||
echo "\n6. 🧪 Testing Token Refresh Contract...\n";
|
||||
|
||||
try {
|
||||
// EXPECTED TO FAIL: Method doesn't exist yet
|
||||
$refresh_result = $this->oauth_lib->refresh_access_token();
|
||||
|
||||
$this->assert_true(
|
||||
is_array($refresh_result) || is_bool($refresh_result),
|
||||
"Token refresh must return array or boolean"
|
||||
);
|
||||
|
||||
if (is_array($refresh_result)) {
|
||||
$this->assert_true(
|
||||
isset($refresh_result['access_token']),
|
||||
"Refresh result must contain new access_token"
|
||||
);
|
||||
}
|
||||
|
||||
echo " ✅ Token refresh contract satisfied\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ EXPECTED FAILURE: " . $e->getMessage() . "\n";
|
||||
echo " 📝 TODO: Implement refresh_access_token() method\n";
|
||||
$this->test_results['token_refresh'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 7: API Authentication Contract
|
||||
*/
|
||||
private function test_api_authentication_contract()
|
||||
{
|
||||
echo "\n7. 🧪 Testing API Authentication Contract...\n";
|
||||
|
||||
try {
|
||||
// EXPECTED TO FAIL: Method doesn't exist yet
|
||||
$auth_headers = $this->oauth_lib->get_auth_headers();
|
||||
|
||||
$this->assert_true(
|
||||
is_array($auth_headers),
|
||||
"Auth headers must be an array"
|
||||
);
|
||||
|
||||
$this->assert_true(
|
||||
isset($auth_headers['Authorization']),
|
||||
"Auth headers must contain Authorization header"
|
||||
);
|
||||
|
||||
$this->assert_true(
|
||||
strpos($auth_headers['Authorization'], 'Bearer ') === 0,
|
||||
"Authorization header must be Bearer token format"
|
||||
);
|
||||
|
||||
echo " ✅ API authentication contract satisfied\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ EXPECTED FAILURE: " . $e->getMessage() . "\n";
|
||||
echo " 📝 TODO: Implement get_auth_headers() method\n";
|
||||
$this->test_results['api_auth'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 8: Error Handling Contract
|
||||
*/
|
||||
private function test_error_handling_contract()
|
||||
{
|
||||
echo "\n8. 🧪 Testing Error Handling Contract...\n";
|
||||
|
||||
try {
|
||||
// EXPECTED TO FAIL: Method doesn't exist yet
|
||||
|
||||
// Test invalid configuration
|
||||
$invalid_result = $this->oauth_lib->configure('', '');
|
||||
|
||||
$this->assert_true(
|
||||
$invalid_result === false,
|
||||
"Invalid configuration must return false"
|
||||
);
|
||||
|
||||
// Test error reporting
|
||||
$last_error = $this->oauth_lib->get_last_error();
|
||||
|
||||
$this->assert_true(
|
||||
is_string($last_error) || is_array($last_error),
|
||||
"Last error must be string or array"
|
||||
);
|
||||
|
||||
echo " ✅ Error handling contract satisfied\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ EXPECTED FAILURE: " . $e->getMessage() . "\n";
|
||||
echo " 📝 TODO: Implement error handling methods\n";
|
||||
$this->test_results['error_handling'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 9: Security Features Contract
|
||||
*/
|
||||
private function test_security_features_contract()
|
||||
{
|
||||
echo "\n9. 🧪 Testing Security Features Contract...\n";
|
||||
|
||||
try {
|
||||
// EXPECTED TO FAIL: Methods don't exist yet
|
||||
|
||||
// Test PKCE support
|
||||
$pkce_supported = $this->oauth_lib->supports_pkce();
|
||||
|
||||
$this->assert_true(
|
||||
is_bool($pkce_supported),
|
||||
"PKCE support check must return boolean"
|
||||
);
|
||||
|
||||
// Test state validation
|
||||
$state_validation = $this->oauth_lib->validate_state('test_state');
|
||||
|
||||
$this->assert_true(
|
||||
is_bool($state_validation),
|
||||
"State validation must return boolean"
|
||||
);
|
||||
|
||||
// Test token encryption
|
||||
$tokens_encrypted = $this->oauth_lib->are_tokens_encrypted();
|
||||
|
||||
$this->assert_true(
|
||||
is_bool($tokens_encrypted),
|
||||
"Token encryption check must return boolean"
|
||||
);
|
||||
|
||||
echo " ✅ Security features contract satisfied\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo " ❌ EXPECTED FAILURE: " . $e->getMessage() . "\n";
|
||||
echo " 📝 TODO: Implement security feature methods\n";
|
||||
$this->test_results['security'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Contract Test Report
|
||||
*/
|
||||
private function generate_contract_report()
|
||||
{
|
||||
$execution_time = microtime(true) - $this->start_time;
|
||||
|
||||
echo "\n" . str_repeat("=", 80) . "\n";
|
||||
echo "MOLONI OAUTH CONTRACT TEST REPORT\n";
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
|
||||
$passed_tests = array_filter($this->test_results, function($result) {
|
||||
return $result === true;
|
||||
});
|
||||
|
||||
$failed_tests = array_filter($this->test_results, function($result) {
|
||||
return $result === false;
|
||||
});
|
||||
|
||||
echo "Execution Time: " . number_format($execution_time, 2) . "s\n";
|
||||
echo "Tests Passed: " . count($passed_tests) . "\n";
|
||||
echo "Tests Failed: " . count($failed_tests) . " (EXPECTED in TDD)\n";
|
||||
|
||||
if (count($failed_tests) > 0) {
|
||||
echo "\n🔴 TDD STATUS: TESTS FAILING AS EXPECTED\n";
|
||||
echo "Next Step: Implement Moloni_oauth library to make tests pass\n";
|
||||
|
||||
echo "\nFailed Test Categories:\n";
|
||||
foreach ($failed_tests as $test => $result) {
|
||||
echo " ❌ " . ucwords(str_replace('_', ' ', $test)) . "\n";
|
||||
}
|
||||
} else {
|
||||
echo "\n🟢 ALL TESTS PASSING\n";
|
||||
echo "OAuth implementation appears to be complete\n";
|
||||
}
|
||||
|
||||
echo "\n📋 IMPLEMENTATION REQUIREMENTS:\n";
|
||||
echo " 1. Create libraries/Moloni_oauth.php\n";
|
||||
echo " 2. Implement class Moloni_oauth with required methods\n";
|
||||
echo " 3. Support OAuth 2.0 with PKCE\n";
|
||||
echo " 4. Secure token storage with encryption\n";
|
||||
echo " 5. Comprehensive error handling\n";
|
||||
echo " 6. State validation for security\n";
|
||||
|
||||
// Save test results
|
||||
$this->save_contract_results();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save contract test results
|
||||
*/
|
||||
private function save_contract_results()
|
||||
{
|
||||
$results = [
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'test_type' => 'oauth_contract',
|
||||
'status' => count(array_filter($this->test_results)) > 0 ? 'passing' : 'failing',
|
||||
'results' => $this->test_results,
|
||||
'execution_time' => microtime(true) - $this->start_time
|
||||
];
|
||||
|
||||
$reports_dir = __DIR__ . '/../reports';
|
||||
if (!is_dir($reports_dir)) {
|
||||
mkdir($reports_dir, 0755, true);
|
||||
}
|
||||
|
||||
$report_file = $reports_dir . '/oauth_contract_test_' . date('Y-m-d_H-i-s') . '.json';
|
||||
file_put_contents($report_file, json_encode($results, JSON_PRETTY_PRINT));
|
||||
|
||||
echo "\n📄 Contract test results saved to: {$report_file}\n";
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// HELPER ASSERTION METHODS
|
||||
// ========================================================================
|
||||
|
||||
private function assert_true($condition, $message)
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new Exception("Assertion failed: {$message}");
|
||||
}
|
||||
}
|
||||
|
||||
private function assert_equals($expected, $actual, $message)
|
||||
{
|
||||
if ($expected !== $actual) {
|
||||
throw new Exception("Assertion failed: {$message}. Expected: {$expected}, Actual: {$actual}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the contract tests if called directly
|
||||
if (basename(__FILE__) === basename($_SERVER['SCRIPT_NAME'])) {
|
||||
$test = new Test_Moloni_OAuth();
|
||||
$test->run_all_tests();
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Contract Test: Moloni OAuth API (Standalone)
|
||||
*
|
||||
* Tests the OAuth 2.0 integration contract with Moloni API
|
||||
* These tests MUST FAIL initially (TDD) before implementing the OAuth library
|
||||
*
|
||||
* @package DeskMoloni
|
||||
* @subpackage Tests\Contract
|
||||
* @version 3.0.0
|
||||
* @author Descomplicar®
|
||||
*/
|
||||
|
||||
// Define constants for testing
|
||||
define('BASEPATH', true);
|
||||
define('ENVIRONMENT', 'testing');
|
||||
|
||||
echo "\n" . str_repeat("=", 80) . "\n";
|
||||
echo "MOLONI OAUTH API CONTRACT TESTS (Standalone)\n";
|
||||
echo "TDD: These tests MUST FAIL before implementation\n";
|
||||
echo str_repeat("=", 80) . "\n\n";
|
||||
|
||||
$test_results = [];
|
||||
$start_time = microtime(true);
|
||||
|
||||
// Test 1: OAuth Library File Existence
|
||||
echo "1. 🧪 Testing OAuth Library File Existence...\n";
|
||||
$oauth_file = __DIR__ . '/../../libraries/Moloni_oauth.php';
|
||||
|
||||
if (file_exists($oauth_file)) {
|
||||
echo " ✅ Moloni_oauth.php file exists\n";
|
||||
|
||||
// Test class definition
|
||||
$content = file_get_contents($oauth_file);
|
||||
if (strpos($content, 'class Moloni_oauth') !== false) {
|
||||
echo " ✅ Moloni_oauth class is defined\n";
|
||||
$test_results['class_exists'] = true;
|
||||
} else {
|
||||
echo " ❌ EXPECTED FAILURE: Moloni_oauth class not found\n";
|
||||
$test_results['class_exists'] = false;
|
||||
}
|
||||
} else {
|
||||
echo " ❌ EXPECTED FAILURE: Moloni_oauth.php file does not exist\n";
|
||||
echo " 📝 TODO: Create libraries/Moloni_oauth.php\n";
|
||||
$test_results['file_exists'] = false;
|
||||
}
|
||||
|
||||
// Test 2: Required Methods Contract
|
||||
echo "\n2. 🧪 Testing Required Methods Contract...\n";
|
||||
|
||||
$required_methods = [
|
||||
'configure',
|
||||
'get_authorization_url',
|
||||
'handle_callback',
|
||||
'save_tokens',
|
||||
'get_access_token',
|
||||
'is_token_valid',
|
||||
'refresh_access_token',
|
||||
'get_auth_headers',
|
||||
'get_last_error',
|
||||
'supports_pkce',
|
||||
'validate_state',
|
||||
'are_tokens_encrypted'
|
||||
];
|
||||
|
||||
if (file_exists($oauth_file)) {
|
||||
$content = file_get_contents($oauth_file);
|
||||
$methods_found = 0;
|
||||
|
||||
foreach ($required_methods as $method) {
|
||||
if (strpos($content, "function {$method}") !== false ||
|
||||
strpos($content, "function {$method}(") !== false) {
|
||||
echo " ✅ Method {$method}() found\n";
|
||||
$methods_found++;
|
||||
} else {
|
||||
echo " ❌ Method {$method}() missing\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['methods_complete'] = ($methods_found === count($required_methods));
|
||||
echo " 📊 Methods found: {$methods_found}/" . count($required_methods) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test methods - file does not exist\n";
|
||||
$test_results['methods_complete'] = false;
|
||||
}
|
||||
|
||||
// Test 3: OAuth Endpoints Configuration
|
||||
echo "\n3. 🧪 Testing OAuth Endpoints Configuration...\n";
|
||||
|
||||
$expected_endpoints = [
|
||||
'auth_url' => 'https://www.moloni.pt',
|
||||
'token_url' => 'https://api.moloni.pt'
|
||||
];
|
||||
|
||||
if (file_exists($oauth_file)) {
|
||||
$content = file_get_contents($oauth_file);
|
||||
$endpoints_found = 0;
|
||||
|
||||
foreach ($expected_endpoints as $endpoint => $domain) {
|
||||
if (strpos($content, $domain) !== false) {
|
||||
echo " ✅ {$endpoint} contains correct domain: {$domain}\n";
|
||||
$endpoints_found++;
|
||||
} else {
|
||||
echo " ❌ {$endpoint} missing or incorrect domain\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['endpoints_configured'] = ($endpoints_found === count($expected_endpoints));
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test endpoints - file does not exist\n";
|
||||
$test_results['endpoints_configured'] = false;
|
||||
}
|
||||
|
||||
// Test 4: Security Features Contract
|
||||
echo "\n4. 🧪 Testing Security Features Contract...\n";
|
||||
|
||||
$security_features = [
|
||||
'PKCE' => ['pkce', 'code_verifier', 'code_challenge'],
|
||||
'State validation' => ['state', 'csrf'],
|
||||
'Token encryption' => ['encrypt', 'decrypt', 'token_manager'],
|
||||
'Rate limiting' => ['rate_limit', 'throttle', 'request_count']
|
||||
];
|
||||
|
||||
if (file_exists($oauth_file)) {
|
||||
$content = file_get_contents($oauth_file);
|
||||
$security_score = 0;
|
||||
|
||||
foreach ($security_features as $feature => $keywords) {
|
||||
$feature_found = false;
|
||||
foreach ($keywords as $keyword) {
|
||||
if (stripos($content, $keyword) !== false) {
|
||||
$feature_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($feature_found) {
|
||||
echo " ✅ {$feature} implementation found\n";
|
||||
$security_score++;
|
||||
} else {
|
||||
echo " ❌ {$feature} implementation missing\n";
|
||||
}
|
||||
}
|
||||
|
||||
$test_results['security_features'] = ($security_score >= 3);
|
||||
echo " 📊 Security features: {$security_score}/" . count($security_features) . "\n";
|
||||
|
||||
} else {
|
||||
echo " ❌ Cannot test security features - file does not exist\n";
|
||||
$test_results['security_features'] = false;
|
||||
}
|
||||
|
||||
// Test 5: Database Integration Contract
|
||||
echo "\n5. 🧪 Testing Database Integration Contract...\n";
|
||||
|
||||
$config_model_file = __DIR__ . '/../../models/Desk_moloni_config_model.php';
|
||||
|
||||
if (file_exists($config_model_file)) {
|
||||
echo " ✅ Config model exists for OAuth storage\n";
|
||||
|
||||
$content = file_get_contents($config_model_file);
|
||||
if (strpos($content, 'oauth') !== false) {
|
||||
echo " ✅ Config model supports OAuth settings\n";
|
||||
$test_results['database_integration'] = true;
|
||||
} else {
|
||||
echo " ⚠️ Config model may not support OAuth settings\n";
|
||||
$test_results['database_integration'] = false;
|
||||
}
|
||||
} else {
|
||||
echo " ❌ Config model missing for OAuth storage\n";
|
||||
$test_results['database_integration'] = false;
|
||||
}
|
||||
|
||||
// Test 6: Token Manager Integration
|
||||
echo "\n6. 🧪 Testing Token Manager Integration...\n";
|
||||
|
||||
$token_manager_file = __DIR__ . '/../../libraries/TokenManager.php';
|
||||
|
||||
if (file_exists($token_manager_file)) {
|
||||
echo " ✅ TokenManager library exists\n";
|
||||
|
||||
$content = file_get_contents($token_manager_file);
|
||||
if (strpos($content, 'save_tokens') !== false ||
|
||||
strpos($content, 'get_token') !== false) {
|
||||
echo " ✅ TokenManager has token management methods\n";
|
||||
$test_results['token_manager_integration'] = true;
|
||||
} else {
|
||||
echo " ❌ TokenManager missing required methods\n";
|
||||
$test_results['token_manager_integration'] = false;
|
||||
}
|
||||
} else {
|
||||
echo " ❌ TokenManager library missing\n";
|
||||
$test_results['token_manager_integration'] = false;
|
||||
}
|
||||
|
||||
// Generate Final Report
|
||||
$execution_time = microtime(true) - $start_time;
|
||||
|
||||
echo "\n" . str_repeat("=", 80) . "\n";
|
||||
echo "MOLONI OAUTH CONTRACT TEST REPORT\n";
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
|
||||
$passed_tests = array_filter($test_results, function($result) {
|
||||
return $result === true;
|
||||
});
|
||||
|
||||
$failed_tests = array_filter($test_results, function($result) {
|
||||
return $result === false;
|
||||
});
|
||||
|
||||
echo "Execution Time: " . number_format($execution_time, 2) . "s\n";
|
||||
echo "Tests Passed: " . count($passed_tests) . "\n";
|
||||
echo "Tests Failed: " . count($failed_tests) . " (EXPECTED in TDD)\n";
|
||||
|
||||
if (count($failed_tests) > 0) {
|
||||
echo "\n🔴 TDD STATUS: TESTS FAILING AS EXPECTED\n";
|
||||
echo "Next Step: Implement Moloni_oauth library to make tests pass\n";
|
||||
|
||||
echo "\nFailed Test Categories:\n";
|
||||
foreach ($test_results as $test => $result) {
|
||||
if ($result === false) {
|
||||
echo " ❌ " . ucwords(str_replace('_', ' ', $test)) . "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo "\n🟢 ALL TESTS PASSING\n";
|
||||
echo "OAuth implementation appears to be complete\n";
|
||||
}
|
||||
|
||||
echo "\n📋 IMPLEMENTATION REQUIREMENTS:\n";
|
||||
echo " 1. Create libraries/Moloni_oauth.php with class Moloni_oauth\n";
|
||||
echo " 2. Implement all required methods listed above\n";
|
||||
echo " 3. Support OAuth 2.0 with PKCE for security\n";
|
||||
echo " 4. Integrate with TokenManager for secure storage\n";
|
||||
echo " 5. Use Config model for persistent settings\n";
|
||||
echo " 6. Implement comprehensive error handling\n";
|
||||
echo " 7. Add rate limiting and security features\n";
|
||||
|
||||
echo "\n🎯 SUCCESS CRITERIA:\n";
|
||||
echo " - All contract tests must pass\n";
|
||||
echo " - OAuth flow must work with real Moloni API\n";
|
||||
echo " - Tokens must be securely encrypted\n";
|
||||
echo " - PKCE must be implemented for security\n";
|
||||
echo " - Proper error handling and logging\n";
|
||||
|
||||
// Save results
|
||||
$reports_dir = __DIR__ . '/../reports';
|
||||
if (!is_dir($reports_dir)) {
|
||||
mkdir($reports_dir, 0755, true);
|
||||
}
|
||||
|
||||
$report_file = $reports_dir . '/oauth_contract_test_' . date('Y-m-d_H-i-s') . '.json';
|
||||
file_put_contents($report_file, json_encode([
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'test_type' => 'oauth_contract_standalone',
|
||||
'status' => count($failed_tests) > 0 ? 'failing' : 'passing',
|
||||
'results' => $test_results,
|
||||
'execution_time' => $execution_time,
|
||||
'tdd_status' => 'Tests failing as expected - ready for implementation'
|
||||
], JSON_PRETTY_PRINT));
|
||||
|
||||
echo "\n📄 Contract test results saved to: {$report_file}\n";
|
||||
echo str_repeat("=", 80) . "\n";
|
||||
Reference in New Issue
Block a user