From b62de4c18f4c90d449589df022fbbcc284ad8520 Mon Sep 17 00:00:00 2001 From: SP Date: Wed, 18 Jun 2025 16:07:13 +0200 Subject: [PATCH] Fix: Add tests, Optimize Sessions --- .gitignore | 3 +- bootstrap.php | 5 +- composer.json | 6 +- composer.lock | 40 +++-- src/RestAPIWrapperProffix/Client.php | 97 ++--------- .../HttpClient/HttpClient.php | 92 +++++++--- .../HttpClient/HttpClientException.php | 4 +- .../HttpClient/Options.php | 2 +- tests/.phpunit.result.cache | 1 + tests/Integration/ClientIntegrationTest.php | 158 ++++++++++++++++++ tests/phpunit.xml | 21 ++- 11 files changed, 299 insertions(+), 130 deletions(-) create mode 100644 tests/.phpunit.result.cache create mode 100644 tests/Integration/ClientIntegrationTest.php diff --git a/.gitignore b/.gitignore index 69ef8c8..4f15f55 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ PhpWrapperProffix.phar box.json box.phar manifest.json -phpunit.xml +.cache +.phpunit.cache \ No newline at end of file diff --git a/bootstrap.php b/bootstrap.php index dfb5ee6..b0cc1dd 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -1,4 +1,3 @@ http = new HttpClient($url, $apiDatabase, $apiUser, $apiPassword, $apiModules, $options); + $this->httpClient = new HttpClient($url, $apiDatabase, $apiUser, $apiPassword, $apiModules, $options); } - - /** - * @param string $endpoint - * @param array $data - * - * @return mixed - * - * @throws HttpClient\HttpClientException - */ - public function post($endpoint, $data) + public function getHttpClient() { - return $this->http->request($endpoint, 'POST', $data); + return $this->httpClient; } - /** - * @param string $endpoint - * @param array $data - * - * @return mixed - * - * @throws HttpClient\HttpClientException - */ - public function put($endpoint, $data) + public function get($endpoint, $parameters = []) { - return $this->http->request($endpoint, 'PUT', $data); + return $this->httpClient->request($endpoint, 'GET', [], $parameters); } - /** - * @param string $endpoint - * @param array $parameters - * - * @return mixed - * - * @throws HttpClient\HttpClientException - */ - public function get($endpoint, $parameters = []) + public function post($endpoint, $data = []) { + return $this->httpClient->request($endpoint, 'POST', $data); + } - return $this->http->request($endpoint, 'GET', [], $parameters); + public function put($endpoint, $data = []) + { + return $this->httpClient->request($endpoint, 'PUT', $data); } - /** - * @param string $endpoint - * @param array $parameters - * - * @return mixed - * - * @throws HttpClient\HttpClientException - */ public function delete($endpoint, $parameters = []) { - return $this->http->request($endpoint, 'DELETE', [], $parameters); + return $this->httpClient->request($endpoint, 'DELETE', [], $parameters); } - /** - * @param string $px_api_key - * - * @return mixed - * - * @throws HttpClient\HttpClientException - */ public function info($px_api_key = '') { - return $this->http->request('PRO/Info', 'GET', [], ['key' => $px_api_key], false); + return $this->httpClient->request('PRO/Info', 'GET', [], ['key' => $px_api_key], false); } - /** - * @param string $px_api_key - * - * @return mixed - * - * @throws HttpClient\HttpClientException - */ public function database($px_api_key = '') { - return $this->http->request('PRO/Datenbank', 'GET', [], ['key' => $px_api_key], false); + return $this->httpClient->request('PRO/Datenbank', 'GET', [], ['key' => $px_api_key], false); } - - } + diff --git a/src/RestAPIWrapperProffix/HttpClient/HttpClient.php b/src/RestAPIWrapperProffix/HttpClient/HttpClient.php index e554c99..e684b7c 100644 --- a/src/RestAPIWrapperProffix/HttpClient/HttpClient.php +++ b/src/RestAPIWrapperProffix/HttpClient/HttpClient.php @@ -205,19 +205,23 @@ protected function buildLoginUrl() */ protected function login() { - $this->initCurl(); + // IMPORTANT: login() now uses the cURL handle initialized by request(). + // It will temporarily set its own options on this handle. + // Login has specific cURL needs, especially CURLOPT_HEADER = true. $body = \json_encode($this->buildLoginJson()); - $headers = [ + $loginHeaders = [ 'Content-Type: application/json', - 'Content-Length: ' . strlen($body) + 'Content-Length: ' . strlen($body), + 'Accept: application/json' // Good practice to include Accept for login too ]; - curl_setopt($this->ch, CURLOPT_URL, $this->buildLoginUrl()); - curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, 'POST'); - curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($this->ch, CURLOPT_POSTFIELDS, $body); - curl_setopt($this->ch, CURLOPT_HEADER, true); + \curl_setopt($this->ch, CURLOPT_URL, $this->buildLoginUrl()); + \curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, 'POST'); + \curl_setopt($this->ch, CURLOPT_HTTPHEADER, $loginHeaders); + \curl_setopt($this->ch, CURLOPT_POSTFIELDS, $body); + \curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); // Make sure login also returns response + \curl_setopt($this->ch, CURLOPT_HEADER, true); // Crucial for extracting PxSessionId from login response headers $response = curl_exec($this->ch); @@ -229,7 +233,7 @@ protected function login() $header = substr($response, 0, $headerSize); $this->pxSessionId = $this->extractSessionId($header); - + if (empty($this->pxSessionId)) { throw new HttpClientException('Failed to retrieve PxSessionId from login response.', 401, $this->request, $this->response); } @@ -277,11 +281,19 @@ protected function logout() */ protected function setupMethod($method) { - if ('POST' == $method) { + // Reset method-specific options first to ensure a clean state + \curl_setopt($this->ch, CURLOPT_HTTPGET, false); + \curl_setopt($this->ch, CURLOPT_POST, false); + \curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, null); // Reset custom request + + if ('GET' == $method) { + \curl_setopt($this->ch, CURLOPT_HTTPGET, true); + } elseif ('POST' == $method) { \curl_setopt($this->ch, CURLOPT_POST, true); } elseif (\in_array($method, ['PUT', 'DELETE', 'OPTIONS'])) { \curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, $method); } + // For POST, PUT, DELETE with body, CURLOPT_POSTFIELDS will be set later in request() } /** @@ -297,10 +309,13 @@ protected function getRequestHeaders($sendData = false) { $headers = [ 'Accept' => 'application/json', - 'User-Agent' => $this->options->userAgent() . '/' . Client::VERSION, - 'PxSessionId' => $this->pxSessionId + 'User-Agent' => $this->options->userAgent() . '/' . Options::VERSION, ]; + if (!empty($this->pxSessionId)) { + $headers['PxSessionId'] = $this->pxSessionId; + } + if ($sendData) { $headers['Content-Type'] = 'application/json;charset=utf-8'; } @@ -413,7 +428,8 @@ protected function setDefaultCurlSettings() \curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, $timeout); \curl_setopt($this->ch, CURLOPT_TIMEOUT, $timeout); \curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); - \curl_setopt($this->ch, CURLOPT_HTTPHEADER, $this->request->getRawHeaders()); + \curl_setopt($this->ch, CURLOPT_HEADER, false); // Default for API calls: no headers in body + // Note: CURLOPT_HTTPHEADER is NOT set here anymore. It's set in request() method. \curl_setopt($this->ch, CURLOPT_URL, $this->request->getUrl()); } @@ -426,16 +442,31 @@ protected function setDefaultCurlSettings() */ protected function lookForErrors($parsedResponse) { - // Any non-200/201/202/204 response code indicates an error. if (!in_array($this->response->getCode(), ['200', '201', '202', '204'])) { $errorMessage = 'An unknown error occurred'; + if (isset($parsedResponse->Message)) { $errorMessage = $parsedResponse->Message; } elseif (is_string($parsedResponse)) { $errorMessage = $parsedResponse; } + if (isset($parsedResponse->Fields) && is_array($parsedResponse->Fields)) { + $fieldErrors = []; + foreach ($parsedResponse->Fields as $field) { + $fieldErrors[] = sprintf( + 'Field: %s (%s) - %s', + $field->Name ?? 'N/A', + $field->Reason ?? 'N/A', + $field->Message ?? 'N/A' + ); + } + if (!empty($fieldErrors)) { + $errorMessage .= ': ' . implode('; ', $fieldErrors); + } + } + throw new HttpClientException( sprintf('Error: %s', $errorMessage), $this->response->getCode(), @@ -488,14 +519,33 @@ protected function processResponse() */ public function request($endpoint, $method, $data = [], $parameters = [], $login = true) { + $this->initCurl(); + + // Create the request object for the main operation. + // This will be available if login() throws an exception. + $this->createRequest($endpoint, $method, $data, $parameters); + + // If login is required, it's performed first. + // login() will use the current $this->ch, temporarily setting options for the login call. if ($login && empty($this->pxSessionId)) { - $this->login(); + $this->login(); // This populates $this->pxSessionId if successful } - $this->initCurl(); + // Now, apply default cURL settings for the MAIN request. + // This sets the main request's URL (from $this->request), CURLOPT_HEADER=false, etc., + // effectively overriding any temporary settings login() might have applied to $this->ch. + $this->setDefaultCurlSettings(); - $this->createRequest($endpoint, $method, $data, $parameters); - $this->setDefaultCurlSettings(); + // Clear any headers from a potential previous login call on the same handle + \curl_setopt($this->ch, CURLOPT_HTTPHEADER, []); + + // Now, get the final headers for this specific request (which will include PxSessionId if login occurred) + $finalRequestHeaders = $this->getRequestHeaders(!empty($data)); + $rawFinalRequestHeaders = []; + foreach ($finalRequestHeaders as $key => $value) { + $rawFinalRequestHeaders[] = $key . ': ' . $value; + } + \curl_setopt($this->ch, CURLOPT_HTTPHEADER, $rawFinalRequestHeaders); // Set the final headers for the main API call // Setup method. $this->setupMethod($method); @@ -507,9 +557,11 @@ public function request($endpoint, $method, $data = [], $parameters = [], $login } $this->createResponse(); - $this->lookForErrors($this->processResponse()); + // Process response once and store it + $processedResponse = $this->processResponse(); + $this->lookForErrors($processedResponse); - return $this->processResponse(); + return $processedResponse; } /** diff --git a/src/RestAPIWrapperProffix/HttpClient/HttpClientException.php b/src/RestAPIWrapperProffix/HttpClient/HttpClientException.php index 377aaf6..2a7f3b3 100644 --- a/src/RestAPIWrapperProffix/HttpClient/HttpClientException.php +++ b/src/RestAPIWrapperProffix/HttpClient/HttpClientException.php @@ -22,7 +22,7 @@ class HttpClientException extends \Exception /** * @var Response */ - private $response; + private ?Response $response; /** @@ -33,7 +33,7 @@ class HttpClientException extends \Exception * @param Request $request * @param Response $response */ - public function __construct($message, $code, Request $request, Response $response) + public function __construct($message, $code, Request $request, ?Response $response) { parent::__construct($message, $code); diff --git a/src/RestAPIWrapperProffix/HttpClient/Options.php b/src/RestAPIWrapperProffix/HttpClient/Options.php index 6c68d57..cd4ce97 100644 --- a/src/RestAPIWrapperProffix/HttpClient/Options.php +++ b/src/RestAPIWrapperProffix/HttpClient/Options.php @@ -11,7 +11,7 @@ class Options { - const VERSION = 'v2'; + const VERSION = 'V4'; const TIMEOUT = 15; diff --git a/tests/.phpunit.result.cache b/tests/.phpunit.result.cache new file mode 100644 index 0000000..7306b06 --- /dev/null +++ b/tests/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":{"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanGetAddressList":8,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanGetArticleStock":7,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanCreateAddress":8,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanUpdateNewlyCreatedAddress":8,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanGetCountryDetailsCH":8,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanDeleteAddress":8},"times":{"Pitwch\\RestAPIWrapperProffix\\Tests\\HttpClientTest::testHttpClientCanBeInstantiated":0.003,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testClientCanBeInstantiated":0,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanGetAddressList":0.011,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanGetArticleStock":2.475,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\MinimalIntegrationTest::testMinimalAssertion":0.001,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\AppleTest::testRedDelicious":0.003,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanGetCountryDetailsCH":0.001,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanCreateAddress":0.001,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanUpdateNewlyCreatedAddress":0.001,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanDeleteAddress":0.001}} \ No newline at end of file diff --git a/tests/Integration/ClientIntegrationTest.php b/tests/Integration/ClientIntegrationTest.php new file mode 100644 index 0000000..a676227 --- /dev/null +++ b/tests/Integration/ClientIntegrationTest.php @@ -0,0 +1,158 @@ +client = new Client( + $_ENV['PROFFIX_API_URL'], + $_ENV['PROFFIX_API_DATABASE'], + $_ENV['PROFFIX_API_USER'], + $_ENV['PROFFIX_API_PASSWORD'], + $_ENV['PROFFIX_API_MODULES'] + ); + } + + public function testCanGetAddressList(): void + { + $addresses = $this->client->get('ADR/Adresse'); + $response = $this->client->getHttpClient()->getResponse(); + + $this->assertEquals(200, $response->getCode()); + $this->assertIsArray($addresses); + $this->assertNotEmpty($addresses, 'Address list should not be empty.'); + $this->assertObjectHasProperty('AdressNr', $addresses[0]); + } + + public function testCanGetCountryDetailsCH(): void + { + $country = $this->client->get('PRO/Land/CH'); + $response = $this->client->getHttpClient()->getResponse(); + + $this->assertEquals(200, $response->getCode()); + $this->assertIsObject($country); + $this->assertEquals('CH', $country->LandNr); + $this->assertEquals('Schweiz', $country->Bezeichnung); + } + + public function testCanCreateAddress(): void + { + $addressData = [ + "Name" => "Testfirma AG " . time(), + "Ort" => "Zürich", + "PLZ" => "8000", + "Land" => ["LandNr" => "CH"], + "Strasse" => "Testweg 1" + ]; + $addressId = null; + + try { + // 1. Create the address + $this->client->post('ADR/Adresse', $addressData); + $createResponse = $this->client->getHttpClient()->getResponse(); + $this->assertEquals(201, $createResponse->getCode(), 'HTTP status code should be 201 Created'); + $headers = $createResponse->getHeaders(); + $this->assertArrayHasKey('Location', $headers, 'Response must contain Location header'); + preg_match('/(\d+)$/', $headers['Location'], $matches); + $addressId = $matches[1]; + $this->assertNotEmpty($addressId, 'Could not extract new AdressNr from Location header'); + + // 2. Verify the address was created by fetching it + $newAddress = $this->client->get('ADR/Adresse/' . $addressId); + $this->assertEquals($addressData['Name'], $newAddress->Name); + $this->assertEquals($addressData['Ort'], $newAddress->Ort); + + } finally { + // 3. Cleanup: Delete the created address + if ($addressId) { + try { + $this->client->delete('ADR/Adresse/' . $addressId); + } catch (HttpClientException $e) { + fwrite(STDERR, "Cleanup failed for AdressNr {$addressId}: " . $e->getMessage() . "\n"); + } + } + } + } + + public function testCanUpdateNewlyCreatedAddress(): void + { + $baseName = "Testfirma Update " . time(); + $initialAddressData = [ + "Name" => $baseName, + "Ort" => "Updateville", + "PLZ" => "8888", + "Land" => ["LandNr" => "CH"], + "Strasse" => "Testweg 1" + ]; + $addressId = null; + + try { + // 1. Create a new address + $this->client->post('ADR/Adresse', $initialAddressData); + $createResponse = $this->client->getHttpClient()->getResponse(); + $this->assertEquals(201, $createResponse->getCode()); + $headers = $createResponse->getHeaders(); + preg_match('/(\d+)$/', $headers['Location'], $matches); + $addressId = $matches[1]; + sleep(3); // Give API time to process + + // 2. Update the address + $updatedName = $baseName . " Updated"; + $updateData = array_merge($initialAddressData, ['Name' => $updatedName, 'AdressNr' => $addressId]); + $this->client->put('ADR/Adresse/' . $addressId, $updateData); + $updateResponse = $this->client->getHttpClient()->getResponse(); + $this->assertEquals(204, $updateResponse->getCode()); + + // 3. Verify the update + $updatedAddress = $this->client->get('ADR/Adresse/' . $addressId); + $this->assertEquals($updatedName, $updatedAddress->Name); + + } finally { + // 4. Cleanup + if ($addressId) { + $this->client->delete('ADR/Adresse/' . $addressId); + } + } + } + + public function testCanDeleteAddress(): void + { + $addressData = [ + "Name" => "Firma zum Löschen " . time(), + "Ort" => "Deleteburg", + "PLZ" => "1234", + "Land" => ["LandNr" => "CH"], + "Strasse" => "Wegwerfweg 1" + ]; + $addressId = null; + + // 1. Create a new address + $this->client->post('ADR/Adresse', $addressData); + $createResponse = $this->client->getHttpClient()->getResponse(); + $this->assertEquals(201, $createResponse->getCode()); + $headers = $createResponse->getHeaders(); + preg_match('/(\d+)$/', $headers['Location'], $matches); + $addressId = $matches[1]; + + // 2. Delete the address + $this->client->delete('ADR/Adresse/' . $addressId); + $deleteResponse = $this->client->getHttpClient()->getResponse(); + $this->assertEquals(204, $deleteResponse->getCode()); + + // 3. Verify it's gone (soft delete). + $getResponse = $this->client->get('ADR/Adresse/' . $addressId); + $this->assertTrue($getResponse->Geloescht); + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 843ecb2..b07d758 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,16 +1,23 @@ - - - - . - - + + + + + + - + + + + ./Integration + + +