/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ CI = &get_instance(); // Load API client $this->CI->load->library('desk_moloni/moloniapiclient'); $this->api_client = $this->CI->moloniapiclient; // Load contract specification $this->loadContractSpec(); // Test company ID $this->test_company_id = 12345; // Set up authentication for contract tests $this->setupContractAuth(); } /** * Load OpenAPI contract specification */ private function loadContractSpec() { $spec_file = FCPATH . '../specs/001-desk-moloni-integration/contracts/moloni-api.yaml'; if (file_exists($spec_file)) { $this->contract_spec = yaml_parse_file($spec_file); } else { // Fallback to embedded spec for testing $this->contract_spec = $this->getEmbeddedSpec(); } } /** * Set up authentication for contract testing */ private function setupContractAuth() { $this->CI->load->library('desk_moloni/molonioauth'); $this->CI->load->library('desk_moloni/tokenmanager'); // Configure OAuth $this->CI->molonioauth->configure('test_client_id', 'test_client_secret'); // Add mock tokens $this->CI->tokenmanager->save_tokens([ 'access_token' => 'contract_test_token', 'expires_in' => 3600 ]); } /** * Test OAuth 2.0 Token Exchange endpoint * POST /oauth2/token */ public function testOAuthTokenExchangeContract() { $endpoint_spec = $this->getEndpointSpec('post', '/oauth2/token'); // Test authorization_code grant $auth_code_data = [ 'grant_type' => 'authorization_code', 'code' => 'test_auth_code', 'client_id' => 'test_client_id', 'client_secret' => 'test_client_secret', 'redirect_uri' => 'https://test.com/callback' ]; $this->validateRequestSchema($auth_code_data, $endpoint_spec['requestBody']); // Test refresh_token grant $refresh_token_data = [ 'grant_type' => 'refresh_token', 'refresh_token' => 'test_refresh_token', 'client_id' => 'test_client_id', 'client_secret' => 'test_client_secret' ]; $this->validateRequestSchema($refresh_token_data, $endpoint_spec['requestBody']); // Validate response schema $mock_response = [ 'access_token' => 'access_token_value', 'token_type' => 'Bearer', 'expires_in' => 3600, 'refresh_token' => 'refresh_token_value', 'scope' => 'read write' ]; $this->validateResponseSchema($mock_response, $endpoint_spec['responses']['200']); } /** * Test Customers List endpoint * GET /customers */ public function testCustomersListContract() { $endpoint_spec = $this->getEndpointSpec('get', '/customers'); // Test required parameters $params = [ 'company_id' => $this->test_company_id, 'qty' => 50, 'offset' => 0 ]; $this->validateQueryParameters($params, $endpoint_spec['parameters']); // Validate response structure $mock_customers = [ [ 'customer_id' => 1, 'number' => 'CUST001', 'name' => 'Test Customer', 'vat' => '123456789', 'email' => 'test@example.com', 'phone' => '+351912345678', 'address' => 'Test Address', 'zip_code' => '4000-000', 'city' => 'Porto', 'country_id' => 1, 'website' => 'https://example.com', 'notes' => 'Test notes' ] ]; $this->validateResponseSchema($mock_customers, $endpoint_spec['responses']['200']); } /** * Test Customer Creation endpoint * POST /customers */ public function testCustomerCreateContract() { $endpoint_spec = $this->getEndpointSpec('post', '/customers'); // Test valid customer creation data $customer_data = [ 'company_id' => $this->test_company_id, 'name' => 'New Test Customer', 'vat' => '987654321', 'email' => 'newcustomer@example.com', 'phone' => '+351987654321', 'address' => 'New Address', 'zip_code' => '1000-000', 'city' => 'Lisboa', 'country_id' => 1, 'website' => 'https://newcustomer.com', 'notes' => 'New customer notes' ]; $this->validateRequestSchema($customer_data, $endpoint_spec['requestBody']); // Test required fields validation $required_fields = ['company_id', 'name', 'vat']; foreach ($required_fields as $field) { $this->assertArrayHasKey($field, $customer_data, "Required field '{$field}' missing"); } // Test response schema $mock_response = $customer_data; $mock_response['customer_id'] = 123; $mock_response['number'] = 'CUST123'; $this->validateResponseSchema($mock_response, $endpoint_spec['responses']['201']); } /** * Test Customer Get endpoint * GET /customers/{customer_id} */ public function testCustomerGetContract() { $endpoint_spec = $this->getEndpointSpec('get', '/customers/{customer_id}'); // Test path parameters $customer_id = 123; $this->assertIsInt($customer_id); // Test query parameters $params = [ 'company_id' => $this->test_company_id ]; $this->validateQueryParameters($params, $endpoint_spec['parameters']); // Test response schema $mock_customer = [ 'customer_id' => $customer_id, 'number' => 'CUST123', 'name' => 'Retrieved Customer', 'vat' => '123456789', 'email' => 'customer@example.com', 'phone' => '+351912345678', 'address' => 'Customer Address', 'zip_code' => '4000-000', 'city' => 'Porto', 'country_id' => 1, 'website' => 'https://customer.com', 'notes' => 'Customer notes' ]; $this->validateResponseSchema($mock_customer, $endpoint_spec['responses']['200']); } /** * Test Customer Update endpoint * PUT /customers/{customer_id} */ public function testCustomerUpdateContract() { $endpoint_spec = $this->getEndpointSpec('put', '/customers/{customer_id}'); // Test update data $update_data = [ 'customer_id' => 123, 'company_id' => $this->test_company_id, 'name' => 'Updated Customer Name', 'email' => 'updated@example.com', 'phone' => '+351999888777', 'address' => 'Updated Address', 'city' => 'Braga', 'notes' => 'Updated notes' ]; $this->validateRequestSchema($update_data, $endpoint_spec['requestBody']); // Test required fields for update $required_fields = ['customer_id', 'company_id']; foreach ($required_fields as $field) { $this->assertArrayHasKey($field, $update_data, "Required field '{$field}' missing"); } // Test response schema $this->validateResponseSchema($update_data, $endpoint_spec['responses']['200']); } /** * Test Products List endpoint * GET /products */ public function testProductsListContract() { $endpoint_spec = $this->getEndpointSpec('get', '/products'); // Test parameters $params = [ 'company_id' => $this->test_company_id ]; $this->validateQueryParameters($params, $endpoint_spec['parameters']); // Test response schema $mock_products = [ [ 'product_id' => 1, 'name' => 'Test Product', 'summary' => 'Product description', 'reference' => 'PROD001', 'price' => 99.99, 'unit_id' => 1, 'has_stock' => 0, 'stock' => 0.0, 'minimum_stock' => 0.0 ] ]; $this->validateResponseSchema($mock_products, $endpoint_spec['responses']['200']); } /** * Test Product Creation endpoint * POST /products */ public function testProductCreateContract() { $endpoint_spec = $this->getEndpointSpec('post', '/products'); // Test product creation data $product_data = [ 'company_id' => $this->test_company_id, 'name' => 'New Product', 'summary' => 'New product description', 'reference' => 'NEWPROD001', 'price' => 149.99, 'unit_id' => 1, 'has_stock' => 0 ]; $this->validateRequestSchema($product_data, $endpoint_spec['requestBody']); // Test required fields $required_fields = ['company_id', 'name', 'price']; foreach ($required_fields as $field) { $this->assertArrayHasKey($field, $product_data, "Required field '{$field}' missing"); } // Test price is numeric $this->assertIsNumeric($product_data['price']); // Test response schema $mock_response = $product_data; $mock_response['product_id'] = 456; $this->validateResponseSchema($mock_response, $endpoint_spec['responses']['201']); } /** * Test Invoice Creation endpoint * POST /invoices */ public function testInvoiceCreateContract() { $endpoint_spec = $this->getEndpointSpec('post', '/invoices'); // Test invoice creation data $invoice_data = [ 'company_id' => $this->test_company_id, 'customer_id' => 123, 'date' => date('Y-m-d'), 'expiration_date' => date('Y-m-d', strtotime('+30 days')), 'document_set_id' => 1, 'products' => [ [ 'product_id' => 1, 'name' => 'Invoice Product', 'summary' => 'Product for invoice', 'qty' => 2.0, 'price' => 50.0, 'discount' => 0.0, 'tax' => 23.0 ] ], 'notes' => 'Invoice notes' ]; $this->validateRequestSchema($invoice_data, $endpoint_spec['requestBody']); // Test required fields $required_fields = ['company_id', 'customer_id', 'date', 'products']; foreach ($required_fields as $field) { $this->assertArrayHasKey($field, $invoice_data, "Required field '{$field}' missing"); } // Test products array $this->assertIsArray($invoice_data['products']); $this->assertNotEmpty($invoice_data['products']); // Test product structure $product = $invoice_data['products'][0]; $product_required_fields = ['product_id', 'name', 'qty', 'price']; foreach ($product_required_fields as $field) { $this->assertArrayHasKey($field, $product, "Product required field '{$field}' missing"); } // Test response schema $mock_response = [ 'document_id' => 789, 'number' => 'INV001/2025', 'date' => $invoice_data['date'], 'customer_id' => $invoice_data['customer_id'], 'net_value' => 100.0, 'tax_value' => 23.0, 'gross_value' => 123.0, 'status' => 1, 'products' => [ [ 'product_id' => 1, 'name' => 'Invoice Product', 'summary' => 'Product for invoice', 'qty' => 2.0, 'price' => 50.0, 'discount' => 0.0, 'tax' => 23.0 ] ] ]; $this->validateResponseSchema($mock_response, $endpoint_spec['responses']['201']); } /** * Test Invoice PDF endpoint * GET /invoices/{invoice_id}/getPDF */ public function testInvoicePdfContract() { $endpoint_spec = $this->getEndpointSpec('get', '/invoices/{invoice_id}/getPDF'); // Test path parameters $invoice_id = 789; $this->assertIsInt($invoice_id); // Test query parameters $params = [ 'company_id' => $this->test_company_id ]; $this->validateQueryParameters($params, $endpoint_spec['parameters']); // For PDF response, we test that it would return binary data // In actual implementation, this would be validated differently $this->assertTrue(true, 'PDF endpoint contract structure verified'); } /** * Test API base URL and versioning */ public function testApiBaseUrlContract() { $expected_base_url = 'https://api.moloni.pt/v1'; $spec_servers = $this->contract_spec['servers']; $this->assertNotEmpty($spec_servers); $this->assertEquals($expected_base_url, $spec_servers[0]['url']); } /** * Test OAuth 2.0 security scheme */ public function testOAuth2SecurityScheme() { $security_schemes = $this->contract_spec['components']['securitySchemes']; $this->assertArrayHasKey('oauth2', $security_schemes); $oauth2_scheme = $security_schemes['oauth2']; $this->assertEquals('oauth2', $oauth2_scheme['type']); $this->assertArrayHasKey('flows', $oauth2_scheme); $this->assertArrayHasKey('authorizationCode', $oauth2_scheme['flows']); $auth_code_flow = $oauth2_scheme['flows']['authorizationCode']; $this->assertEquals('https://api.moloni.pt/v1/oauth2/authorize', $auth_code_flow['authorizationUrl']); $this->assertEquals('https://api.moloni.pt/v1/oauth2/token', $auth_code_flow['tokenUrl']); } /** * Test all schema definitions exist */ public function testSchemaDefinitions() { $schemas = $this->contract_spec['components']['schemas']; $required_schemas = [ 'TokenResponse', 'Customer', 'CustomerCreate', 'CustomerUpdate', 'Product', 'ProductCreate', 'Invoice', 'InvoiceCreate', 'InvoiceProduct', 'InvoiceProductCreate' ]; foreach ($required_schemas as $schema) { $this->assertArrayHasKey($schema, $schemas, "Schema '{$schema}' not defined"); $this->assertArrayHasKey('type', $schemas[$schema], "Schema '{$schema}' missing type"); $this->assertArrayHasKey('properties', $schemas[$schema], "Schema '{$schema}' missing properties"); } } /** * Test API client implementation matches contract */ public function testApiClientMethodsMatchContract() { $paths = $this->contract_spec['paths']; // Verify API client has methods for all endpoints $this->assertTrue(method_exists($this->api_client, 'list_customers')); $this->assertTrue(method_exists($this->api_client, 'get_customer')); $this->assertTrue(method_exists($this->api_client, 'create_customer')); $this->assertTrue(method_exists($this->api_client, 'update_customer')); $this->assertTrue(method_exists($this->api_client, 'list_products')); $this->assertTrue(method_exists($this->api_client, 'create_product')); $this->assertTrue(method_exists($this->api_client, 'create_invoice')); $this->assertTrue(method_exists($this->api_client, 'get_invoice_pdf')); } /** * Validate request data against schema */ private function validateRequestSchema($data, $request_body_spec) { if (!isset($request_body_spec['content']['application/json']['schema'])) { return; // No schema to validate against } $schema_ref = $request_body_spec['content']['application/json']['schema']['$ref'] ?? null; if ($schema_ref) { $schema_name = str_replace('#/components/schemas/', '', $schema_ref); $schema = $this->contract_spec['components']['schemas'][$schema_name]; $this->validateDataAgainstSchema($data, $schema); } } /** * Validate response data against schema */ private function validateResponseSchema($data, $response_spec) { if (!isset($response_spec['content']['application/json']['schema'])) { return; // No schema to validate against } $schema = $response_spec['content']['application/json']['schema']; if (isset($schema['type']) && $schema['type'] === 'array') { $this->assertIsArray($data); if (isset($schema['items']['$ref'])) { $item_schema_name = str_replace('#/components/schemas/', '', $schema['items']['$ref']); $item_schema = $this->contract_spec['components']['schemas'][$item_schema_name]; if (!empty($data)) { $this->validateDataAgainstSchema($data[0], $item_schema); } } } elseif (isset($schema['$ref'])) { $schema_name = str_replace('#/components/schemas/', '', $schema['$ref']); $schema_def = $this->contract_spec['components']['schemas'][$schema_name]; $this->validateDataAgainstSchema($data, $schema_def); } } /** * Validate query parameters */ private function validateQueryParameters($params, $parameters_spec) { foreach ($parameters_spec as $param_spec) { if ($param_spec['in'] === 'query' && isset($param_spec['required']) && $param_spec['required']) { $param_name = $param_spec['name']; $this->assertArrayHasKey($param_name, $params, "Required query parameter '{$param_name}' missing"); } } } /** * Validate data against schema definition */ private function validateDataAgainstSchema($data, $schema) { $this->assertIsArray($data); if (isset($schema['required'])) { foreach ($schema['required'] as $required_field) { $this->assertArrayHasKey($required_field, $data, "Required field '{$required_field}' missing"); } } if (isset($schema['properties'])) { foreach ($data as $field => $value) { if (isset($schema['properties'][$field])) { $field_schema = $schema['properties'][$field]; $this->validateFieldType($value, $field_schema, $field); } } } } /** * Validate field type against schema */ private function validateFieldType($value, $field_schema, $field_name) { if (!isset($field_schema['type'])) { return; // No type constraint } switch ($field_schema['type']) { case 'string': $this->assertIsString($value, "Field '{$field_name}' should be string"); break; case 'integer': $this->assertIsInt($value, "Field '{$field_name}' should be integer"); break; case 'number': $this->assertIsNumeric($value, "Field '{$field_name}' should be numeric"); break; case 'array': $this->assertIsArray($value, "Field '{$field_name}' should be array"); break; } // Validate format if specified if (isset($field_schema['format'])) { switch ($field_schema['format']) { case 'date': $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2}$/', $value, "Field '{$field_name}' should be valid date"); break; case 'email': $this->assertFilter($value, FILTER_VALIDATE_EMAIL, "Field '{$field_name}' should be valid email"); break; } } } /** * Get endpoint specification from contract */ private function getEndpointSpec($method, $path) { $paths = $this->contract_spec['paths']; $this->assertArrayHasKey($path, $paths, "Endpoint '{$path}' not found in contract"); $this->assertArrayHasKey($method, $paths[$path], "Method '{$method}' not found for endpoint '{$path}'"); return $paths[$path][$method]; } /** * Get embedded specification for testing when file is not available */ private function getEmbeddedSpec() { return [ 'openapi' => '3.0.3', 'info' => [ 'title' => 'Moloni API Integration Contract', 'version' => '3.0.0' ], 'servers' => [ ['url' => 'https://api.moloni.pt/v1'] ], 'paths' => [ '/oauth2/token' => [ 'post' => [ 'operationId' => 'exchangeToken', 'requestBody' => [ 'content' => [ 'application/x-www-form-urlencoded' => [ 'schema' => [ 'type' => 'object', 'properties' => [ 'grant_type' => ['type' => 'string'], 'code' => ['type' => 'string'], 'client_id' => ['type' => 'string'], 'client_secret' => ['type' => 'string'] ] ] ] ] ], 'responses' => [ '200' => [ 'content' => [ 'application/json' => [ 'schema' => ['$ref' => '#/components/schemas/TokenResponse'] ] ] ] ] ] ], '/customers' => [ 'get' => [ 'operationId' => 'listCustomers', 'parameters' => [ [ 'name' => 'company_id', 'in' => 'query', 'required' => true, 'schema' => ['type' => 'integer'] ] ], 'responses' => [ '200' => [ 'content' => [ 'application/json' => [ 'schema' => [ 'type' => 'array', 'items' => ['$ref' => '#/components/schemas/Customer'] ] ] ] ] ] ], 'post' => [ 'operationId' => 'createCustomer', 'requestBody' => [ 'content' => [ 'application/json' => [ 'schema' => ['$ref' => '#/components/schemas/CustomerCreate'] ] ] ], 'responses' => [ '201' => [ 'content' => [ 'application/json' => [ 'schema' => ['$ref' => '#/components/schemas/Customer'] ] ] ] ] ] ] // Additional endpoints would be defined here... ], 'components' => [ 'securitySchemes' => [ 'oauth2' => [ 'type' => 'oauth2', 'flows' => [ 'authorizationCode' => [ 'authorizationUrl' => 'https://api.moloni.pt/v1/oauth2/authorize', 'tokenUrl' => 'https://api.moloni.pt/v1/oauth2/token' ] ] ] ], 'schemas' => [ 'TokenResponse' => [ 'type' => 'object', 'properties' => [ 'access_token' => ['type' => 'string'], 'token_type' => ['type' => 'string'], 'expires_in' => ['type' => 'integer'], 'refresh_token' => ['type' => 'string'] ] ], 'Customer' => [ 'type' => 'object', 'properties' => [ 'customer_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'vat' => ['type' => 'string'], 'email' => ['type' => 'string'] ] ], 'CustomerCreate' => [ 'type' => 'object', 'required' => ['company_id', 'name', 'vat'], 'properties' => [ 'company_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'vat' => ['type' => 'string'], 'email' => ['type' => 'string'] ] ] ] ] ]; } }