diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 6488198..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: "Build - CI" - -on: - push: - branches: - - "master" - -jobs: - coding-standards-php-8: - name: "[BACKEND] - Coding standards (PHP-8)" - - runs-on: "ubuntu-latest" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "8.0" - - - name: "Cache dependencies" - uses: "actions/cache@v2" - with: - path: "~/.composer/cache" - key: "php-8.0-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-8.0-composer-" - - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress" - - - name: "PHP - Coding Standard" - run: "vendor/bin/phing phpcs" - - static-analysis-php-8: - name: "[BACKEND] - Static analysis (PHP-8)" - - runs-on: "ubuntu-latest" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "8.0" - - - name: "Cache dependencies" - uses: "actions/cache@v2" - with: - path: "~/.composer/cache" - key: "php-8.0-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-8.0-composer-" - - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress" - - - name: "PHPStan" - run: "vendor/bin/phing phpstan" - - tests-php-8: - name: "[BACKEND] - Tests (PHP-8)" - - runs-on: "ubuntu-latest" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "8.0" - - - name: "Cache dependencies" - uses: "actions/cache@v2" - with: - path: "~/.composer/cache" - key: "php-8.0-composer-${{ hashFiles('**/composer.json') }}" - restore-keys: "php-8.0-composer-" - - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress" - - - name: "PHPUnit" - run: "vendor/bin/paratest tests" diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..446a885 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,91 @@ +name: Checks + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + checks: + name: Checks (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.4', '8.5'] + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl, json, mbstring, simplexml + coverage: none + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php }}-composer- + + - name: Install dependencies + run: composer install --no-interaction --no-progress --prefer-dist + + - name: Security audit + run: composer security-audit + + - name: Composer normalize + run: composer composer-normalize-check + + - name: PHP CodeSniffer + run: composer phpcs-check + + - name: Rector + run: composer rector-check + + - name: PHPStan + run: composer analyse + + - name: Tests + run: composer test + + runtime: + name: Runtime (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3'] + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl, json, mbstring, simplexml + coverage: none + + - name: Validate composer.json + run: composer validate --strict --no-check-lock + + # The development tools (pekral/*, PHPStan 2, Rector 2) require PHP 8.4+, so composer --no-dev + # still fails to resolve require-dev here. Resolve a dev-free manifest instead, so this step + # verifies exactly what a consumer on PHP 8.2 / 8.3 installs. + - name: Verify runtime dependencies resolve + run: | + php -r '$c = json_decode(file_get_contents("composer.json"), true); unset($c["require-dev"], $c["scripts"], $c["autoload-dev"]); file_put_contents("composer.runtime.json", json_encode($c, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));' + COMPOSER=composer.runtime.json composer update --no-interaction --no-progress --prefer-dist --no-plugins + + - name: Lint shipped sources + run: find src -name '*.php' -print0 | xargs -0 -n1 php -l diff --git a/.gitignore b/.gitignore index 9064738..a3d0bb2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,12 @@ .DS_Store tests/.DS_Store .phpunit.result.cache +/.phpunit.cache composer.lock + +# Generated by pekral/ai-olympus (vendor/bin/ai-olympus install) +/.claude +/.codex +/.agents +/AGENTS.md +/composer.runtime.* diff --git a/README.md b/README.md index 90b8513..7976839 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,10 @@ Jednoduchý cURL client, který se stará o správné složení requestu do syst [![License](https://poser.pugx.org/ecomailcz/flexibee-client/license?format=flat)](https://packagist.org/packages/ecomailcz/flexibee-client) [![Latest version](https://img.shields.io/packagist/v/ecomailcz/flexibee-client.svg?colorB=007EC6)](https://packagist.org/packages/ecomailcz/flexibee-client) [![Downloads](https://img.shields.io/packagist/dt/ecomailcz/flexibee-client.svg?colorB=007EC6)](https://packagist.org/packages/ecomailcz/flexibee-client) -![PHPStan](https://img.shields.io/badge/style-level%207-brightgreen.svg?&label=phpstan) +![PHPStan](https://img.shields.io/badge/style-level%20max-brightgreen.svg?&label=phpstan) + +Podporované verze PHP: 8.2, 8.3, 8.4, 8.5. + ## Sponsored by [![Downloads](https://ecomail.cz/images/logo@2.png)](https://ecomail.cz) diff --git a/build.xml b/build.xml deleted file mode 100644 index b88df78..0000000 --- a/build.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/composer.json b/composer.json index 19b1354..7e2871e 100644 --- a/composer.json +++ b/composer.json @@ -10,33 +10,34 @@ "ext-json": "*", "ext-mbstring": "*", "ext-simplexml": "*", - "ecomailcz/purl": "^2.0", - "rakit/validation": "^1.4" + "ecomailcz/purl": "^2.0" }, "require-dev": { "brianium/paratest": "^6.10.0", "ergebnis/composer-normalize": "^2.34.0", - "fzaninotto/faker": "^1.9", + "fakerphp/faker": "^1.24", "mockery/mockery": "^1.6.4", - "phing/phing": "^2.17.4", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "pekral/ai-olympus": "^0.4", + "pekral/phpcs-rules": "dev-master", + "pekral/phpstan-rules": "dev-master", + "pekral/rector-rules": "dev-master", "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.10.26", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.13", - "phpstan/phpstan-strict-rules": "^1.5.1", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.6.10", - "slevomat/coding-standard": "^6.0 || ^7.2.1" + "rector/rector": "^2.8" }, "minimum-stability": "dev", "prefer-stable": true, "autoload": { "psr-4": { - "EcomailFlexibee\\": [ - "src/" - ], "EcomailFlexibeeTest\\": [ "tests/" + ], + "EcomailFlexibee\\": [ + "src/" ] } }, @@ -47,10 +48,39 @@ }, "config": { "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, "ergebnis/composer-normalize": true, - "phpstan/extension-installer": true, - "dealerdirect/phpcodesniffer-composer-installer": true + "pekral/ai-olympus": true, + "phpstan/extension-installer": true }, "sort-packages": true + }, + "scripts": { + "ai-olympus:install": "vendor/bin/ai-olympus install --force", + "analyse": "vendor/bin/phpstan analyse --memory-limit=2G --no-progress", + "build": [ + "@fix", + "@check" + ], + "check": [ + "@composer-normalize-check", + "@phpcs-check", + "@rector-check", + "@analyse", + "@test" + ], + "composer-normalize-check": "@composer normalize --dry-run", + "fix": [ + "@composer normalize", + "@rector-fix", + "@phpcs-fix" + ], + "phpcs-check": "vendor/bin/phpcs --standard=ruleset.xml src tests", + "phpcs-fix": "vendor/bin/phpcbf --standard=ruleset.xml src tests || true", + "rector-check": "vendor/bin/rector process --dry-run", + "rector-fix": "vendor/bin/rector process", + "security-audit": "@composer audit", + "test": "vendor/bin/paratest --exclude-group=integration", + "test:integration": "vendor/bin/paratest --group=integration" } } diff --git a/phpstan.neon b/phpstan.neon index 118aa45..5b81967 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,8 @@ parameters: level: max - checkMissingIterableValueType: false - treatPhpDocTypesAsCertain: false + phpVersion: + min: 80200 + max: 80599 + paths: + - src + - tests diff --git a/phpunit.xml b/phpunit.xml index 409dcbc..d8dd3e1 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,11 +1,11 @@ - + - ../src + src - . + tests diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..212fe7a --- /dev/null +++ b/rector.php @@ -0,0 +1,12 @@ +withPaths([__DIR__ . '/src', __DIR__ . '/tests']) + // The package supports PHP 8.2+: rules that need a newer PHP version are skipped. + ->withPhpVersion(PhpVersion::PHP_82) + ->withSets([__DIR__ . '/vendor/pekral/rector-rules/rector.php']); diff --git a/ruleset.xml b/ruleset.xml index 029af56..70ba0ce 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -1,197 +1,17 @@ - - - - - - - + + - /resources + - - - - - + + + src/Config.php - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + src/Client.php - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Client.php b/src/Client.php index 4bdbb25..adbccd8 100644 --- a/src/Client.php +++ b/src/Client.php @@ -13,9 +13,15 @@ use EcomailFlexibee\Http\ResponseHydrator; use EcomailFlexibee\Http\UrlBuilder; use EcomailFlexibee\Result\EvidenceResult; + use function array_filter; use function array_merge; use function count; +use function is_array; +use function is_int; +use function is_numeric; +use function is_scalar; +use function is_string; use function mb_strtolower; use function sprintf; @@ -23,17 +29,29 @@ class Client { protected UrlBuilder $queryBuilder; + protected HttpClient $httpClient; - protected Config $config; - private ResponseHydrator $responseHydrator; + protected Config $config; - public function __construct(string $url, string $company, string $user, string $password, string $evidence, bool $verifySSLCertificate, ?string $authSessionId = null, bool $debugMode = false) + private readonly ResponseHydrator $responseHydrator; + + public function __construct( + string $url, + string $company, + string $user, + string $password, + string $evidence, + bool $verifySSLCertificate, + ?string $authSessionId = null, + bool $debugMode = false, + ?HttpClient $httpClient = null, + ) { $this->config = new Config($url, $company, $user, $password, $evidence, $verifySSLCertificate, $authSessionId, $debugMode); $this->queryBuilder = new UrlBuilder($this->config); $this->responseHydrator = new ResponseHydrator($this->config); - $this->httpClient = new HttpClient(); + $this->httpClient = $httpClient ?? new HttpClient(); } public function isAllowedChangesApi(): bool @@ -101,12 +119,16 @@ public function getCompanies(): Response */ public function getCompany(): array { + $companies = $this->getCompanies()->getData()['companies'] ?? []; $result = array_filter( - $this->getCompanies()->getData()['companies'], - fn (array $data): bool => mb_strtolower($data['dbNazev']) === mb_strtolower($this->config->getCompany()), + is_array($companies) ? $companies : [], + fn (mixed $data): bool => is_array($data) + && isset($data['dbNazev']) + && is_string($data['dbNazev']) + && mb_strtolower($data['dbNazev']) === mb_strtolower($this->config->getCompany()), ); - if (isset($result['company'])) { + if (isset($result['company']) && is_array($result['company'])) { return $result['company']; } @@ -128,8 +150,8 @@ public function getAuthAndRefreshToken(): Response Method::POST, [], [ - 'username' => $this->config->getUser(), 'password' => $this->config->getPassword(), + 'username' => $this->config->getUser(), ], [ 'Content-Type: application/x-www-form-urlencoded', @@ -166,19 +188,19 @@ public function deleteByCode(string $id, bool $dryRun = false): void } /** - * @param array $uriParameters * @throws \EcomailFlexibee\Exception\EcomailFlexibeeConnectionFail * @throws \EcomailFlexibee\Exception\EcomailFlexibeeForbidden * @throws \EcomailFlexibee\Exception\EcomailFlexibeeInvalidAuthorization * @throws \EcomailFlexibee\Exception\EcomailFlexibeeMethodNotAllowed * @throws \EcomailFlexibee\Exception\EcomailFlexibeeNotAcceptableRequest + * @param array $uriParameters * @throws \EcomailFlexibee\Exception\EcomailFlexibeeRequestFail */ public function findById(int $id, array $uriParameters = []): EvidenceResult { try { return $this->getById($id, $uriParameters); - } catch (EcomailFlexibeeNoEvidenceResult $exception) { + } catch (EcomailFlexibeeNoEvidenceResult) { return new EvidenceResult([]); } } @@ -205,7 +227,7 @@ public function getByCode(string $code, array $uriParameters = []): EvidenceResu [], $this->config, ) , - true, + throwException: true, ); } @@ -230,7 +252,7 @@ public function getById(int $id, array $uriParameters = []): EvidenceResult [], $this->config, ), - true, + throwException: true, ); } @@ -248,7 +270,7 @@ public function findByCode(string $code, array $uriParameters = []): EvidenceRes { try { return $this->getByCode($code, $uriParameters); - } catch (EcomailFlexibeeNoEvidenceResult $exception) { + } catch (EcomailFlexibeeNoEvidenceResult) { return new EvidenceResult([]); } } @@ -276,55 +298,39 @@ public function save(array $evidenceData, ?int $id, bool $dryRun = false, array ? array_merge($uriParameters, ['dry-run' => 'true']) : $uriParameters; /** @var \EcomailFlexibee\Result\EvidenceResult $response */ - $response = $this->callRequest(Method::PUT, null, $uriParameters, $postData, [])[0]; + $response = $this->callRequest(Method::PUT, queryFilterOrId: null, uriParameters: $uriParameters, postFields: $postData, headers: [])[0]; $data = $response->getData(); - - if ( - isset($data['created']) - && (int) $data['created'] === 0 - && isset($data['updated']) - && (int) $data['updated'] === 0 - ) { - $errorMessage = sprintf('(%d) %s', $data['status_code'], $data['message']); - - throw new EcomailFlexibeeSaveFailed($errorMessage); - } - - if (isset($data['success']) && $data['success'] !== 'true' && isset($data['message'])) { - throw new EcomailFlexibeeSaveFailed($data['message']); - } + $this->throwWhenNotSaved($data); return new FlexibeeResponse( 200, - null, - true, - null, - count($data), - null, - $response->getData(), - [], + version: null, + success: true, + rowCount: count($data), + data: $response->getData(), + statistics: [], ); } public function getUserRelations(int $objectId): EvidenceResult { - return new EvidenceResult( - $this->getById($objectId, ['relations' => 'uzivatelske-vazby'])->getData()[0]['uzivatelske-vazby'], - ); + $relations = $this->getFirstEvidenceItem($objectId, ['relations' => 'uzivatelske-vazby'])['uzivatelske-vazby'] ?? null; + + return new EvidenceResult(is_array($relations) ? $relations : []); } public function addUserRelation(int $objectAId, int $objectBId, float $price, int $relationTypeId, ?string $description = null): void { - $objectBData = $this->getById($objectBId, [])->getData()[0]; + $objectBCode = $this->getFirstEvidenceItem($objectBId, [])['kod'] ?? null; $relationData = [ 'id' => $objectAId, 'uzivatelske-vazby' => [ 'uzivatelska-vazba' => [ - 'vazbaTyp' => $relationTypeId, 'cena' => $price, - 'popis' => $description, 'evidenceType' => $this->config->getEvidence(), - 'object' => sprintf('code:%s', $objectBData['kod']), + 'object' => sprintf('code:%s', is_scalar($objectBCode) ? $objectBCode : ''), + 'popis' => $description, + 'vazbaTyp' => $relationTypeId, ], ], ]; @@ -366,7 +372,7 @@ public function countInEvidence(): int $this->config, ); - return $response->getRowCount(); + return $response->getRowCount(); } /** @@ -444,7 +450,7 @@ public function searchInEvidencePaginated(string $query, array $uriParameters): /** * @param array $uriParameters * @param array $postFields - * @param array $headers + * @param array $headers * @return array<\EcomailFlexibee\Result\EvidenceResult> * @throws \EcomailFlexibee\Exception\EcomailFlexibeeConnectionFail * @throws \EcomailFlexibee\Exception\EcomailFlexibeeForbidden @@ -453,7 +459,7 @@ public function searchInEvidencePaginated(string $query, array $uriParameters): * @throws \EcomailFlexibee\Exception\EcomailFlexibeeNotAcceptableRequest * @throws \EcomailFlexibee\Exception\EcomailFlexibeeRequestFail */ - public function callRequest(Method $httpMethod, int|string|null $queryFilterOrId, array $uriParameters, array $postFields, array $headers): array + public function callRequest(Method $httpMethod, null|int|string $queryFilterOrId, array $uriParameters, array $postFields, array $headers): array { $response = $this->httpClient->request( $this->queryBuilder->createUri($queryFilterOrId, $uriParameters), @@ -502,4 +508,40 @@ public function findLastInEvidence(bool $fullDetail): Response ); } + /** + * @param array $data + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeSaveFailed + */ + private function throwWhenNotSaved(array $data): void + { + if ($this->isZero($data['created'] ?? null) && $this->isZero($data['updated'] ?? null)) { + $statusCode = $data['status_code'] ?? 0; + $message = $data['message'] ?? ''; + + throw new EcomailFlexibeeSaveFailed( + sprintf('(%d) %s', is_int($statusCode) ? $statusCode : 0, is_string($message) ? $message : ''), + ); + } + + if (isset($data['success'], $data['message']) && $data['success'] !== 'true' && is_string($data['message'])) { + throw new EcomailFlexibeeSaveFailed($data['message']); + } + } + + private function isZero(mixed $value): bool + { + return is_numeric($value) && (int) $value === 0; + } + + /** + * @param array $uriParameters + * @return array + */ + private function getFirstEvidenceItem(int $id, array $uriParameters): array + { + $item = $this->getById($id, $uriParameters)->getData()[0] ?? null; + + return is_array($item) ? $item : []; + } + } diff --git a/src/CompanyClient.php b/src/CompanyClient.php index efae900..9f26dde 100644 --- a/src/CompanyClient.php +++ b/src/CompanyClient.php @@ -4,6 +4,7 @@ namespace EcomailFlexibee; +use EcomailFlexibee\Http\HttpClient; use EcomailFlexibee\Http\Method; use EcomailFlexibee\Http\Response\Response; @@ -12,7 +13,7 @@ class CompanyClient extends Client private const EVIDENCE = 'c'; - public function __construct(Config $config) + public function __construct(Config $config, ?HttpClient $httpClient = null) { parent::__construct( $config->getUrl(), @@ -22,14 +23,16 @@ public function __construct(Config $config) self::EVIDENCE, $config->verifySSLCertificate(), $config->getAuthSessionId(), - ); + $config->debugMode(), + $httpClient, + ); } public function backup(): Response { return $this->httpClient->request( $this->queryBuilder->createBackupUrl(), - Method::get(Method::GET), + Method::GET, [], [], [], @@ -41,7 +44,7 @@ public function restore(string $companyName, string $data): Response { return $this->httpClient->request( $this->queryBuilder->createRestoreUrl($companyName), - Method::get(Method::PUT), + Method::PUT, [$data], [], [], diff --git a/src/Config.php b/src/Config.php index 3e52e29..1d3ce6c 100644 --- a/src/Config.php +++ b/src/Config.php @@ -4,7 +4,7 @@ namespace EcomailFlexibee; -final class Config +final readonly class Config { public function __construct( diff --git a/src/Enum/SearchQueryOperator.php b/src/Enum/SearchQueryOperator.php index 5924908..da848e4 100644 --- a/src/Enum/SearchQueryOperator.php +++ b/src/Enum/SearchQueryOperator.php @@ -4,52 +4,47 @@ namespace EcomailFlexibee\Enum; -use function array_keys; -use function array_values; +use function array_map; use function implode; use function preg_match_all; use function preg_replace; use function preg_split; use function str_replace; +use function strtr; use function urldecode; class SearchQueryOperator { /** - * @var array + * @var array */ private static array $operators = [ - '==' => ' eq ', + '!=' => ' neq ', + '<' => ' lt ', '<=' => ' lte ', '<>' => ' neq ', - '>=' => ' gte ', - '!=' => ' neq ', '=' => ' eq ', - '<' => ' lt ', + '==' => ' eq ', '>' => ' gt ', + '>=' => ' gte ', ]; public static function convertOperatorsInQuery(string $query): string { $query = urldecode($query); - /** @var array $queryExploded */ $queryExploded = preg_split('/\s+/', $query); - foreach($queryExploded as &$part) { - $toReplace = []; + $converted = array_map(static function (string $part): string { preg_match_all('/[^\'](?=(?:[^\']*\'[^"]*\'[^\']*|[^\'])*$)/', $part, $matches); + $text = implode('', $matches[0]); - if (isset($matches[0])) { - $text = implode('', $matches[0]); - $toReplace[$text] = str_replace(array_keys(self::$operators), array_values(self::$operators), $text); - } - - $part = str_replace(array_keys($toReplace), array_values($toReplace), $part); - } + // strtr() always replaces the longest operator first ("<=" before "<"), independent of key order. + return str_replace($text, strtr($text, self::$operators), $part); + }, $queryExploded === false ? [] : $queryExploded); /** @var string $result */ - $result = preg_replace('/\s+/', ' ', implode(' ',$queryExploded)); + $result = preg_replace('/\s+/', ' ', implode(' ', $converted)); return $result; } diff --git a/src/Exception/EcomailFlexibeeInvalidRequestParameter.php b/src/Exception/EcomailFlexibeeInvalidRequestParameter.php index adee262..d916819 100644 --- a/src/Exception/EcomailFlexibeeInvalidRequestParameter.php +++ b/src/Exception/EcomailFlexibeeInvalidRequestParameter.php @@ -5,17 +5,8 @@ namespace EcomailFlexibee\Exception; use Exception; -use Rakit\Validation\Validation; -use Throwable; -use function implode; -use const PHP_EOL; final class EcomailFlexibeeInvalidRequestParameter extends Exception { - public function __construct(Validation $validation, ?Throwable $previous = null) - { - parent::__construct(implode(PHP_EOL,$validation->errors()->all()), 0, $previous); - } - } diff --git a/src/Http/CurlOptionsFactory.php b/src/Http/CurlOptionsFactory.php new file mode 100644 index 0000000..4f9b261 --- /dev/null +++ b/src/Http/CurlOptionsFactory.php @@ -0,0 +1,115 @@ + + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeRequestFail + */ + public function create(CurlRequest $request, Config $config): array + { + if ($request->url === '') { + throw new EcomailFlexibeeRequestFail('Request URL must not be empty.'); + } + + $verifySSLCertificate = $config->verifySSLCertificate(); + $options = [ + CURLINFO_HEADER_OUT => $config->debugMode(), + CURLOPT_CONNECTTIMEOUT => self::CONNECT_TIMEOUT_SECONDS, + CURLOPT_CUSTOMREQUEST => $request->httpMethod->value, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYHOST => $verifySSLCertificate ? 2 : 0, + CURLOPT_SSL_VERIFYPEER => $verifySSLCertificate, + CURLOPT_TIMEOUT => self::TIMEOUT_SECONDS, + CURLOPT_URL => $request->url, + CURLOPT_USERAGENT => self::USER_AGENT, + ] + $this->createAuthenticationOptions($config); + + if (count($request->postFields) > 0) { + $options[CURLOPT_POSTFIELDS] = $this->encodePostFields($request->postFields); + } + + if (count($request->queryParameters) > 0) { + $options[CURLOPT_POSTFIELDS] = http_build_query($request->queryParameters); + } + + $headers = $request->headers; + + if ($config->getAuthSessionId() !== null) { + $headers[] = sprintf('X-authSessionId: %s', $config->getAuthSessionId()); + } + + if (count($headers) !== 0) { + $options[CURLOPT_HTTPHEADER] = $headers; + } + + return $options; + } + + /** + * @return array + */ + private function createAuthenticationOptions(Config $config): array + { + if ($config->getAuthSessionId() !== null) { + return [CURLOPT_HTTPAUTH => 0]; + } + + return [ + CURLOPT_HTTPAUTH => CURLAUTH_BASIC, + CURLOPT_USERPWD => sprintf('%s:%s', $config->getUser(), $config->getPassword()), + ]; + } + + /** + * @param array $postFields + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeRequestFail + */ + private function encodePostFields(array $postFields): string + { + try { + return json_encode(['winstrom' => $postFields], JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + throw new EcomailFlexibeeRequestFail('Request data cannot be encoded to JSON.', 0, $exception); + } + } + +} diff --git a/src/Http/CurlRequest.php b/src/Http/CurlRequest.php new file mode 100644 index 0000000..befe031 --- /dev/null +++ b/src/Http/CurlRequest.php @@ -0,0 +1,25 @@ + $postFields + * @param array $queryParameters + * @param array $headers + */ + public function __construct( + public string $url, + public Method $httpMethod, + public array $postFields = [], + public array $queryParameters = [], + public array $headers = [], + ) + { + } + +} diff --git a/src/Http/HttpClient.php b/src/Http/HttpClient.php index e851b94..921cb3c 100644 --- a/src/Http/HttpClient.php +++ b/src/Http/HttpClient.php @@ -4,8 +4,10 @@ namespace EcomailFlexibee\Http; +use CurlHandle; use EcomailFlexibee\Config; use EcomailFlexibee\Http\Response\FlexibeeResponse; + use function curl_errno; use function curl_error; use function curl_exec; @@ -13,28 +15,29 @@ use function is_string; use function mb_strlen; use function trim; + use const CURLINFO_HTTP_CODE; -final class HttpClient +class HttpClient { - private HttpCurlBuilder $httpCurlBuilder; + private readonly HttpCurlBuilder $httpCurlBuilder; public function __construct() { $this->httpCurlBuilder = new HttpCurlBuilder(); } - /** - * @param array $postFields - * @param array $queryParameters - * @param array $headers - * @throws \EcomailFlexibee\Exception\EcomailFlexibeeConnectionFail - * @throws \EcomailFlexibee\Exception\EcomailFlexibeeForbidden - * @throws \EcomailFlexibee\Exception\EcomailFlexibeeInvalidAuthorization - * @throws \EcomailFlexibee\Exception\EcomailFlexibeeMethodNotAllowed - * @throws \EcomailFlexibee\Exception\EcomailFlexibeeNotAcceptableRequest - * @throws \EcomailFlexibee\Exception\EcomailFlexibeeRequestFail - */ + /** + * @param array $postFields + * @param array $queryParameters + * @param array $headers + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeConnectionFail + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeForbidden + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeInvalidAuthorization + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeMethodNotAllowed + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeNotAcceptableRequest + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeRequestFail + */ public function request(string $url, Method $httpMethod, array $postFields, array $queryParameters, array $headers, Config $config): FlexibeeResponse { @@ -55,8 +58,19 @@ public function request(string $url, Method $httpMethod, array $postFields, arra $statusCode, curl_errno($ch), $errorMessage, - $config->debugMode() ? json_encode(curl_getinfo($ch, CURLINFO_HEADER_OUT)) : null + $this->getDebugInfo($ch, $config), ); } + private function getDebugInfo(CurlHandle $ch, Config $config): ?string + { + if (!$config->debugMode()) { + return null; + } + + $debugInfo = json_encode(curl_getinfo($ch, CURLINFO_HEADER_OUT)); + + return $debugInfo === false ? null : $debugInfo; + } + } diff --git a/src/Http/HttpCurlBuilder.php b/src/Http/HttpCurlBuilder.php index 9b42aa0..6bad11c 100644 --- a/src/Http/HttpCurlBuilder.php +++ b/src/Http/HttpCurlBuilder.php @@ -6,72 +6,29 @@ use CurlHandle; use EcomailFlexibee\Config; -use function count; + use function curl_init; -use function curl_setopt; -use function http_build_query; -use function json_encode; -use function sprintf; -use const CURLOPT_CUSTOMREQUEST; -use const CURLOPT_FOLLOWLOCATION; -use const CURLOPT_HTTPAUTH; -use const CURLOPT_HTTPHEADER; -use const CURLOPT_POSTFIELDS; -use const CURLOPT_RETURNTRANSFER; -use const CURLOPT_SSL_VERIFYHOST; -use const CURLOPT_SSL_VERIFYPEER; -use const CURLOPT_URL; -use const CURLOPT_USERAGENT; -use const CURLOPT_USERPWD; +use function curl_setopt_array; -final class HttpCurlBuilder +final readonly class HttpCurlBuilder { + private CurlOptionsFactory $curlOptionsFactory; + + public function __construct() + { + $this->curlOptionsFactory = new CurlOptionsFactory(); + } + /** - * @param array $postFields - * @param array $queryParameters - * @param array $headers + * @param array $postFields + * @param array $queryParameters + * @param array $headers */ public function build(string $url, Method $httpMethod, array $postFields, array $queryParameters, array $headers, Config $config): CurlHandle { $ch = curl_init(); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); - - if ($config->getAuthSessionId() !== null) { - curl_setopt($ch, CURLOPT_HTTPAUTH, FALSE); - $headers[] = sprintf('X-authSessionId: %s', $config->getAuthSessionId()); - } else { - curl_setopt($ch, CURLOPT_HTTPAUTH, TRUE); - curl_setopt($ch, CURLOPT_USERPWD, sprintf('%s:%s', $config->getUser(), $config->getPassword())); - } - - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $httpMethod->value); - curl_setopt($ch, CURLOPT_USERAGENT, 'Ecomail.cz Flexibee client (https://github.com/Ecomailcz/flexibee-client)'); - $verifySSLCertificate = $config->verifySSLCertificate() && $config->getAuthSessionId() === null; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifySSLCertificate); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifySSLCertificate); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); - curl_setopt($ch, CURLOPT_TIMEOUT, 25); - - if($config->debugMode()) { - curl_setopt($ch, CURLINFO_HEADER_OUT, true); - } - - if (count($postFields) > 0) { - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ - 'winstrom' => $postFields, - ])); - } - - if (count($queryParameters) > 0) { - curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($queryParameters)); - } - - if (count($headers) !== 0) { - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - } + curl_setopt_array($ch, $this->curlOptionsFactory->create(new CurlRequest($url, $httpMethod, $postFields, $queryParameters, $headers), $config)); return $ch; } diff --git a/src/Http/Response/FlexibeeBackupResponse.php b/src/Http/Response/FlexibeeBackupResponse.php index 346cf4c..c6f7271 100644 --- a/src/Http/Response/FlexibeeBackupResponse.php +++ b/src/Http/Response/FlexibeeBackupResponse.php @@ -9,7 +9,7 @@ class FlexibeeBackupResponse extends FlexibeeResponse public function __construct(string $backupContent) { - parent::__construct(200, null, true, null,0,null, [ + parent::__construct(200, version: null, success: true,rowCount: 0, data: [ 'backupContent' => $backupContent, ]); } diff --git a/src/Http/Response/FlexibeePdfResponse.php b/src/Http/Response/FlexibeePdfResponse.php index a93f71d..2429003 100644 --- a/src/Http/Response/FlexibeePdfResponse.php +++ b/src/Http/Response/FlexibeePdfResponse.php @@ -9,7 +9,7 @@ class FlexibeePdfResponse extends FlexibeeResponse public function __construct(string $pdfContent) { - parent::__construct(200, null, true, null,0,null, [ + parent::__construct(200, version: null, success: true,rowCount: 0, data: [ 'pdfContent' => $pdfContent, ]); } diff --git a/src/Http/Response/FlexibeeResponse.php b/src/Http/Response/FlexibeeResponse.php index 283bee0..2f0c9a0 100644 --- a/src/Http/Response/FlexibeeResponse.php +++ b/src/Http/Response/FlexibeeResponse.php @@ -11,9 +11,18 @@ class FlexibeeResponse implements Response * FlexibeeResponse constructor. * * @param array $data - * @param array $statistics + * @param array $statistics */ - public function __construct(private int $statusCode, private ?float $version, private bool $success, private ?string $message = null, private int $rowCount = 0, private ?int $globalVersion = null, private array $data = [], private array $statistics = []) + public function __construct( + private readonly int $statusCode, + private readonly ?float $version, + private readonly bool $success, + private readonly ?string $message = null, + private readonly int $rowCount = 0, + private readonly ?int $globalVersion = null, + private readonly array $data = [], + private readonly array $statistics = [], + ) { } @@ -46,7 +55,7 @@ public function getData(): array } /** - * @return array + * @return array */ public function getStatistics(): array { diff --git a/src/Http/Response/Response.php b/src/Http/Response/Response.php index 213f5b2..36a9b10 100644 --- a/src/Http/Response/Response.php +++ b/src/Http/Response/Response.php @@ -16,12 +16,12 @@ public function isSuccess(): bool; public function getStatusCode(): int; /** - * @return array + * @return array */ public function getData(): array; /** - * @return array + * @return array */ public function getStatistics(): array; diff --git a/src/Http/ResponseFactory.php b/src/Http/ResponseFactory.php index a769233..c1af413 100644 --- a/src/Http/ResponseFactory.php +++ b/src/Http/ResponseFactory.php @@ -13,18 +13,40 @@ use EcomailFlexibee\Http\Response\FlexibeeBackupResponse; use EcomailFlexibee\Http\Response\FlexibeePdfResponse; use EcomailFlexibee\Http\Response\FlexibeeResponse; + use function implode; use function in_array; +use function is_array; +use function is_numeric; +use function is_scalar; +use function is_string; use function json_decode; use function mb_stripos; use function mb_strpos; +use function reset; use function sprintf; + use const CURLE_OK; final class ResponseFactory { - public static function createFromOutput(string $url, Method $httpMethod, ?string $responseContent, int $statusCode, int $errorNumber, ?string $errorMessage, ?string $curlInfoDebug): FlexibeeResponse + private const ERROR_FIELD_LABELS = [ + ['code', 'code'], + ['for', 'for attribute'], + ['path', 'path'], + ['message', 'message'], + ]; + + public static function createFromOutput( + string $url, + Method $httpMethod, + ?string $responseContent, + int $statusCode, + int $errorNumber, + ?string $errorMessage, + ?string $curlInfoDebug, + ): FlexibeeResponse { if ($responseContent === null) { throw new EcomailFlexibeeRequestFail(); @@ -34,124 +56,144 @@ public static function createFromOutput(string $url, Method $httpMethod, ?string throw new EcomailFlexibeeConnectionFail(sprintf('cURL error (%s): %s', $errorNumber, $errorMessage)); } - // PDF content if (mb_strpos($url, '.pdf') !== false) { return new FlexibeePdfResponse($responseContent); } - // Backup content if ($httpMethod === Method::GET && mb_stripos($url, '/backup') !== false) { return new FlexibeeBackupResponse($responseContent); } - /** @var array|null $data */ - $data = json_decode($responseContent, true); - $data ??= []; - $data = $data['winstrom'] ?? $data; - $results = $data['results'] ?? $data; - - $version = null; - /** @var string|null $message */ - $message = $responseContent; - $success = false; - $statistics = []; - $rowCount = 0; - $globalVersion = null; - - if (isset($data['@version'])) { - $version = (float) $data['@version']; - unset($data['@version']); - } + return self::createFromJson($responseContent, $statusCode, $curlInfoDebug); + } - if (isset($data['@rowCount'])) { - $rowCount = (int) $data['@rowCount']; - unset($data['@rowCount']); - } + /** + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeForbidden + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeInvalidAuthorization + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeMethodNotAllowed + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeNotAcceptableRequest + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeRequestFail + */ + private static function createFromJson(string $responseContent, int $statusCode, ?string $curlInfoDebug): FlexibeeResponse + { + $decoded = json_decode($responseContent, associative: true); + $data = is_array($decoded) ? $decoded : []; + $data = self::getArray($data, 'winstrom') ?? $data; + $results = self::getArray($data, 'results') ?? $data; + $message = isset($data['message']) && is_string($data['message']) ? $data['message'] : $responseContent; + + self::throwExceptionForStatusCode($statusCode, $curlInfoDebug); + + if (in_array($statusCode, [500, 400], strict: true)) { + self::throwErrorFromResults($results, $statusCode, $message); + } + + return new FlexibeeResponse( + $statusCode, + self::getNumber($data, '@version'), + self::isSuccess($data, $statusCode), + $message, + self::getInt($data, '@rowCount') ?? 0, + self::getInt($data, '@globalVersion'), + $results, + self::getArray($data, 'stats') ?? [], + ); + } - if (isset($data['@globalVersion'])) { - $globalVersion = (int) $data['@globalVersion']; - unset($data['@globalVersion']); - } + /** + * @param array $data + * @return array|null + */ + private static function getArray(array $data, string $key): ?array + { + return isset($data[$key]) && is_array($data[$key]) ? $data[$key] : null; + } - if (isset($data['message'])) { - $message = $data['message']; - unset($data['message']); - } + /** + * @param array $data + */ + private static function getNumber(array $data, string $key): ?float + { + return isset($data[$key]) && is_numeric($data[$key]) ? (float) $data[$key] : null; + } - if (isset($data['stats'])) { - $statistics = $data['stats']; - unset($data['stats']); - } + /** + * @param array $data + */ + private static function getInt(array $data, string $key): ?int + { + return isset($data[$key]) && is_numeric($data[$key]) ? (int) $data[$key] : null; + } + /** + * @param array $data + */ + private static function isSuccess(array $data, int $statusCode): bool + { if (isset($data['success'])) { - $success = (isset($data['success']) && ($data['success'] === 'true' || $data['success'] === true)); - unset($data['success']); - } elseif(in_array($statusCode, [200, 201], true)) { - $success = true; - } - - if ($statusCode === 401) { - throw new EcomailFlexibeeInvalidAuthorization('Uživatel se musí pro provedení dané operace přihlásit.'); + return $data['success'] === 'true' || $data['success'] === true; } - if ($statusCode === 403) { - if($curlInfoDebug !== null) { - throw new EcomailFlexibeeForbidden($curlInfoDebug); - } else { - throw new EcomailFlexibeeForbidden('Uživatel na tuto operaci nemá oprávnění. Tato chyba se zobrazí i v případě, že danou operaci neumožňuje licence.'); - } - } - - if ($statusCode === 406) { - throw new EcomailFlexibeeNotAcceptableRequest('Cílový formát není nad konkrétním zdrojem podporovaný (např. export adresáře jako ISDOC).'); - } - - if ($statusCode === 405) { - throw new EcomailFlexibeeMethodNotAllowed(); - } + return in_array($statusCode, [200, 201], strict: true); + } - if (in_array($statusCode, [500, 400], true)) { - foreach ($results as $resultData) { - if (!isset($resultData['errors'])) { - continue; - } + /** + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeForbidden + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeInvalidAuthorization + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeMethodNotAllowed + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeNotAcceptableRequest + */ + private static function throwExceptionForStatusCode(int $statusCode, ?string $curlInfoDebug): void + { + match ($statusCode) { + 401 => throw new EcomailFlexibeeInvalidAuthorization('Uživatel se musí pro provedení dané operace přihlásit.'), + 403 => throw new EcomailFlexibeeForbidden( + $curlInfoDebug ?? 'Uživatel na tuto operaci nemá oprávnění. Tato chyba se zobrazí i v případě, že danou operaci neumožňuje licence.', + ), + 405 => throw new EcomailFlexibeeMethodNotAllowed(), + 406 => throw new EcomailFlexibeeNotAcceptableRequest( + 'Cílový formát není nad konkrétním zdrojem podporovaný (např. export adresáře jako ISDOC).', + ), + default => null, + }; + } + /** + * @param array $results + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeRequestFail + */ + private static function throwErrorFromResults(array $results, int $statusCode, string $message): never + { + foreach ($results as $resultData) { + if (is_array($resultData) && isset($resultData['errors']) && is_array($resultData['errors'])) { self::throwErrorMessage($resultData['errors'], $statusCode); } - - throw new EcomailFlexibeeRequestFail($message); } - return new FlexibeeResponse($statusCode, $version, $success, $message, $rowCount, $globalVersion, $results, $statistics); + throw new EcomailFlexibeeRequestFail($message); } /** - * @param array $errors + * @param array $errors * @throws \EcomailFlexibee\Exception\EcomailFlexibeeRequestFail */ private static function throwErrorMessage(array $errors, int $statusCode): void { - foreach ($errors as $error) { - $messageLines = []; + $error = reset($errors); - if (isset($error['code'])) { - $messageLines[] = sprintf('code: %s',$error['code']); - } - - if (isset($error['for'])) { - $messageLines[] = sprintf('for attribute: %s', $error['for']); - } + if (!is_array($error)) { + return; + } - if (isset($error['path'])) { - $messageLines[] = sprintf('path: %s',$error['path']); - } + $messageLines = []; - if (isset($error['message'])) { - $messageLines[] = sprintf('message: %s', $error['message']); + foreach (self::ERROR_FIELD_LABELS as [$field, $label]) { + if (isset($error[$field]) && is_scalar($error[$field])) { + $messageLines[] = sprintf('%s: %s', $label, $error[$field]); } - - throw new EcomailFlexibeeRequestFail(implode("\n", $messageLines), $statusCode); } + + throw new EcomailFlexibeeRequestFail(implode("\n", $messageLines), $statusCode); } } diff --git a/src/Http/ResponseHydrator.php b/src/Http/ResponseHydrator.php index e7becaa..9f5c2d7 100644 --- a/src/Http/ResponseHydrator.php +++ b/src/Http/ResponseHydrator.php @@ -9,13 +9,17 @@ use EcomailFlexibee\Exception\EcomailFlexibeeRequestFail; use EcomailFlexibee\Http\Response\Response; use EcomailFlexibee\Result\EvidenceResult; + +use function array_filter; use function array_map; use function count; +use function is_array; +use function is_string; class ResponseHydrator { - public function __construct(private Config $config) + public function __construct(private readonly Config $config) { } @@ -25,95 +29,129 @@ public function __construct(private Config $config) */ public function convertResponseToEvidenceResults(Response $response): array { - $data = $response->getData(); - $hasErrors = isset($data[0], $data[0]['errors']); + $data = $this->getDataOrThrowRequestFail($response); + $evidenceData = $this->getEvidenceData($data); - if ($hasErrors || (isset($data['success']) && $data['success'] === 'false')) { + return $evidenceData === null + ? [new EvidenceResult($this->getDataOrStatistics($response, $data))] + : $this->createEvidenceResults($evidenceData); + } - if ($hasErrors && !isset($data['message'])) { - $data['message'] = ''; + /** + * @return array + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeRequestFail + */ + public function convertResponseToPaginatedEvidenceResults(Response $response): array + { + $data = $this->getDataOrThrowRequestFail($response); + $evidenceData = $this->getEvidenceData($data); - foreach ($data[0]['errors'] as $errors) { - $data['message'] .= "\n" . $errors['message']; - } - } + return [ + 'data' => $evidenceData === null + ? [new EvidenceResult($this->getDataOrStatistics($response, $data))] + : $this->createEvidenceResults($evidenceData), + 'row_count' => $response->getRowCount(), + ]; + } - throw new EcomailFlexibeeRequestFail($data['message'], $response->getStatusCode()); - } + public function convertResponseToEvidenceResult(Response $response, bool $throwException): EvidenceResult + { + $data = $response->getData(); + + $evidenceData = $this->getEvidenceData($data); - if (!isset($data[$this->config->getEvidence()])) { - if (count($data) === 0) { - $data = $response->getStatistics(); - $data['status_code'] = $response->getStatusCode(); - $data['message'] = $response->getMessage(); - $data['version'] = $response->getVersion(); - $data['row_count'] = $response->getRowCount(); + if ($response->getStatusCode() === 404 || $evidenceData === null) { + if ($throwException) { + throw new EcomailFlexibeeNoEvidenceResult(); } - return [new EvidenceResult($data)]; + return count($data) !== 0 + ? new EvidenceResult($data) + : new EvidenceResult([]); } - return array_map(static fn (array $data) => new EvidenceResult($data), $data[$this->config->getEvidence()]); + return new EvidenceResult($evidenceData); } /** * @return array * @throws \EcomailFlexibee\Exception\EcomailFlexibeeRequestFail */ - public function convertResponseToPaginatedEvidenceResults(Response $response): array + private function getDataOrThrowRequestFail(Response $response): array { $data = $response->getData(); - $hasErrors = isset($data[0], $data[0]['errors']); + $errors = isset($data[0]) && is_array($data[0]) && isset($data[0]['errors']) && is_array($data[0]['errors']) + ? $data[0]['errors'] + : null; - if ($hasErrors || (isset($data['success']) && $data['success'] === 'false')) { + if ($errors === null && (!isset($data['success']) || $data['success'] !== 'false')) { + return $data; + } - if ($hasErrors && !isset($data['message'])) { - $data['message'] = ''; + $message = $data['message'] ?? null; - foreach ($data[0]['errors'] as $errors) { - $data['message'] .= "\n" . $errors['message']; - } - } + throw new EcomailFlexibeeRequestFail( + is_string($message) ? $message : $this->createErrorMessage($errors ?? []), + $response->getStatusCode(), + ); + } - throw new EcomailFlexibeeRequestFail($data['message'], $response->getStatusCode()); - } + /** + * @param array $errors + */ + private function createErrorMessage(array $errors): string + { + $message = ''; - if (!isset($data[$this->config->getEvidence()])) { - if (count($data) === 0) { - $data = $response->getStatistics(); - $data['status_code'] = $response->getStatusCode(); - $data['message'] = $response->getMessage(); - $data['version'] = $response->getVersion(); - $data['row_count'] = $response->getRowCount(); + foreach ($errors as $error) { + if (is_array($error) && isset($error['message']) && is_string($error['message'])) { + $message .= "\n" . $error['message']; } - - return [ - 'row_count' => $response->getRowCount(), - 'data' => [new EvidenceResult($data)], - ]; } - return [ - 'row_count' => $response->getRowCount(), - 'data' => array_map(static fn (array $data) => new EvidenceResult($data), $data[$this->config->getEvidence()]), - ]; + return $message; } - public function convertResponseToEvidenceResult(Response $response, bool $throwException): EvidenceResult + /** + * @param array $data + * @return array + */ + private function getDataOrStatistics(Response $response, array $data): array { - $data = $response->getData(); + if (count($data) !== 0) { + return $data; + } - if ($response->getStatusCode() === 404 || !isset($data[$this->config->getEvidence()])) { - if ($throwException) { - throw new EcomailFlexibeeNoEvidenceResult(); - } + $data = $response->getStatistics(); + $data['status_code'] = $response->getStatusCode(); + $data['message'] = $response->getMessage(); + $data['version'] = $response->getVersion(); + $data['row_count'] = $response->getRowCount(); - return count($data) !== 0 - ? new EvidenceResult($data) - : new EvidenceResult([]); - } + return $data; + } - return new EvidenceResult($data[$this->config->getEvidence()]); + /** + * @param array $data + * @return array|null + */ + private function getEvidenceData(array $data): ?array + { + $evidenceData = $data[$this->config->getEvidence()] ?? null; + + return is_array($evidenceData) ? $evidenceData : null; + } + + /** + * @param array $evidenceData + * @return array<\EcomailFlexibee\Result\EvidenceResult> + */ + private function createEvidenceResults(array $evidenceData): array + { + return array_map( + static fn (array $item): EvidenceResult => new EvidenceResult($item), + array_filter($evidenceData, is_array(...)), + ); } } diff --git a/src/Http/UrlBuilder.php b/src/Http/UrlBuilder.php index 518a214..9ba122b 100644 --- a/src/Http/UrlBuilder.php +++ b/src/Http/UrlBuilder.php @@ -11,6 +11,7 @@ use Purl\Path; use Purl\Query; use Purl\Url; + use function array_merge; use function http_build_query; use function sprintf; @@ -18,11 +19,11 @@ class UrlBuilder extends Url { - private string $company; + private readonly string $company; - private string $evidence; + private readonly string $evidence; - private ParameterValidator $validator; + private readonly ParameterValidator $validator; public function __construct(Config $config, ?ParserInterface $parser = null) { @@ -148,7 +149,7 @@ public function getUrl(): string /** * @param array $uriParams */ - public function createUri(int|string|null $filterQueryOrId, array $uriParams): string + public function createUri(null|int|string $filterQueryOrId, array $uriParams): string { if ($filterQueryOrId === null) { $this->setPath($this->buildPathForOnlyEvidence()); diff --git a/src/Result/EvidenceResult.php b/src/Result/EvidenceResult.php index 3de4133..b6b9895 100644 --- a/src/Result/EvidenceResult.php +++ b/src/Result/EvidenceResult.php @@ -4,18 +4,18 @@ namespace EcomailFlexibee\Result; -final class EvidenceResult +final readonly class EvidenceResult { /** - * @param array $data + * @param array $data */ public function __construct(private array $data) { } /** - * @return array + * @return array */ public function getData(): array { diff --git a/src/Validator/ParameterValidator.php b/src/Validator/ParameterValidator.php index aa21153..2d66dd8 100644 --- a/src/Validator/ParameterValidator.php +++ b/src/Validator/ParameterValidator.php @@ -5,30 +5,25 @@ namespace EcomailFlexibee\Validator; use EcomailFlexibee\Exception\EcomailFlexibeeInvalidRequestParameter; -use Rakit\Validation\Validator; -final class ParameterValidator extends Validator +use function mb_strlen; +use function mb_strtoupper; + +final class ParameterValidator { + private const CODE_MAX_LENGTH = 20; + + /** + * @throws \EcomailFlexibee\Exception\EcomailFlexibeeInvalidRequestParameter + */ public function validateFlexibeeRequestCodeParameter(string $code): void { - $validation = $this->make( - [ - 'code' => $code, - ], - [ - 'code' => 'max:20|uppercase', - ], - [ - 'code' => 'Parameter code must have a maximum of 20 characters and must be an uppercase', - ], - ); - - $validation->validate(); - - if ($validation->fails()) { - throw new EcomailFlexibeeInvalidRequestParameter($validation); + if (mb_strlen($code) <= self::CODE_MAX_LENGTH && mb_strtoupper($code) === $code) { + return; } + + throw new EcomailFlexibeeInvalidRequestParameter('Parameter code must have a maximum of 20 characters and must be an uppercase'); } } diff --git a/tests/ClientOfflineTest.php b/tests/ClientOfflineTest.php new file mode 100644 index 0000000..389847d --- /dev/null +++ b/tests/ClientOfflineTest.php @@ -0,0 +1,212 @@ + 'DEMO', 'nazev' => 'Demo s.r.o.']; + $client = $this->createClient($this->respondWith(['companies' => ['company' => $company]])); + + Assert::assertSame($company, $client->getCompany()); + } + + public function testGetCompanyThrowsWhenTheCompanyIsMissing(): void + { + $client = $this->createClient($this->respondWith(['companies' => ['company' => ['dbNazev' => 'other']]])); + + $this->expectException(EcomailFlexibeeNoEvidenceResult::class); + + $client->getCompany(); + } + + public function testGetAuthAndRefreshTokenPostsCredentials(): void + { + $httpClient = Mockery::mock(HttpClient::class); + $httpClient->shouldReceive('request') + ->once() + ->with( + Mockery::type('string'), + Method::POST, + [], + ['password' => 'pass', 'username' => 'user'], + ['Content-Type: application/x-www-form-urlencoded'], + Mockery::type(ClientConfig::class), + ) + ->andReturn($this->createResponse(['authSessionId' => 'abc'])); + + Assert::assertSame(['authSessionId' => 'abc'], $this->createClient($httpClient)->getAuthAndRefreshToken()->getData()); + } + + public function testGetByIdAndGetByCodeReturnTheEvidence(): void + { + $client = $this->createClient($this->respondWith(['adresar' => [['id' => '1', 'kod' => 'A']]])); + + Assert::assertSame([['id' => '1', 'kod' => 'A']], $client->getById(1)->getData()); + Assert::assertSame([['id' => '1', 'kod' => 'A']], $client->getByCode('A')->getData()); + } + + public function testFindByIdAndFindByCodeReturnEmptyResultWhenMissing(): void + { + $client = $this->createClient($this->respondWith([], statusCode: 404)); + + Assert::assertSame([], $client->findById(1)->getData()); + Assert::assertSame([], $client->findByCode('A')->getData()); + } + + public function testSaveSendsEvidenceDataAndReturnsTheResult(): void + { + $httpClient = Mockery::mock(HttpClient::class); + $httpClient->shouldReceive('request') + ->once() + ->with( + Mockery::pattern('/dry-run=true/'), + Method::PUT, + ['adresar' => ['kod' => 'A', 'id' => 5]], + [], + [], + Mockery::type(ClientConfig::class), + ) + ->andReturn($this->createResponse([['id' => '5']])); + + $response = $this->createClient($httpClient)->save(['kod' => 'A'], id: 5, dryRun: true); + + Assert::assertTrue($response->isSuccess()); + Assert::assertSame(1, $response->getRowCount()); + Assert::assertSame([['id' => '5']], $response->getData()); + } + + public function testSaveThrowsWhenNothingWasCreatedOrUpdated(): void + { + $response = new FlexibeeResponse( + 200, + version: null, + success: true, + message: 'Nothing saved', + rowCount: 0, + data: [], + statistics: ['created' => '0', 'updated' => '0'], + ); + $client = $this->createClient($this->respondWithResponse($response)); + + $this->expectException(EcomailFlexibeeSaveFailed::class); + $this->expectExceptionMessage('(200) Nothing saved'); + + $client->save(['kod' => 'A'], id: null); + } + + public function testSaveThrowsWhenTheResultIsNotSuccessful(): void + { + $client = $this->createClient($this->respondWith(['message' => 'Invalid data', 'success' => 'no'])); + + $this->expectException(EcomailFlexibeeSaveFailed::class); + $this->expectExceptionMessage('Invalid data'); + + $client->save(['kod' => 'A'], id: null); + } + + public function testGetUserRelationsReturnsTheRelations(): void + { + $client = $this->createClient($this->respondWith(['adresar' => [['uzivatelske-vazby' => [['id' => '9']]]]])); + + Assert::assertSame([['id' => '9']], $client->getUserRelations(1)->getData()); + } + + public function testGetUserRelationsReturnsEmptyResultWithoutRelations(): void + { + $client = $this->createClient($this->respondWith(['adresar' => [['id' => '1']]])); + + Assert::assertSame([], $client->getUserRelations(1)->getData()); + } + + public function testAddUserRelationLinksTheObjectByCode(): void + { + $httpClient = Mockery::mock(HttpClient::class); + $httpClient->shouldReceive('request') + ->once() + ->with(Mockery::type('string'), Method::GET, [], [], [], Mockery::type(ClientConfig::class)) + ->andReturn($this->createResponse(['adresar' => [['kod' => 'B']]])); + $httpClient->shouldReceive('request') + ->once() + ->with( + Mockery::type('string'), + Method::PUT, + Mockery::on(static fn (array $postFields): bool => $postFields === [ + 'adresar' => [ + 'id' => 1, + 'uzivatelske-vazby' => [ + 'uzivatelska-vazba' => [ + 'cena' => 10.5, + 'evidenceType' => 'adresar', + 'object' => 'code:B', + 'popis' => 'note', + 'vazbaTyp' => 3, + ], + ], + ], + ]), + [], + [], + Mockery::type(ClientConfig::class), + ) + ->andReturn($this->createResponse([['id' => '1']])); + + $this->createClient($httpClient)->addUserRelation(1, 2, 10.5, 3, 'note'); + } + + public function testCountInEvidenceReturnsTheRowCount(): void + { + $response = new FlexibeeResponse(200, version: null, success: true, rowCount: 12, data: []); + + Assert::assertSame(12, $this->createClient($this->respondWithResponse($response))->countInEvidence()); + } + + private function createClient(HttpClient $httpClient): Client + { + return new Client('https://demo.flexibee.eu', 'demo', 'user', 'pass', 'adresar', verifySSLCertificate: true, httpClient: $httpClient); + } + + /** + * @param array $data + */ + private function respondWith(array $data, int $statusCode = 200): HttpClient&MockInterface + { + return $this->respondWithResponse($this->createResponse($data, $statusCode)); + } + + private function respondWithResponse(FlexibeeResponse $response): HttpClient&MockInterface + { + $httpClient = Mockery::mock(HttpClient::class); + $httpClient->shouldReceive('request')->andReturn($response); + + return $httpClient; + } + + /** + * @param array $data + */ + private function createResponse(array $data, int $statusCode = 200): FlexibeeResponse + { + return new FlexibeeResponse($statusCode, version: null, success: true, rowCount: 0, data: $data); + } + +} diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 22741b1..5ae2338 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -9,35 +9,32 @@ use EcomailFlexibee\Exception\EcomailFlexibeeNoEvidenceResult; use EcomailFlexibee\Exception\EcomailFlexibeeRequestFail; use EcomailFlexibee\Http\Method; -use EcomailFlexibee\Result\EvidenceResult; use Faker\Factory; use Faker\Generator; +use Iterator; use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; + use function count; use function mb_substr; use function uniqid; +/** + * @group integration + */ final class ClientTest extends TestCase { private Client $client; - private Generator $faker; - - public function setUp(): void - { - parent::setUp(); - $this->faker = Factory::create(); - $this->client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, Config::EVIDENCE, false, null); - } + private Generator $faker; public function testGetCompanies(): void { /** @var array $companies */ $companies = $this->client->getCompanies()->getData(); Assert::assertArrayHasKey('companies', $companies); - Assert::assertTrue(count($companies) > 0); + Assert::assertNotEmpty($companies['companies']); } public function testGetCompany(): void @@ -55,7 +52,7 @@ public function testGetLoginFormUrl(): void public function testInvalidAuthorization(): void { - $client = new Client(Config::HOST, Config::COMPANY, 'xxx', 'xxx', Config::EVIDENCE, false, null); + $client = new Client(Config::HOST, Config::COMPANY, 'xxx', 'xxx', Config::EVIDENCE, verifySSLCertificate: false); $this->expectException(EcomailFlexibeeInvalidAuthorization::class); $client->findById($this->faker->numberBetween()); } @@ -66,7 +63,15 @@ public function testGetAuthTokenAndMakeSuccessCallWithSessionAuthId(): void Assert::assertArrayHasKey('refreshToken', $authToken); Assert::assertArrayHasKey('authSessionId', $authToken); Assert::assertArrayHasKey('csrfToken', $authToken); - $client = new Client(Config::HOST, Config::COMPANY, 'xxx', 'xxx', Config::EVIDENCE, false, $authToken['authSessionId']); + $client = new Client( + Config::HOST, + Config::COMPANY, + 'xxx', + 'xxx', + Config::EVIDENCE, + verifySSLCertificate: false, + authSessionId: $this->getString($authToken, 'authSessionId'), + ); Assert::assertNotEmpty($client->allInEvidence()); } @@ -77,11 +82,12 @@ public function testCount(): void public function testInvalidGetAuthToken(): void { - $client = new Client(Config::HOST, Config::COMPANY, 'xxx', 'xxx', Config::EVIDENCE, false, null); + $client = new Client(Config::HOST, Config::COMPANY, 'xxx', 'xxx', Config::EVIDENCE, verifySSLCertificate: false); $flexibeeResponse = $client->getAuthAndRefreshToken(); Assert::assertFalse($flexibeeResponse->isSuccess()); $data = $flexibeeResponse->getData(); Assert::assertArrayHasKey('errors', $data); + Assert::assertIsArray($data['errors']); Assert::assertArrayHasKey('reason', $data['errors']); } @@ -91,7 +97,7 @@ public function testGetChanges(): void return; } - $response = $this->client->getAllApiChanges(null); + $response = $this->client->getAllApiChanges(fromVersion: null); Assert::assertTrue($response->isSuccess()); $data = $response->getData(); Assert::assertArrayHasKey('changes', $data); @@ -99,18 +105,21 @@ public function testGetChanges(): void Assert::assertTrue($response->isSuccess()); } - public function testCRUDForCustomIds(): void + public function testCrudForCustomIds(): void { $evidenceData = [ 'nazev' => $this->faker->firstName, ]; - $id = (int) $this->client->save($evidenceData, null)->getData()[0]['id']; - $code = $this->client->getById($id)->getData()[0]['kod']; + $id = $this->getInt($this->getFirstItem($this->client->save($evidenceData, id: null)->getData()), 'id'); + $code = $this->getString($this->getFirstItem($this->client->getById($id)->getData()), 'kod'); $evidenceItem = $this->client->getByCode($code); Assert::assertCount(1, $evidenceItem->getData()); - Assert::assertEquals($id, (int) $evidenceItem->getData()[0]['id']); + Assert::assertSame($id, $this->getInt($this->getFirstItem($evidenceItem->getData()), 'id')); $evidenceItemFull = $this->client->getByCode($code, ['detail' => 'full']); - Assert::assertNotEquals(count($evidenceItem->getData()[0]), count($evidenceItemFull->getData()[0])); + Assert::assertNotCount( + count($this->getFirstItem($evidenceItem->getData())), + $this->getFirstItem($evidenceItemFull->getData()), + ); $this->client->deleteByCode($code); Assert::assertCount(0, $this->client->findByCode($code)->getData()); } @@ -120,11 +129,11 @@ public function testEvidenceGetOnlyCustomFields(): void $evidenceData = [ 'nazev' => $this->faker->firstName, ]; - $id = (int) $this->client->save($evidenceData, null)->getData()[0]['id']; + $id = $this->getInt($this->getFirstItem($this->client->save($evidenceData, id: null)->getData()), 'id'); $evidenceData = $this->client->getById($id, ['detail' => 'custom:id,email,kontakty(primarni,email)'])->getData()[0]; Assert::assertEquals([ - 'id' => $id, 'email' => '', + 'id' => $id, 'kontakty' => [], ], $evidenceData, @@ -144,15 +153,15 @@ public function testEvidenceGetOnlyCustomFields(): void * @throws \EcomailFlexibee\Exception\EcomailFlexibeeRequestFail * @throws \EcomailFlexibee\Exception\EcomailFlexibeeSaveFailed */ - public function testCRUDOperations(string $evidence, array $evidenceData, array $expectedDataAfterUpdate): void + public function testCrudOperations(string $evidence, array $evidenceData, array $expectedDataAfterUpdate): void { - $client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, $evidence, false, null); - $addressBookId = (int) $client->save($evidenceData, null)->getData()[0]['id']; + $client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, $evidence, verifySSLCertificate: false); + $addressBookId = $this->getInt($this->getFirstItem($client->save($evidenceData, id: null)->getData()), 'id'); $client->save($expectedDataAfterUpdate, $addressBookId); - $addressBookRefreshed = $client->getById($addressBookId); + $addressBookRefreshed = $this->getFirstItem($client->getById($addressBookId)->getData()); foreach ($expectedDataAfterUpdate as $key => $value) { - Assert::assertEquals($value, $addressBookRefreshed->getData()[0][$key]); + Assert::assertEquals($value, $addressBookRefreshed[$key]); } Assert::assertNotEmpty($client->getPdfById($addressBookId, [])); @@ -165,40 +174,39 @@ public function testCRUDOperations(string $evidence, array $evidenceData, array $client->getById($addressBookId); $this->expectException(EcomailFlexibeeRequestFail::class); $evidenceData = []; - $this->client->save($evidenceData, null); + $this->client->save($evidenceData, id: null); } /** - * @return array> + * @return \Iterator, array}> */ - public function getEvidences(): array + public function getEvidences(): Iterator { $faker = Factory::create(); $code = mb_substr($faker->uuid, 0, 20); $name = $faker->userName; - return [ + yield [ + 'adresar', [ - 'adresar', - [ - 'kod' => $code, - 'nazev' => $name, - ], - [ - 'nazev' => 'Adresar edited', - ], + 'kod' => $code, + 'nazev' => $name, ], [ - 'faktura-vydana', - [ - 'kod' => $code, - 'typDokl' => 'code:FAKTURA', - 'cisDosle' => '1234', - 'eetTypK' => 'eetTyp.ne', - ], - [ - 'cisDosle' => '1234', - ], + 'nazev' => 'Adresar edited', + ], + ]; + + yield [ + 'faktura-vydana', + [ + 'cisDosle' => '1234', + 'eetTypK' => 'eetTyp.ne', + 'kod' => $code, + 'typDokl' => 'code:FAKTURA', + ], + [ + 'cisDosle' => '1234', ], ]; } @@ -218,7 +226,7 @@ public function testAllAndChunkInEvidence(): void public function testSearchInEvidence(): void { - $client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, 'faktura-vydana', false, null); + $client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, 'faktura-vydana', verifySSLCertificate: false); $result = $client->searchInEvidence('(kod neq \'JAN\')', []); Assert::assertTrue(count($result) > 0); $this->expectException(EcomailFlexibeeRequestFail::class); @@ -227,42 +235,45 @@ public function testSearchInEvidence(): void public function testSearchInEvidencePaginated(): void { - $client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, 'faktura-vydana', false, null); + $client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, 'faktura-vydana', verifySSLCertificate: false); $result = $client->searchInEvidencePaginated('(kod neq \'JAN\')', []); Assert::assertIsInt($result['row_count']); Assert::assertTrue($result['row_count'] > 0); - Assert::assertTrue(count($result['data']) > 0); + Assert::assertIsArray($result['data']); + Assert::assertNotEmpty($result['data']); $this->expectException(EcomailFlexibeeRequestFail::class); $client->searchInEvidencePaginated('kod neq \'JAN\'', []); } public function testFindLastInEvidence(): void { - $client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, 'faktura-vydana', false, null); - $result = $client->findLastInEvidence(true); + $client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, 'faktura-vydana', verifySSLCertificate: false); + $result = $client->findLastInEvidence(fullDetail: true); + Assert::assertIsArray($result->getData()['faktura-vydana']); Assert::assertCount(1, $result->getData()['faktura-vydana']); } public function testSearchInEvidenceWithInvalidUrl(): void { - $client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, 'faktura-vydana', false, null); + $client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, 'faktura-vydana', verifySSLCertificate: false); $this->expectException(EcomailFlexibeeRequestFail::class); $client->searchInEvidence(' bla.json', []); } public function testDryRunRequest(): void { - $response = $this->client->save(['kod' => uniqid(), 'nazev' => 'SDSDXXXXX'], null, true); - $firstItem = $response->getData()[0]; - Assert::assertArrayHasKey('content', $firstItem); - Assert::assertArrayHasKey(Config::EVIDENCE, $firstItem['content']); - Assert::assertTrue((int) $firstItem['content'][Config::EVIDENCE]['id'] < 0); + $response = $this->client->save(['kod' => uniqid(), 'nazev' => 'SDSDXXXXX'], id: null, dryRun: true); + $content = $this->getFirstItem($response->getData())['content'] ?? null; + Assert::assertIsArray($content); + Assert::assertArrayHasKey(Config::EVIDENCE, $content); + Assert::assertIsArray($content[Config::EVIDENCE]); + Assert::assertLessThan(0, $this->getInt($content[Config::EVIDENCE], 'id')); } public function testMakeCustomRequest(): void { $results = $this->client->callRequest( - Method::get(Method::GET), + Method::GET, 'properties', [], [], @@ -275,62 +286,74 @@ public function testMakeCustomRequest(): void Assert::assertArrayHasKey('properties', $data->getData()); } - public function testRunBackendProcesses(): void - { - $this->markTestSkipped('Live tests is disabled here'); - $client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, 'pokladni-pohyb', false); - $results = $client->callRequest(Method::get(Method::POST), 'automaticke-parovani', [], [], []); - - foreach ($results as $result) { - $this->checkResponseStructure($result); - } - } - public function testUnknownCompany(): void { - $client = new Client(Config::HOST, 'xxx', Config::USERNAME, Config::PASSWORD, 'faktura-vydana', false); + $client = new Client(Config::HOST, 'xxx', Config::USERNAME, Config::PASSWORD, 'faktura-vydana', verifySSLCertificate: false); $this->expectException(EcomailFlexibeeRequestFail::class); - $client->save([], null); - + $client->save([], id: null); } public function testCreateSameEvidenceRecord(): void { $code = uniqid(); $data = [ - 'nazev' => $code, 'kod' => $code, + 'nazev' => $code, ]; - $this->client->save($data, null); + $this->client->save($data, id: null); $this->expectException(EcomailFlexibeeRequestFail::class); $this->expectExceptionCode(400); - $this->client->save($data, null); + $this->client->save($data, id: null); } public function testGetPropertiesForEvidence(): void { $responseData = $this->client->getPropertiesForEvidence()->getData(); Assert::assertArrayHasKey('properties', $responseData); + Assert::assertIsArray($responseData['properties']); Assert::assertArrayHasKey('property', $responseData['properties']); } - private function checkResponseStructure(EvidenceResult $result): void + protected function setUp(): void { - $requiredKeys = [ - 'created', - 'updated', - 'deleted', - 'skipped', - 'failed', - 'status_code', - 'message', - 'version', - 'row_count', - ]; + parent::setUp(); - foreach ($requiredKeys as $requiredKey) { - Assert::assertArrayHasKey($requiredKey, $result->getData()); - } + $this->faker = Factory::create(); + $this->client = new Client(Config::HOST, Config::COMPANY, Config::USERNAME, Config::PASSWORD, Config::EVIDENCE, verifySSLCertificate: false); + } + + /** + * @param array $data + * @return array + */ + private function getFirstItem(array $data): array + { + $item = $data[0] ?? null; + Assert::assertIsArray($item); + + return $item; + } + + /** + * @param array $data + */ + private function getString(array $data, string $key): string + { + $value = $data[$key] ?? null; + Assert::assertIsString($value); + + return $value; + } + + /** + * @param array $data + */ + private function getInt(array $data, string $key): int + { + $value = $data[$key] ?? null; + Assert::assertIsNumeric($value); + + return (int) $value; } } diff --git a/tests/CompanyClientTest.php b/tests/CompanyClientTest.php new file mode 100644 index 0000000..0aa5af6 --- /dev/null +++ b/tests/CompanyClientTest.php @@ -0,0 +1,68 @@ +shouldReceive('request') + ->once() + ->with( + Mockery::type('string'), + Method::GET, + [], + [], + [], + Mockery::on(static fn (ClientConfig $config): bool => $config->debugMode()), + ) + ->andReturn($response); + $client = new CompanyClient($this->createConfig(debugMode: true), $httpClient); + + Assert::assertSame($response, $client->backup()); + } + + public function testRestoreSendsTheBackupWithPut(): void + { + $response = new FlexibeeResponse(200, version: null, success: true); + $httpClient = Mockery::mock(HttpClient::class); + $httpClient->shouldReceive('request') + ->once() + ->with(Mockery::type('string'), Method::PUT, ['backup-data'], [], [], Mockery::type(ClientConfig::class)) + ->andReturn($response); + $client = new CompanyClient($this->createConfig(debugMode: false), $httpClient); + + Assert::assertSame($response, $client->restore('new-company', 'backup-data')); + } + + private function createConfig(bool $debugMode): ClientConfig + { + return new ClientConfig( + Config::HOST, + Config::COMPANY, + Config::USERNAME, + Config::PASSWORD, + 'c', + verifySSLCertificate: false, + debugMode: $debugMode, + ); + } + +} diff --git a/tests/Config.php b/tests/Config.php index 9816e21..210ddac 100644 --- a/tests/Config.php +++ b/tests/Config.php @@ -4,15 +4,17 @@ namespace EcomailFlexibeeTest; -use Consistence\ObjectPrototype; - -final class Config extends ObjectPrototype +final class Config { public const HOST = 'https://demo.flexibee.eu'; + public const COMPANY = 'demo'; + public const USERNAME = 'winstrom'; + public const PASSWORD = 'winstrom'; + public const EVIDENCE = 'adresar'; } diff --git a/tests/Enum/SearchQueryOperatorTest.php b/tests/Enum/SearchQueryOperatorTest.php index d162344..246e0ea 100644 --- a/tests/Enum/SearchQueryOperatorTest.php +++ b/tests/Enum/SearchQueryOperatorTest.php @@ -5,6 +5,7 @@ namespace EcomailFlexibeeTest\Enum; use EcomailFlexibee\Enum\SearchQueryOperator; +use Iterator; use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; @@ -16,43 +17,47 @@ final class SearchQueryOperatorTest extends TestCase */ public function testConvertOperators(string $query, string $expectedQuery): void { - Assert::assertEquals($expectedQuery, SearchQueryOperator::convertOperatorsInQuery($query)); + Assert::assertSame($expectedQuery, SearchQueryOperator::convertOperatorsInQuery($query)); } /** - * @return array + * @return \Iterator */ - public function getQueryStrings(): array + public function getQueryStrings(): Iterator { - return [ - [ - 'kod = \'JAN\'', - 'kod eq \'JAN\'', - ], - [ - 'kod=\'=JAN\'', - 'kod eq \'=JAN\'', - ], - [ - 'kod=\'=JAN=<>\'', - 'kod eq \'=JAN=<>\'', - ], - [ - 'kod<>\'JAN\'', - 'kod neq \'JAN\'', - ], - [ - 'datSplat<\'2018-12-04\'%20and%20zuctovano=false', - 'datSplat lt \'2018-12-04\' and zuctovano eq false', - ], - [ - 'datSplat<\'2018-12-04\' and zuctovano=false', - 'datSplat lt \'2018-12-04\' and zuctovano eq false', - ], - [ - 'sparovano = true and lastUpdate <= \'2018-12-11\'', - 'sparovano eq true and lastUpdate lte \'2018-12-11\'', - ], + yield [ + 'kod = \'JAN\'', + 'kod eq \'JAN\'', + ]; + + yield [ + 'kod=\'=JAN\'', + 'kod eq \'=JAN\'', + ]; + + yield [ + 'kod=\'=JAN=<>\'', + 'kod eq \'=JAN=<>\'', + ]; + + yield [ + 'kod<>\'JAN\'', + 'kod neq \'JAN\'', + ]; + + yield [ + 'datSplat<\'2018-12-04\'%20and%20zuctovano=false', + 'datSplat lt \'2018-12-04\' and zuctovano eq false', + ]; + + yield [ + 'datSplat<\'2018-12-04\' and zuctovano=false', + 'datSplat lt \'2018-12-04\' and zuctovano eq false', + ]; + + yield [ + 'sparovano = true and lastUpdate <= \'2018-12-11\'', + 'sparovano eq true and lastUpdate lte \'2018-12-11\'', ]; } diff --git a/tests/Http/CurlOptionsFactoryTest.php b/tests/Http/CurlOptionsFactoryTest.php new file mode 100644 index 0000000..8273992 --- /dev/null +++ b/tests/Http/CurlOptionsFactoryTest.php @@ -0,0 +1,139 @@ +create(new CurlRequest('https://demo.flexibee.eu/c/demo/adresar.json', Method::GET, [], [], []), $config); + + Assert::assertTrue($options[CURLOPT_SSL_VERIFYPEER]); + Assert::assertSame(2, $options[CURLOPT_SSL_VERIFYHOST]); + } + + public function testWrapsJsonEncodingFailureIntoRequestFail(): void + { + $config = new Config('https://demo.flexibee.eu', 'demo', 'user', 'pass', 'adresar', verifySSLCertificate: true, debugMode: false); + + try { + (new CurlOptionsFactory())->create( + new CurlRequest('https://demo.flexibee.eu/c/demo/adresar.json', Method::PUT, ['nazev' => "\xB1\x31"], [], []), + $config, + ); + Assert::fail('EcomailFlexibeeRequestFail was not thrown.'); + } catch (EcomailFlexibeeRequestFail $exception) { + Assert::assertInstanceOf(JsonException::class, $exception->getPrevious()); + } + } + + public function testRejectsAnEmptyUrl(): void + { + $config = new Config('https://demo.flexibee.eu', 'demo', 'user', 'pass', 'adresar', verifySSLCertificate: true, debugMode: false); + + $this->expectException(EcomailFlexibeeRequestFail::class); + + (new CurlOptionsFactory())->create(new CurlRequest('', Method::GET, [], [], []), $config); + } + + public function testUsesBasicAuthenticationWithoutSession(): void + { + $config = new Config('https://demo.flexibee.eu', 'demo', 'user', 'pass', 'adresar', verifySSLCertificate: false, debugMode: true); + + $options = (new CurlOptionsFactory())->create( + new CurlRequest('https://demo.flexibee.eu/c/demo/adresar.json', Method::DELETE, [], [], []), + $config, + ); + + Assert::assertSame('https://demo.flexibee.eu/c/demo/adresar.json', $options[CURLOPT_URL]); + Assert::assertSame('DELETE', $options[CURLOPT_CUSTOMREQUEST]); + Assert::assertSame(CURLAUTH_BASIC, $options[CURLOPT_HTTPAUTH]); + Assert::assertSame('user:pass', $options[CURLOPT_USERPWD]); + Assert::assertFalse($options[CURLOPT_SSL_VERIFYPEER]); + Assert::assertSame(0, $options[CURLOPT_SSL_VERIFYHOST]); + Assert::assertTrue($options[CURLINFO_HEADER_OUT]); + Assert::assertArrayNotHasKey(CURLOPT_HTTPHEADER, $options); + Assert::assertArrayNotHasKey(CURLOPT_POSTFIELDS, $options); + } + + public function testSessionAuthenticationSendsSessionHeader(): void + { + $config = new Config( + 'https://demo.flexibee.eu', + 'demo', + 'user', + 'pass', + 'adresar', + verifySSLCertificate: true, + authSessionId: 'session-id', + debugMode: false, + ); + + $options = (new CurlOptionsFactory())->create( + new CurlRequest('https://demo.flexibee.eu', Method::GET, [], [], ['Accept: application/json']), + $config, + ); + + Assert::assertSame(0, $options[CURLOPT_HTTPAUTH]); + Assert::assertArrayNotHasKey(CURLOPT_USERPWD, $options); + Assert::assertSame(['Accept: application/json', 'X-authSessionId: session-id'], $options[CURLOPT_HTTPHEADER]); + } + + public function testEncodesPostFieldsAsWinstromJson(): void + { + $config = new Config('https://demo.flexibee.eu', 'demo', 'user', 'pass', 'adresar', verifySSLCertificate: true, debugMode: false); + + $options = (new CurlOptionsFactory())->create( + new CurlRequest('https://demo.flexibee.eu', Method::PUT, ['adresar' => ['kod' => 'A']], [], []), + $config, + ); + + Assert::assertSame('{"winstrom":{"adresar":{"kod":"A"}}}', $options[CURLOPT_POSTFIELDS]); + } + + public function testQueryParametersReplacePostFields(): void + { + $config = new Config('https://demo.flexibee.eu', 'demo', 'user', 'pass', 'adresar', verifySSLCertificate: true, debugMode: false); + + $request = new CurlRequest('https://demo.flexibee.eu', Method::POST, ['a' => 'b'], ['username' => 'u', 'password' => 'p']); + + $options = (new CurlOptionsFactory())->create($request, $config); + + Assert::assertSame('username=u&password=p', $options[CURLOPT_POSTFIELDS]); + } + +} diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php new file mode 100644 index 0000000..19d9a18 --- /dev/null +++ b/tests/Http/HttpClientTest.php @@ -0,0 +1,44 @@ +request($this->getFixtureUrl(), Method::GET, [], [], [], $this->createConfig(debugMode: false)); + + Assert::assertSame(1.0, $response->getVersion()); + Assert::assertSame(['@version' => '1.0', 'adresar' => [['id' => '1']]], $response->getData()); + } + + public function testDebugModeDoesNotBreakTheRequest(): void + { + $response = (new HttpClient())->request($this->getFixtureUrl(), Method::GET, [], [], [], $this->createConfig(debugMode: true)); + + Assert::assertSame(['@version' => '1.0', 'adresar' => [['id' => '1']]], $response->getData()); + } + + private function getFixtureUrl(): string + { + return sprintf('file://%s', realpath(__DIR__ . '/fixtures/adresar.json')); + } + + private function createConfig(bool $debugMode): Config + { + return new Config('https://demo.flexibee.eu', 'demo', 'user', 'pass', 'adresar', verifySSLCertificate: false, debugMode: $debugMode); + } + +} diff --git a/tests/Http/QueryBuilderTest.php b/tests/Http/QueryBuilderTest.php index ee99e41..ff1f82e 100644 --- a/tests/Http/QueryBuilderTest.php +++ b/tests/Http/QueryBuilderTest.php @@ -10,34 +10,18 @@ use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; -class QueryBuilderTest extends TestCase +final class QueryBuilderTest extends TestCase { private UrlBuilder $urlBuilder; - public function setUp(): void - { - parent::setUp(); - - $this->urlBuilder = new UrlBuilder( - new Config( - TestConfig::HOST, - TestConfig::COMPANY, - TestConfig::USERNAME, - TestConfig::PASSWORD, - TestConfig::EVIDENCE, - false, - ), - ); - } - public function testCreateUriByCodeOnly(): void { - Assert::assertEquals( + Assert::assertSame( 'https://demo.flexibee.eu/c/demo/adresar/(kod%20eq%20\'CODE:TEST\').json?limit=0', $this->urlBuilder->createUriByCodeOnly('CODE:TEST', []), ); - Assert::assertEquals( + Assert::assertSame( 'https://demo.flexibee.eu/c/demo/adresar/(kod%20eq%20\'CODE:TEST\').json?limit=0&test=1', $this->urlBuilder->createUriByCodeOnly('CODE:TEST', ['test' => true]), ); @@ -45,7 +29,7 @@ public function testCreateUriByCodeOnly(): void public function testCreateUriByEvidenceOnly(): void { - Assert::assertEquals( + Assert::assertSame( 'https://demo.flexibee.eu/c/demo/adresar.json?limit=0', $this->urlBuilder->createUriByEvidenceOnly([]), ); @@ -53,7 +37,7 @@ public function testCreateUriByEvidenceOnly(): void public function testCreateUriByCustomId(): void { - Assert::assertEquals( + Assert::assertSame( 'https://demo.flexibee.eu/c/demo/adresar/code:TEST.json?limit=0', $this->urlBuilder->createFilterQuery('code:TEST', []), ); @@ -61,14 +45,14 @@ public function testCreateUriByCustomId(): void public function testCreateUriPdf(): void { - Assert::assertEquals( + Assert::assertSame( 'https://demo.flexibee.eu/c/demo/adresar/11.pdf?limit=0', $this->urlBuilder->createPdfUrl(11, []), ); $queryParams = []; $queryParams['report-name'] = 'test'; $queryParams['report-lang'] = 'en'; - Assert::assertEquals( + Assert::assertSame( 'https://demo.flexibee.eu/c/demo/adresar/11.pdf?limit=0&report-name=test&report-lang=en', $this->urlBuilder->createPdfUrl(11, $queryParams), ); @@ -76,22 +60,38 @@ public function testCreateUriPdf(): void public function testCreateUriByEvidenceForSearchQuery(): void { - Assert::assertEquals( + Assert::assertSame( 'https://demo.flexibee.eu/c/demo/adresar/1.json?limit=10000', - $this->urlBuilder->createFilterQuery('1', ['limit' => 10000]), + $this->urlBuilder->createFilterQuery('1', ['limit' => 10_000]), ); - Assert::assertEquals( + Assert::assertSame( 'https://demo.flexibee.eu/c/demo/adresar/1.json?limit=10000&start=0', - $this->urlBuilder->createFilterQuery('1', ['limit' => 10000, 'start' => 0]), + $this->urlBuilder->createFilterQuery('1', ['limit' => 10_000, 'start' => 0]), ); } public function testCreateUriByEvidenceForSearchQueryWithouLimitParameter(): void { - Assert::assertEquals( + Assert::assertSame( 'https://demo.flexibee.eu/c/demo/adresar/1.json?limit=0', $this->urlBuilder->createFilterQuery('1', []), ); } + protected function setUp(): void + { + parent::setUp(); + + $this->urlBuilder = new UrlBuilder( + new Config( + TestConfig::HOST, + TestConfig::COMPANY, + TestConfig::USERNAME, + TestConfig::PASSWORD, + TestConfig::EVIDENCE, + verifySSLCertificate: false, + ), + ); + } + } diff --git a/tests/Http/ResponseFactoryTest.php b/tests/Http/ResponseFactoryTest.php new file mode 100644 index 0000000..dab8935 --- /dev/null +++ b/tests/Http/ResponseFactoryTest.php @@ -0,0 +1,182 @@ +expectException(EcomailFlexibeeRequestFail::class); + + ResponseFactory::createFromOutput( + self::URL, + Method::GET, + responseContent: null, + statusCode: 200, + errorNumber: CURLE_OK, + errorMessage: null, + curlInfoDebug: null, + ); + } + + public function testCurlErrorThrowsConnectionFail(): void + { + $this->expectException(EcomailFlexibeeConnectionFail::class); + $this->expectExceptionMessage('cURL error (7): Connection refused'); + + ResponseFactory::createFromOutput(self::URL, Method::GET, '', 0, CURLE_COULDNT_CONNECT, 'Connection refused', curlInfoDebug: null); + } + + public function testPdfUrlReturnsPdfResponse(): void + { + $response = ResponseFactory::createFromOutput( + 'https://demo.flexibee.eu/c/demo/faktura-vydana/1.pdf', + Method::GET, + '%PDF', + 200, + CURLE_OK, + errorMessage: null, + curlInfoDebug: null, + ); + + Assert::assertInstanceOf(FlexibeePdfResponse::class, $response); + Assert::assertSame(['pdfContent' => '%PDF'], $response->getData()); + } + + public function testBackupUrlReturnsBackupResponse(): void + { + $response = ResponseFactory::createFromOutput( + 'https://demo.flexibee.eu/c/demo/backup', + Method::GET, + 'dump', + 200, + CURLE_OK, + errorMessage: null, + curlInfoDebug: null, + ); + + Assert::assertInstanceOf(FlexibeeBackupResponse::class, $response); + Assert::assertSame(['backupContent' => 'dump'], $response->getData()); + } + + public function testParsesWinstromMetadata(): void + { + $content = '{"winstrom":{"@version":"1.5","@rowCount":"3","@globalVersion":"42","message":"ok","stats":{"created":"1"},' + . '"success":"true","results":[{"id":"1"}]}}'; + + $response = ResponseFactory::createFromOutput(self::URL, Method::GET, $content, 201, CURLE_OK, errorMessage: null, curlInfoDebug: null); + + Assert::assertSame(201, $response->getStatusCode()); + Assert::assertSame(1.5, $response->getVersion()); + Assert::assertTrue($response->isSuccess()); + Assert::assertSame('ok', $response->getMessage()); + Assert::assertSame(3, $response->getRowCount()); + Assert::assertSame(42, $response->getGlobalVersion()); + Assert::assertSame([['id' => '1']], $response->getData()); + Assert::assertSame(['created' => '1'], $response->getStatistics()); + } + + public function testUsesDefaultsWhenMetadataIsMissing(): void + { + $response = ResponseFactory::createFromOutput(self::URL, Method::GET, '{"adresar":[]}', 200, CURLE_OK, errorMessage: null, curlInfoDebug: null); + + Assert::assertNull($response->getVersion()); + Assert::assertTrue($response->isSuccess()); + Assert::assertSame('{"adresar":[]}', $response->getMessage()); + Assert::assertSame(0, $response->getRowCount()); + Assert::assertNull($response->getGlobalVersion()); + Assert::assertSame(['adresar' => []], $response->getData()); + Assert::assertSame([], $response->getStatistics()); + } + + public function testExplicitSuccessFlagOverridesStatusCode(): void + { + $response = ResponseFactory::createFromOutput(self::URL, Method::GET, '{"success":false}', 200, CURLE_OK, errorMessage: null, curlInfoDebug: null); + + Assert::assertFalse($response->isSuccess()); + } + + public function testNonJsonContentProducesEmptyData(): void + { + $response = ResponseFactory::createFromOutput(self::URL, Method::GET, 'not json', 302, CURLE_OK, errorMessage: null, curlInfoDebug: null); + + Assert::assertFalse($response->isSuccess()); + Assert::assertSame([], $response->getData()); + Assert::assertSame('not json', $response->getMessage()); + } + + /** + * @dataProvider getStatusCodeExceptions + * @param class-string<\Throwable> $exceptionClass + */ + public function testStatusCodeThrowsMatchingException(int $statusCode, ?string $curlInfoDebug, string $exceptionClass, string $message): void + { + $this->expectException($exceptionClass); + $this->expectExceptionMessage($message); + + ResponseFactory::createFromOutput(self::URL, Method::GET, '{}', $statusCode, CURLE_OK, errorMessage: null, curlInfoDebug: $curlInfoDebug); + } + + /** + * @return \Iterator, string}> + */ + public function getStatusCodeExceptions(): Iterator + { + yield 'unauthorized' => [401, null, EcomailFlexibeeInvalidAuthorization::class, 'přihlásit']; + yield 'forbidden' => [403, null, EcomailFlexibeeForbidden::class, 'nemá oprávnění']; + yield 'forbidden with debug info' => [403, '"GET /c/demo"', EcomailFlexibeeForbidden::class, 'GET /c/demo']; + yield 'method not allowed' => [405, null, EcomailFlexibeeMethodNotAllowed::class, '']; + yield 'not acceptable' => [406, null, EcomailFlexibeeNotAcceptableRequest::class, 'ISDOC']; + } + + public function testBadRequestThrowsFirstErrorWithStatusCode(): void + { + $content = '{"winstrom":{"results":[{"id":"1"},{"errors":[{"code":"E1","for":"kod","path":"adresar","message":"Duplicate","extra":[]},' + . '{"message":"Second"}]}]}}'; + + $this->expectException(EcomailFlexibeeRequestFail::class); + $this->expectExceptionMessage("code: E1\nfor attribute: kod\npath: adresar\nmessage: Duplicate"); + $this->expectExceptionCode(400); + + ResponseFactory::createFromOutput(self::URL, Method::PUT, $content, 400, CURLE_OK, errorMessage: null, curlInfoDebug: null); + } + + public function testServerErrorWithoutErrorDetailsThrowsResponseMessage(): void + { + $this->expectException(EcomailFlexibeeRequestFail::class); + $this->expectExceptionMessage('Internal failure'); + + ResponseFactory::createFromOutput( + self::URL, + Method::GET, + '{"message":"Internal failure","results":[{"errors":["invalid"]}]}', + 500, + CURLE_OK, + errorMessage: null, + curlInfoDebug: null, + ); + } + +} diff --git a/tests/Http/ResponseHydratorTest.php b/tests/Http/ResponseHydratorTest.php new file mode 100644 index 0000000..9b3b1f9 --- /dev/null +++ b/tests/Http/ResponseHydratorTest.php @@ -0,0 +1,151 @@ + $data + */ + public function testFailedResponseThrowsRequestFail(array $data, string $expectedMessage): void + { + $this->expectException(EcomailFlexibeeRequestFail::class); + $this->expectExceptionMessage($expectedMessage); + $this->expectExceptionCode(400); + + $this->createHydrator()->convertResponseToEvidenceResults($this->createResponse($data, statusCode: 400)); + } + + /** + * @return \Iterator, string}> + */ + public function getFailedResponses(): Iterator + { + yield 'errors without message' => [[['errors' => [['message' => 'First'], 'ignored', ['message' => 'Second']]]], "\nFirst\nSecond"]; + yield 'errors with message' => [['message' => 'Explicit', ['errors' => []]], 'Explicit']; + yield 'success false flag' => [['message' => 'Rejected', 'success' => 'false'], 'Rejected']; + yield 'success false without message' => [['success' => 'false'], '']; + } + + public function testFailedPaginatedResponseThrowsRequestFail(): void + { + $this->expectException(EcomailFlexibeeRequestFail::class); + + $this->createHydrator()->convertResponseToPaginatedEvidenceResults($this->createResponse(['success' => 'false', 'message' => 'x'])); + } + + public function testEvidenceItemsBecomeEvidenceResults(): void + { + $results = $this->createHydrator()->convertResponseToEvidenceResults( + $this->createResponse(['adresar' => [['id' => '1'], 'not-an-item', ['id' => '2']]]), + ); + + Assert::assertEquals([0 => new EvidenceResult(['id' => '1']), 2 => new EvidenceResult(['id' => '2'])], $results); + } + + public function testResponseWithoutEvidenceIsReturnedAsSingleResult(): void + { + $results = $this->createHydrator()->convertResponseToEvidenceResults($this->createResponse(['properties' => []])); + + Assert::assertEquals([new EvidenceResult(['properties' => []])], $results); + } + + public function testEmptyResponseIsReplacedByStatistics(): void + { + $response = new FlexibeeResponse(201, 1.0, success: true, message: 'done', rowCount: 4, data: [], statistics: ['created' => '1']); + + $results = $this->createHydrator()->convertResponseToEvidenceResults($response); + + Assert::assertEquals([ + new EvidenceResult([ + 'created' => '1', + 'message' => 'done', + 'row_count' => 4, + 'status_code' => 201, + 'version' => 1.0, + ]), + ], $results); + } + + public function testPaginatedResultsCarryRowCount(): void + { + $response = new FlexibeeResponse(200, version: null, success: true, rowCount: 7, data: ['adresar' => [['id' => '1']]]); + + $results = $this->createHydrator()->convertResponseToPaginatedEvidenceResults($response); + + Assert::assertEquals(['data' => [new EvidenceResult(['id' => '1'])], 'row_count' => 7], $results); + } + + public function testPaginatedResponseWithoutEvidenceIsReturnedAsSingleResult(): void + { + $response = new FlexibeeResponse(200, version: null, success: true, rowCount: 0, data: ['properties' => []]); + + $results = $this->createHydrator()->convertResponseToPaginatedEvidenceResults($response); + + Assert::assertEquals(['data' => [new EvidenceResult(['properties' => []])], 'row_count' => 0], $results); + } + + public function testSingleEvidenceResultIsReturned(): void + { + $result = $this->createHydrator()->convertResponseToEvidenceResult( + $this->createResponse(['adresar' => [['id' => '1']]]), + throwException: true, + ); + + Assert::assertEquals(new EvidenceResult([['id' => '1']]), $result); + } + + public function testMissingEvidenceThrowsWhenRequested(): void + { + $this->expectException(EcomailFlexibeeNoEvidenceResult::class); + + $this->createHydrator()->convertResponseToEvidenceResult( + $this->createResponse(['adresar' => [['id' => '1']]], statusCode: 404), + throwException: true, + ); + } + + public function testMissingEvidenceReturnsRawDataWhenNotThrowing(): void + { + $result = $this->createHydrator()->convertResponseToEvidenceResult($this->createResponse(['other' => 'x']), throwException: false); + + Assert::assertEquals(new EvidenceResult(['other' => 'x']), $result); + } + + public function testMissingEvidenceReturnsEmptyResultForEmptyData(): void + { + $result = $this->createHydrator()->convertResponseToEvidenceResult($this->createResponse([]), throwException: false); + + Assert::assertEquals(new EvidenceResult([]), $result); + } + + private function createHydrator(): ResponseHydrator + { + return new ResponseHydrator( + new Config('https://demo.flexibee.eu', 'demo', 'user', 'pass', 'adresar', verifySSLCertificate: true, debugMode: false), + ); + } + + /** + * @param array $data + */ + private function createResponse(array $data, int $statusCode = 200): FlexibeeResponse + { + return new FlexibeeResponse($statusCode, version: null, success: true, rowCount: 0, data: $data); + } + +} diff --git a/tests/Http/fixtures/adresar.json b/tests/Http/fixtures/adresar.json new file mode 100644 index 0000000..5dfc42b --- /dev/null +++ b/tests/Http/fixtures/adresar.json @@ -0,0 +1 @@ +{"winstrom":{"@version":"1.0","adresar":[{"id":"1"}]}} \ No newline at end of file diff --git a/tests/Validator/ParameterValidatorTest.php b/tests/Validator/ParameterValidatorTest.php index 94f9003..4b9a754 100644 --- a/tests/Validator/ParameterValidatorTest.php +++ b/tests/Validator/ParameterValidatorTest.php @@ -6,6 +6,7 @@ use EcomailFlexibee\Exception\EcomailFlexibeeInvalidRequestParameter; use EcomailFlexibee\Validator\ParameterValidator; +use Iterator; use PHPUnit\Framework\TestCase; final class ParameterValidatorTest extends TestCase @@ -13,16 +14,10 @@ final class ParameterValidatorTest extends TestCase private ParameterValidator $validator; - public function setUp(): void - { - parent::setUp(); - - $this->validator = new ParameterValidator(); - } - /** * @dataProvider getDataForTestValidateFlexibeeRequestCodeParameter * @throws \EcomailFlexibee\Exception\EcomailFlexibeeInvalidRequestParameter + * @param class-string<\Throwable>|null $exceptionClass */ public function testValidateFlexibeeRequestCodeParameter(string $code, ?string $exceptionClass): void { @@ -31,28 +26,49 @@ public function testValidateFlexibeeRequestCodeParameter(string $code, ?string $ } $this->validator->validateFlexibeeRequestCodeParameter($code); - } /** - * @return array> + * @return \Iterator|null}> */ - public function getDataForTestValidateFlexibeeRequestCodeParameter(): array + public function getDataForTestValidateFlexibeeRequestCodeParameter(): Iterator { - return [ - [ - 'TEST', - null, - ], - [ - 'ahoj', - EcomailFlexibeeInvalidRequestParameter::class, - ], - [ - 'ASASASASASASASASASASASASASASAS', - EcomailFlexibeeInvalidRequestParameter::class, - ], + yield [ + 'TEST', + null, ]; + + yield [ + 'ahoj', + EcomailFlexibeeInvalidRequestParameter::class, + ]; + + yield [ + 'ASASASASASASASASASASASASASASAS', + EcomailFlexibeeInvalidRequestParameter::class, + ]; + } + + public function testReportsOneMessageWhenBothRulesFail(): void + { + $this->expectException(EcomailFlexibeeInvalidRequestParameter::class); + $this->expectExceptionMessageMatches('/^Parameter code must have a maximum of 20 characters and must be an uppercase$/'); + + $this->validator->validateFlexibeeRequestCodeParameter('lowercase-code-longer-than-twenty'); + } + + public function testAcceptsUppercaseCodeWithDiacriticsAtTheLengthLimit(): void + { + $this->expectNotToPerformAssertions(); + + $this->validator->validateFlexibeeRequestCodeParameter('ŽLUŤOUČKÝ-KŮŇ-ÚPĚL-Á'); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->validator = new ParameterValidator(); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..b7c0abb --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,11 @@ +