/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ 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']); } } }