From 28403d6861bd00ad8ee3da298c7467ab08ddb56b Mon Sep 17 00:00:00 2001 From: nakagleo Date: Tue, 28 Jan 2025 10:35:15 -0300 Subject: [PATCH 1/3] Adding PHP auth and auth lib --- clients/sellingpartner-api-aa-php/README.md | 81 ++ .../sellingpartner-api-aa-php/composer.json | 53 ++ .../resources/openapi-generator/config.json | 20 + .../templates/ApiException.mustache | 117 +++ .../templates/Configuration.mustache | 452 +++++++++++ .../templates/ModelInterface.mustache | 103 +++ .../templates/ObjectSerializer.mustache | 634 +++++++++++++++ .../templates/TestHelper.mustache | 678 ++++++++++++++++ .../openapi-generator/templates/api.mustache | 764 ++++++++++++++++++ .../templates/api_test.mustache | 220 +++++ .../templates/composer.mustache | 49 ++ .../templates/model.mustache | 48 ++ .../templates/model_enum.mustache | 33 + .../templates/model_generic.mustache | 692 ++++++++++++++++ .../templates/model_test.mustache | 98 +++ .../src/authandauth/LWAAccessTokenCache.php | 45 ++ .../authandauth/LWAAccessTokenRequestMeta.php | 99 +++ .../LWAAuthorizationCredentials.php | 105 +++ .../authandauth/LWAAuthorizationSigner.php | 40 + .../src/authandauth/LWAClient.php | 103 +++ .../authandauth/RateLimitConfiguration.php | 14 + .../RateLimitConfigurationOnRequests.php | 87 ++ .../src/authandauth/ScopeConstants.php | 9 + .../LWAAuthorizationSignerTest.php | 203 +++++ .../tests/authandauth/LWAClientTest.php | 209 +++++ 25 files changed, 4956 insertions(+) create mode 100644 clients/sellingpartner-api-aa-php/README.md create mode 100644 clients/sellingpartner-api-aa-php/composer.json create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/config.json create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/ApiException.mustache create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/Configuration.mustache create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/ModelInterface.mustache create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/ObjectSerializer.mustache create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/TestHelper.mustache create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api.mustache create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api_test.mustache create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/composer.mustache create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model.mustache create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_enum.mustache create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_generic.mustache create mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_test.mustache create mode 100644 clients/sellingpartner-api-aa-php/src/authandauth/LWAAccessTokenCache.php create mode 100644 clients/sellingpartner-api-aa-php/src/authandauth/LWAAccessTokenRequestMeta.php create mode 100644 clients/sellingpartner-api-aa-php/src/authandauth/LWAAuthorizationCredentials.php create mode 100644 clients/sellingpartner-api-aa-php/src/authandauth/LWAAuthorizationSigner.php create mode 100644 clients/sellingpartner-api-aa-php/src/authandauth/LWAClient.php create mode 100644 clients/sellingpartner-api-aa-php/src/authandauth/RateLimitConfiguration.php create mode 100644 clients/sellingpartner-api-aa-php/src/authandauth/RateLimitConfigurationOnRequests.php create mode 100644 clients/sellingpartner-api-aa-php/src/authandauth/ScopeConstants.php create mode 100644 clients/sellingpartner-api-aa-php/tests/authandauth/LWAAuthorizationSignerTest.php create mode 100644 clients/sellingpartner-api-aa-php/tests/authandauth/LWAClientTest.php diff --git a/clients/sellingpartner-api-aa-php/README.md b/clients/sellingpartner-api-aa-php/README.md new file mode 100644 index 00000000..cf977234 --- /dev/null +++ b/clients/sellingpartner-api-aa-php/README.md @@ -0,0 +1,81 @@ +# Selling Partner API Authentication/Authorization Library +This library provides helper classes for use when signing HTTP requests for Amazon Selling Partner APIs. It is intended for use +with the Selling Partner API Client Libraries generated by [openapi generator](https://openapi-generator.tech/) +using the Guzzlehttp library. It can also be integrated into custom projects. + +## LWAAuthorizationSigner +Obtains and signs a request with an access token from LWA (Login with Amazon) for the specified endpoint using the provided LWA credentials. + +*Example* +``` +$request = new \GuzzleHttp\Psr7\Request( + "method", "uri", ... +); + +// Seller APIs + +$lwaAuthorizationCredentials = new LWAAuthorizationCredentials([ + "clientId" => "...", + "clientSecret" => "...", + "refreshToken" => "...", + "endpoint" => "..." +]); + +/* Sellerless APIs +The Selling Partner API scopes can be retrieved from the ScopeConstants class and passed as +argument(s) to either the "scopes" => "..." or setScopes(...) method during +lwaAuthorizationCredentials object instantiation. +*/ + +use SpApi\AuthAndAuth\ScopeConstants; + +$lwaAuthorizationCredentials = new LWAAuthorizationCredentials([ + "clientId" => "...", + "clientSecret" => "...", + "scopes" => "...", + "endpoint" => "..." +]); + +$signedRequest = (new LWAAuthorizationSigner($lwaAuthorizationCredentials)) + ->sign($request); +``` + +## LWAAccessTokenCache +Implements cache for access token that is returned in LWAClient and reuses the access token until time to live. + +## RateLimitConfiguration +Interface to set and get rateLimit configurations that are used with RateLimiter. RateLimiter is used on client side to restrict the rate at which requests are made. RateLimitConfiguration takes a Permit, the rate at which requests are made, and TimeOut. + +*Example* +``` +$rateLimitOption = new RateLimitConfigurationOnRequests([ + "rateLimitToken" => "...", + "rateLimitTokenLimit" => "...", + "waitTimeOutInMilliSeconds" => "..." +]); +``` + +## Resources +This package features Mustache templates designed for use with [openapi generator](https://openapi-generator.tech/). +When you build Selling Partner API OpenAPI models with these templates, they help generate a rich SDK with functionality to invoke Selling Partner APIs built in. The templates are located in *resources/openapi-generator*. + +Dependencies are declared in the composer.json file. + +## License +OpenAPI Generator templates are subject to the [OpenAPI Generator License](https://github.com/OpenAPITools/openapi-generator/blob/v5.2.1/LICENSE). + +All other work licensed as follows: + +Copyright 2019 Amazon.com, Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this library except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/clients/sellingpartner-api-aa-php/composer.json b/clients/sellingpartner-api-aa-php/composer.json new file mode 100644 index 00000000..fa5a33d0 --- /dev/null +++ b/clients/sellingpartner-api-aa-php/composer.json @@ -0,0 +1,53 @@ +{ + "name": "spapi/auth-and-auth", + "description": "Amazon Selling Partner APIs official client library.", + "type": "library", + "keywords": [ + "sp-api", + "amazon", + "sdk", + "openapi-generator", + "php", + "rest" + ], + "homepage": "https://developer-docs.amazon.com/sp-api", + "license": "Apache-2.0", + "authors": [ + { + "name": "Amazon API Services", + "homepage": "https://developer-docs.amazon.com/sp-api" + } + ], + "require": { + "php": "^8.3", + "ext-json": "*", + "guzzlehttp/guzzle": "^8.3", + "guzzlehttp/psr7": "^2.0", + "aws/aws-sdk-php": "^3.228", + "symfony/http-kernel": "^7.2", + "symfony/rate-limiter": "^7.2", + "vlucas/phpdotenv": "^5.6", + "ext-openssl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^9.6", + "squizlabs/php_codesniffer": "^3.7" + }, + "autoload": { + "psr-4": { + "SpApi\\AuthAndAuth\\": "src/authandauth/", + "OpenAPI\\Client\\": "sdk/lib/", + "OpenAPI\\Client\\Test\\" : "sdk/test/" + } + }, + "autoload-dev": { + "psr-4": { + "SpApi\\Test\\AuthAndAuth\\" : "tests/authandauth/", + "OpenAPI\\Client\\": "sdk/lib/" + } + }, + "scripts": { + "fix": "phpcs --standard=PSR12 src/authandauth tests/authandauth", + "test": "phpunit tests/authandauth" + } +} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/config.json b/clients/sellingpartner-api-aa-php/resources/openapi-generator/config.json new file mode 100644 index 00000000..07bd3f9f --- /dev/null +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/config.json @@ -0,0 +1,20 @@ +{ + "additionalProperties": { + "customTemplateDir": "./resources/openapi-generator/templates", + "common_header" : "/**\n * Selling Partner API\n *\n * The Selling Partner API enables developers to programmatically retrieve information from various domains.\n * These APIs provide tools for building fast, flexible, and custom applications,\n * as well as demand-based decision support systems.\n *\n * The version of the OpenAPI document: v0\n * Generated by: https://openapi-generator.tech\n * Generator version: 7.9.0\n */" + }, + "files": { + "TestHelper.mustache": { + "templateFile": "TestHelper.mustache", + "destinationFilename": "test/TestHelper.php" + }, + "ModelInterface.mustache": { + "templateFile": "ModelInterface.mustache", + "destinationFilename": "lib/Model/ModelInterface.php" + }, + "BaseTestCase.mustache": { + "templateFile": "BaseTestCase.mustache", + "destinationFilename": "test/Api/BaseTestCase.php" + } + } +} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/ApiException.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/ApiException.mustache new file mode 100644 index 00000000..ee9edd6a --- /dev/null +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/ApiException.mustache @@ -0,0 +1,117 @@ +responseHeaders = $responseHeaders; + $this->responseBody = $responseBody; + } + + /** + * Gets the HTTP response header + * + * @return string[][]|null HTTP response header + */ + public function getResponseHeaders(): ?array + { + return $this->responseHeaders; + } + + /** + * Gets the HTTP body of the server response either as Json or string + * + * @return stdClass|string|null HTTP body of the server response either as \stdClass or string + */ + public function getResponseBody(): string|stdClass|null + { + return $this->responseBody; + } + + /** + * Sets the deserialized response object (during deserialization) + * + * @param mixed $obj Deserialized response object + * + * @return void + */ + public function setResponseObject(mixed $obj): void + { + $this->responseObject = $obj; + } + + /** + * Gets the deserialized response object (during deserialization) + * + * @return stdClass|string|null the deserialized response object + */ + public function getResponseObject(): stdClass|string|null + { + return $this->responseObject; + } +} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/Configuration.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/Configuration.mustache new file mode 100644 index 00000000..8329bcde --- /dev/null +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/Configuration.mustache @@ -0,0 +1,452 @@ +tempFolderPath = sys_get_temp_dir(); + if ($lwaAuthorizationCredentials) { + if ($disableAccessTokenCache) { + $this->lwaAuthSigner = new LWAAuthorizationSigner($lwaAuthorizationCredentials); + } else { + if (!$lwaTokenCache) { + $lwaTokenCache = new LWAAccessTokenCache(); + } + $this->lwaAuthSigner = new LWAAuthorizationSigner($lwaAuthorizationCredentials, $lwaTokenCache); + } + } elseif (!empty($config)) { + $missing_variables = []; + foreach (static::REQUIRED_INPUTS as $key) { + if (!isset($config[$key])) { + $missing_variables[] = $key; + } + } + if (!empty($missing_variables)) { + throw new InvalidArgumentException( + "The following variables are missing: " . implode(" ", $missing_variables) + ); + } + + $lwaCredentials = new LWAAuthorizationCredentials([ + 'clientId' => $config['clientId'], + 'clientSecret' => $config['clientSecret'], + 'endpoint' => $config['endpoint'], + 'refreshToken' => $config['refreshToken'] ?? null, + 'scopes' => $config['scopes'] ?? null + ]); + + $lwaTokenCache = $config['lwaTokenCache'] ?? null; + + if ($disableAccessTokenCache) { + $this->lwaAuthSigner = new LWAAuthorizationSigner($lwaCredentials); + } else { + if (!$lwaTokenCache) { + $lwaTokenCache = new LWAAccessTokenCache(); + } + $this->lwaAuthSigner = new LWAAuthorizationSigner($lwaCredentials, $lwaTokenCache); + } + } else { + throw new InvalidArgumentException( + "Configuration must have valid config array or credential objects" + ); + } + } + + public function enableCache(?LWAAccessTokenCache $lwaAccessTokenCache = null): void + { + $this->lwaAuthSigner->getLwaClient()->setLWAAccessTokenCache( + $lwaAccessTokenCache ?? new LWAAccessTokenCache() + ); + } + + public function disableCache(): void + { + $this->lwaAuthSigner->getLwaClient()->setLWAAccessTokenCache(null); + } + + public function sign(Request $request): Request + { + $request = $this->lwaAuthSigner->sign($request); + return $request; + } + + /** + * Sets boolean format for query string. + * + * @param string $booleanFormat Boolean format for query string + * + * @return $this + */ + public function setBooleanFormatForQueryString(string $booleanFormat): Configuration + { + $this->booleanFormatForQueryString = $booleanFormat; + + return $this; + } + + /** + * Gets boolean format for query string. + * + * @return string Boolean format for query string + */ + public function getBooleanFormatForQueryString(): string + { + return $this->booleanFormatForQueryString; + } + + /** + * Sets the host + * + * @param string $host Host + * + * @return $this + */ + public function setHost(string $host): Configuration + { + $this->host = $host; + return $this; + } + + /** + * Gets the host + * + * @return string Host + */ + public function getHost(): string + { + return $this->host; + } + + /** + * Sets the user agent of the api client + * + * @param string $userAgent the user agent of the api client + * + * @throws \InvalidArgumentException + * @return $this + */ + public function setUserAgent(string $userAgent): Configuration + { + if (!is_string($userAgent)) { + throw new \InvalidArgumentException('User-agent must be a string.'); + } + + $this->userAgent = $userAgent; + return $this; + } + + /** + * Gets the user agent of the api client + * + * @return string user agent + */ + public function getUserAgent(): string + { + return $this->userAgent; + } + + /** + * Sets debug flag + * + * @param bool $debug Debug flag + * + * @return $this + */ + public function setDebug(bool $debug): Configuration + { + $this->debug = $debug; + return $this; + } + + /** + * Gets the debug flag + * + * @return bool + */ + public function getDebug(): bool + { + return $this->debug; + } + + /** + * Sets the debug file + * + * @param string $debugFile Debug file + * + * @return $this + */ + public function setDebugFile(string $debugFile): Configuration + { + $this->debugFile = $debugFile; + return $this; + } + + /** + * Gets the debug file + * + * @return string + */ + public function getDebugFile(): string + { + return $this->debugFile; + } + + /** + * Sets the temp folder path + * + * @param string $tempFolderPath Temp folder path + * + * @return $this + */ + public function setTempFolderPath(string $tempFolderPath): Configuration + { + $this->tempFolderPath = $tempFolderPath; + return $this; + } + + /** + * Gets the temp folder path + * + * @return string Temp folder path + */ + public function getTempFolderPath(): string + { + return $this->tempFolderPath; + } + + /** + * Gets the default configuration instance + * + * @return Configuration + */ + public static function getDefaultConfiguration(): Configuration + { + if (self::$defaultConfiguration === null) { + self::$defaultConfiguration = new Configuration(); + } + + return self::$defaultConfiguration; + } + + /** + * Sets the default configuration instance + * + * @param Configuration $config An instance of the Configuration Object + * + * @return void + */ + public static function setDefaultConfiguration(Configuration $config) + { + self::$defaultConfiguration = $config; + } + + /** + * Gets the essential information for debugging + * + * @return string The report for debugging + */ + public static function toDebugReport(): string + { + $report = 'PHP SDK ({{invokerPackage}}) Debug Report:' . PHP_EOL; + $report .= ' OS: ' . php_uname() . PHP_EOL; + $report .= ' PHP Version: ' . PHP_VERSION . PHP_EOL; + $report .= ' The version of the OpenAPI document: {{version}}' . PHP_EOL; + {{#artifactVersion}} + $report .= ' SDK Package Version: {{.}}' . PHP_EOL; + {{/artifactVersion}} + $report .= ' Temp Folder Path: ' . self::getDefaultConfiguration()->getTempFolderPath() . PHP_EOL; + + return $report; + } + + /** + * Returns an array of host settings + * + * @return array an array of host settings + */ + public function getHostSettings(): array + { + return [ + {{#servers}} + [ + "url" => "{{{url}}}", + "description" => "{{{description}}}{{^description}}No description provided{{/description}}", + {{#variables}} + {{#-first}} + "variables" => [ + {{/-first}} + "{{{name}}}" => [ + "description" => "{{{description}}}{{^description}}No description provided{{/description}}", + "default_value" => "{{{defaultValue}}}", + {{#enumValues}} + {{#-first}} + "enum_values" => [ + {{/-first}} + "{{{.}}}"{{^-last}},{{/-last}} + {{#-last}} + ] + {{/-last}} + {{/enumValues}} + ]{{^-last}},{{/-last}} + {{#-last}} + ] + {{/-last}} + {{/variables}} + ]{{^-last}},{{/-last}} + {{/servers}} + ]; + } + + /** + * Returns URL based on the index and variables + * + * @param int $index index of the host settings + * @param array|null $variables hash of variable and the corresponding value (optional) + * @return string URL based on host settings + */ + public function getHostFromSettings(int $index, ?array $variables = null): string + { + if (null === $variables) { + $variables = []; + } + + $hosts = $this->getHostSettings(); + + // check array index out of bound + if ($index < 0 || $index >= sizeof($hosts)) { + throw new \InvalidArgumentException( + "Invalid index $index when selecting the host. Must be less than ".sizeof($hosts) + ); + } + + $host = $hosts[$index]; + $url = $host["url"]; + + // go through variable and assign a value + foreach ($host["variables"] ?? [] as $name => $variable) { + // check to see if it's in the variables provided by the user + if (array_key_exists($name, $variables)) { + // check to see if the value is in the enum + if (in_array($variables[$name], $variable["enum_values"], true)) { + $url = str_replace("{" . $name . "}", $variables[$name], $url); + } else { + throw new \InvalidArgumentException( + "The variable `$name` in the host URL has invalid value " + . $variables[$name] + . ". Must be " + . join(',', $variable["enum_values"]) + . "." + ); + } + } else { + // use default value + $url = str_replace("{" . $name . "}", $variable["default_value"], $url); + } + } + + return $url; + } +} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/ModelInterface.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/ModelInterface.mustache new file mode 100644 index 00000000..2d150c14 --- /dev/null +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/ModelInterface.mustache @@ -0,0 +1,103 @@ +format('Y-m-d') : $data->format(self::$dateTimeFormat); + } + + if (is_array($data)) { + foreach ($data as $property => $value) { + $data[$property] = self::sanitizeForSerialization($value); + } + return $data; + } + + if (is_object($data)) { + $values = []; + if ($data instanceof ModelInterface) { + $formats = $data::openAPIFormats(); + foreach ($data::openAPITypes() as $property => $openAPIType) { + $getter = $data::getters()[$property]; + $value = $data->$getter(); + if ($value !== null && !in_array($openAPIType, [{{&primitives}}], true)) { + $callable = [$openAPIType, 'getAllowableEnumValues']; + if (is_callable($callable)) { + /** array $callable */ + $allowedEnumTypes = $callable(); + if (!in_array($value, $allowedEnumTypes, true)) { + $imploded = implode("', '", $allowedEnumTypes); + throw new \InvalidArgumentException("Invalid value for enum '$openAPIType', must be one of: '$imploded'"); + } + } + } + if (($data::isNullable($property) && $data->isNullableSetToNull($property)) || $value !== null) { + $values[$data::attributeMap()[$property]] = self::sanitizeForSerialization($value, $openAPIType, $formats[$property]); + } + } + } else { + foreach ($data as $property => $value) { + $values[$property] = self::sanitizeForSerialization($value); + } + } + return (object)$values; + } else { + return (string)$data; + } + } + + /** + * Sanitize filename by removing path. + * e.g. ../../sun.gif becomes sun.gif + * + * @param string $filename filename to be sanitized + * + * @return string the sanitized filename + */ + public static function sanitizeFilename(string $filename): string + { + if (preg_match("/.*[\/\\\\](.*)$/", $filename, $match)) { + return $match[1]; + } else { + return $filename; + } + } + + /** + * Shorter timestamp microseconds to 6 digits length. + * + * @param string $timestamp Original timestamp + * + * @return string the shorten timestamp + */ + public static function sanitizeTimestamp(string $timestamp): string + { + if (!is_string($timestamp)) return $timestamp; + + return preg_replace('/(:\d{2}.\d{6})\d*/', '$1', $timestamp); + } + + /** + * Take value and turn it into a string suitable for inclusion in + * the path, by url-encoding. + * + * @param string $value a string which will be part of the path + * + * @return string the serialized object + */ + public static function toPathValue(string $value): string + { + return rawurlencode(self::toString($value)); + } + + /** + * Checks if a value is empty, based on its OpenAPI type. + * + * @param mixed $value + * @param string $openApiType + * + * @return bool true if $value is empty + */ + private static function isEmptyValue(mixed $value, string $openApiType): bool + { + # If empty() returns false, it is not empty regardless of its type. + if (!empty($value)) { + return false; + } + + # Null is always empty, as we cannot send a real "null" value in a query parameter. + if ($value === null) { + return true; + } + + switch ($openApiType) { + # For numeric values, false and '' are considered empty. + # This comparison is safe for floating point values, since the previous call to empty() will + # filter out values that don't match 0. + case 'int': + case 'integer': + return $value !== 0; + + case 'number': + case 'float': + return $value !== 0 && $value !== 0.0; + + # For boolean values, '' is considered empty + case 'bool': + case 'boolean': + return !in_array($value, [false, 0], true); + + # For string values, '' is considered empty. + case 'string': + return $value === ''; + + # For all the other types, any value at this point can be considered empty. + default: + return true; + } + } + + /** + * Take query parameter properties and turn it into an array suitable for + * native http_build_query or GuzzleHttp\Psr7\Query::build. + * + * @param mixed $value Parameter value + * @param string $paramName Parameter name + * @param string $openApiType OpenAPIType eg. array or object + * @param string $style Parameter serialization style + * @param bool $explode Parameter explode option + * @param bool $required Whether query param is required or not + * + * @return array + */ + public static function toQueryValue( + mixed $value, + string $paramName, + string $openApiType = 'string', + string $style = 'form', + bool $explode = true, + bool $required = true + ): array { + + # Check if we should omit this parameter from the query. This should only happen when: + # - Parameter is NOT required; AND + # - its value is set to a value that is equivalent to "empty", depending on its OpenAPI type. For + # example, 0 as "int" or "boolean" is NOT an empty value. + if (self::isEmptyValue($value, $openApiType)) { + if ($required) { + return ["{$paramName}" => '']; + } else { + return []; + } + } + + # Handle DateTime objects in query + if ($openApiType === "\\DateTime" && $value instanceof DateTime) { + return ["{$paramName}" => $value->format(self::$dateTimeFormat)]; + } + + $query = []; + $value = (in_array($openApiType, ['object', 'array'], true)) ? (array)$value : $value; + + // since \GuzzleHttp\Psr7\Query::build fails with nested arrays + // need to flatten array first + $flattenArray = function ($arr, $name, &$result = []) use (&$flattenArray, $style, $explode) { + if (!is_array($arr)) { + return $arr; + } + + foreach ($arr as $k => $v) { + $prop = ($style === 'deepObject') ? $prop = "{$name}[{$k}]" : $k; + + if (is_array($v)) { + $flattenArray($v, $prop, $result); + } else { + if ($style !== 'deepObject' && !$explode) { + // push key itself + $result[] = $prop; + } + $result[$prop] = $v; + } + } + return $result; + }; + + $value = $flattenArray($value, $paramName); + + // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values + if ($openApiType === 'array' && $style === 'deepObject' && $explode) { + return $value; + } + + if ($openApiType === 'object' && ($style === 'deepObject' || $explode)) { + return $value; + } + + if ('boolean' === $openApiType && is_bool($value)) { + $value = self::convertBoolToQueryStringFormat($value); + } + + // handle style in serializeCollection + $query[$paramName] = ($explode) ? $value : self::serializeCollection((array)$value, $style); + + return $query; + } + + /** + * Convert boolean value to format for query string. + * + * @param bool $value Boolean value + * + * @return int|string Boolean value in format + */ + public static function convertBoolToQueryStringFormat(bool $value): int|string + { + if ( + Configuration::BOOLEAN_FORMAT_STRING + == Configuration::getDefaultConfiguration()->getBooleanFormatForQueryString() + ) { + return $value ? 'true' : 'false'; + } + + return (int) $value; + } + + /** + * Take value and turn it into a string suitable for inclusion in + * the header. If it's a string, pass through unchanged + * If it's a datetime object, format it in ISO8601 + * + * @param string $value a string which will be part of the header + * + * @return string the header string + */ + public static function toHeaderValue(string $value): string + { + $callable = [$value, 'toHeaderValue']; + if (is_callable($callable)) { + return $callable(); + } + + return self::toString($value); + } + + /** + * Take value and turn it into a string suitable for inclusion in + * the http body (form parameter). If it's a string, pass through unchanged + * If it's a datetime object, format it in ISO8601 + * + * @param string|\SplFileObject $value the value of the form parameter + * + * @return string the form string + */ + public static function toFormValue(string|\SplFileObject $value): string + { + if ($value instanceof \SplFileObject) { + return $value->getRealPath(); + } else { + return self::toString($value); + } + } + + /** + * Take value and turn it into a string suitable for inclusion in + * the parameter. If it's a string, pass through unchanged + * If it's a datetime object, format it in ISO8601 + * If it's a boolean, convert it to "true" or "false". + * + * @param float|DateTime|bool|int|string $value the value of the parameter + * + * @return string the header string + */ + public static function toString(float|DateTime|bool|int|string $value): string + { + if ($value instanceof DateTime) { // datetime in ISO8601 format + return $value->format(self::$dateTimeFormat); + } elseif (is_bool($value)) { + return $value ? 'true' : 'false'; + } else { + return (string) $value; + } + } + + /** + * Serialize an array to a string. + * + * @param array $collection collection to serialize to a string + * @param string $style the format use for serialization (csv, + * ssv, tsv, pipes, multi) + * @param bool $allowCollectionFormatMulti allow collection format to be a multidimensional array + * + * @return string + */ + public static function serializeCollection( + array $collection, + string $style, + bool $allowCollectionFormatMulti = false + ): string { + if ($allowCollectionFormatMulti && ('multi' === $style)) { + // http_build_query() almost does the job for us. We just + // need to fix the result of multidimensional arrays. + return preg_replace('/%5B[0-9]+%5D=/', '=', http_build_query($collection, '', '&')); + } + switch ($style) { + case 'pipeDelimited': + case 'pipes': + return implode('|', $collection); + + case 'tsv': + return implode("\t", $collection); + + case 'spaceDelimited': + case 'ssv': + return implode(' ', $collection); + + case 'simple': + case 'csv': + // Deliberate fall through. CSV is default format. + default: + return implode(',', $collection); + } + } + + /** + * Deserialize a JSON string into an object + * + * @param mixed $data object or primitive to be deserialized + * @param string $class class name is passed as a string + * @param string[]|null $httpHeaders HTTP headers + * + * @return object|array|string|null a single or an array of $class instances + */ + public static function deserialize(mixed $data, string $class, ?array $httpHeaders = null): object|array|string|null + { + if (null === $data) { + return null; + } + + if (strcasecmp(substr($class, -2), '[]') === 0) { + $data = is_string($data) ? json_decode($data) : $data; + + if (!is_array($data)) { + throw new \InvalidArgumentException("Invalid array '$class'"); + } + + $subClass = substr($class, 0, -2); + $values = []; + foreach ($data as $key => $value) { + $values[] = self::deserialize($value, $subClass, null); + } + return $values; + } + + if (preg_match('/^(array<|map\[)/', $class)) { // for associative array e.g. array + $data = is_string($data) ? json_decode($data) : $data; + settype($data, 'array'); + $inner = substr($class, 4, -1); + $deserialized = []; + if (strrpos($inner, ",") !== false) { + $subClass_array = explode(',', $inner, 2); + $subClass = $subClass_array[1]; + foreach ($data as $key => $value) { + $deserialized[$key] = self::deserialize($value, $subClass, null); + } + } + return $deserialized; + } + + if ($class === 'object') { + settype($data, 'array'); + return $data; + } elseif ($class === 'mixed') { + settype($data, gettype($data)); + return $data; + } + + if ($class === '\DateTime') { + // Some APIs return an invalid, empty string as a + // date-time property. DateTime::__construct() will return + // the current time for empty input which is probably not + // what is meant. The invalid empty string is probably to + // be interpreted as a missing field/value. Let's handle + // this graceful. + if (!empty($data)) { + try { + return new DateTime($data); + } catch (\Exception $exception) { + // Some APIs return a date-time with too high nanosecond + // precision for php's DateTime to handle. + // With provided regexp 6 digits of microseconds saved + return new DateTime(self::sanitizeTimestamp($data)); + } + } else { + return null; + } + } + + if ($class === '\SplFileObject') { + $data = Utils::streamFor($data); + + /** @var \Psr\Http\Message\StreamInterface $data */ + + // determine file name + if ( + is_array($httpHeaders) + && array_key_exists('Content-Disposition', $httpHeaders) + && preg_match('/inline; filename=[\'"]?([^\'"\s]+)[\'"]?$/i', $httpHeaders['Content-Disposition'], $match) + ) { + $filename = Configuration::getDefaultConfiguration()->getTempFolderPath() . DIRECTORY_SEPARATOR . self::sanitizeFilename($match[1]); + } else { + $filename = tempnam(Configuration::getDefaultConfiguration()->getTempFolderPath(), ''); + } + + $file = fopen($filename, 'w'); + while ($chunk = $data->read(200)) { + fwrite($file, $chunk); + } + fclose($file); + + return new \SplFileObject($filename, 'r'); + } + + /** @psalm-suppress ParadoxicalCondition */ + if (in_array($class, [{{&primitives}}], true)) { + settype($data, $class); + return $data; + } + + + if (method_exists($class, 'getAllowableEnumValues')) { + if (!in_array($data, $class::getAllowableEnumValues(), true)) { + $imploded = implode("', '", $class::getAllowableEnumValues()); + throw new \InvalidArgumentException("Invalid value for enum '$class', must be one of: '$imploded'"); + } + return $data; + } else { + $data = is_string($data) ? json_decode($data) : $data; + + if (is_array($data)) { + $data = (object)$data; + } + + // If a discriminator is defined and points to a valid subclass, use it. + $discriminator = $class::DISCRIMINATOR; + if (!empty($discriminator) && isset($data->{$discriminator}) && is_string($data->{$discriminator})) { + $subclass = '\{{invokerPackage}}\Model\\' . $data->{$discriminator}; + if (is_subclass_of($subclass, $class)) { + $class = $subclass; + } + } + + /** @var ModelInterface $instance */ + $instance = new $class(); + foreach ($instance::openAPITypes() as $property => $type) { + $propertySetter = $instance::setters()[$property]; + + if (!isset($propertySetter)) { + continue; + } + + if (!isset($data->{$instance::attributeMap()[$property]})) { + if ($instance::isNullable($property)) { + $instance->$propertySetter(null); + } + + continue; + } + + if (isset($data->{$instance::attributeMap()[$property]})) { + $propertyValue = $data->{$instance::attributeMap()[$property]}; + $instance->$propertySetter(self::deserialize($propertyValue, $type, null)); + } + } + return $instance; + } + } + + /** + * Build a query string from an array of key value pairs. + * + * This function can use the return value of `parse()` to build a query + * string. This function does not modify the provided keys when an array is + * encountered (like `http_build_query()` would). + * + * The function is copied from https://github.com/guzzle/psr7/blob/a243f80a1ca7fe8ceed4deee17f12c1930efe662/src/Query.php#L59-L112 + * with a modification which is described in https://github.com/guzzle/psr7/pull/603 + * + * @param array $params Query string parameters. + * @param bool|int $encoding Set to false to not encode, PHP_QUERY_RFC3986 + * to encode using RFC3986, or PHP_QUERY_RFC1738 + * to encode using RFC1738. + */ + public static function buildQuery( + array $params, + ?Configuration $config = null, + bool|int $encoding = PHP_QUERY_RFC3986 + ): string { + if (!$params) { + return ''; + } + + if ($encoding === false) { + $encoder = function (string $str): string { + return $str; + }; + } elseif ($encoding === PHP_QUERY_RFC3986) { + $encoder = 'rawurlencode'; + } elseif ($encoding === PHP_QUERY_RFC1738) { + $encoder = 'urlencode'; + } else { + throw new \InvalidArgumentException('Invalid type'); + } + + $config = $config ?? Configuration::getDefaultConfiguration(); + $castBool = $config->getBooleanFormatForQueryString() == Configuration::BOOLEAN_FORMAT_INT + ? function ($v) { + return (int) $v; + } + : function ($v) { + return $v ? 'true' : 'false'; + }; + + $qs = ''; + foreach ($params as $k => $v) { + $k = $encoder((string) $k); + if (!is_array($v)) { + $qs .= $k; + $v = is_bool($v) ? $castBool($v) : $v; + if ($v !== null) { + $qs .= '=' . $encoder((string) $v); + } + $qs .= '&'; + } else { + $qs .= $k . '='; + $isFirst = true; + foreach ($v as $vv) { + $vv = is_bool($vv) ? $castBool($vv) : $vv; + if ($vv !== null) { + if (!$isFirst) { + $qs .= ','; + } + $qs .= $encoder((string) $vv); + $isFirst = false; + } + } + $qs .= '&'; + } + } + + return $qs ? (string) substr($qs, 0, -1) : ''; + } +} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/TestHelper.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/TestHelper.mustache new file mode 100644 index 00000000..9e4c7089 --- /dev/null +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/TestHelper.mustache @@ -0,0 +1,678 @@ +getName(); + + // Handle special case for 'payload' parameter. + if ( + $paramName === 'payload' + || $paramName === 'body' + || $paramName === 'requests' + || $paramName === 'get_featured_offer_expected_price_batch_request_body' + || $paramName === 'get_item_offers_batch_request_body' + || $paramName === 'get_listing_offers_batch_request_body' + || $paramName === 'create_inventory_item_request_body' + || $paramName === 'create_scheduled_package_request' + || $paramName === 'list_handover_slots_request' + || $paramName === 'update_scheduled_packages_request' + ) { + $typeName = $param->getType()->getName(); + if (class_exists($typeName)) { + $requestInstance = new $typeName(); + if (isset($requestParameters['body']['value'])) { + $requestInstance = + ObjectSerializer::deserialize($requestParameters['body']['value'], $typeName, []); + } elseif ( + // TODO This condition should be removed when EasyShip swagger is fixed + isset($requestParameters['body']) + && ($paramName === 'update_scheduled_packages_request' + || $paramName === 'list_handover_slots_request') + ) { + $requestInstance = + ObjectSerializer::deserialize($requestParameters['body'], $typeName, []); + } elseif (!$param->isOptional()) { + // Insert Dummy object + $openAPITypes = $typeName::openAPITypes(); + $setters = $typeName::setters(); + + foreach ($openAPITypes as $propertyName => $propertyType) { + // Skip if the property is nullable + if ($typeName::isNullable($propertyName)) { + continue; + } + + // Generate dummy value based on the type + $dummyValue = self::getDummyValueForType($propertyType, $propertyName); + + // Check if a setter exists for the property + if (array_key_exists($propertyName, $setters)) { + $setterMethod = $setters[$propertyName]; + if (method_exists($requestInstance, $setterMethod)) { + // Call the setter method with the dummy value + $requestInstance->$setterMethod($dummyValue); + } + } + } + } + + $requestParams['payload'] = $requestInstance; + } elseif ($typeName === 'array') { + $requestParams['payload'] = $requestParameters['body']['value']; + } + continue; + } + + // Process regular parameters with snake_case to camelCase conversion. + $value = null; + if ($requestParameters) { + foreach ([false, true] as $capitalizeFirst) { + $camelCaseName = self::snakeToCamelCase($paramName, $capitalizeFirst); + // Check for standard camelCase name + $subArrayValue = self::extractValue($camelCaseName, $requestParameters); + if ($subArrayValue !== null) { + $value = $subArrayValue; + break; + } + // Special handling for 'Sku' + if (str_contains($camelCaseName, 'Sku')) { + $camelCaseName = str_replace('Sku', 'SKU', $camelCaseName); + $subArrayValue = self::extractValue($camelCaseName, $requestParameters); + if ($subArrayValue !== null) { + $value = $subArrayValue; + break; + } + // Special handling for 'ASINList' + } elseif ($camelCaseName === 'asinList') { + $camelCaseName = 'ASINList'; + $subArrayValue = self::extractValue($camelCaseName, $requestParameters); + if ($subArrayValue !== null) { + $value = $subArrayValue; + break; + } + } + } + } + if (empty($value) && !$param->isOptional()) { + $typeName = $param->getType()->getName(); + // Insert Dummy parameter + $value = self::getDummyValueForType($typeName, $paramName); + } + $requestParams[$paramName] = $value; + } + + return $requestParams; + } + + /** + * Helper method to extract a value from the request parameters. + * + * @param string $Name + * @param array $requestParameters The array of request parameters to search within. + * @return string|null The value if found and valid, otherwise null. + */ + private static function extractValue(string $Name, array $requestParameters): mixed + { + if (isset($requestParameters[$Name])) { + $subArray = $requestParameters[$Name]; + if (is_array($subArray) && isset($subArray['value'])) { + return $subArray['value']; + } + } + + return null; + } + + /** + * Generates a UUID (Universally Unique Identifier) version 4. + * + * This method generates a random UUID (v4) in compliance with RFC 4122. It uses random bytes for + * entropy and ensures the version and variant fields are correctly set according to the specification. + * + * UUID v4 format example: 123e4567-e89b-12d3-a456-426614174000 + * + * @return string A randomly generated UUID v4 as a string. + */ + private static function generateUuidV4(): string + { + $data = openssl_random_pseudo_bytes(16); + + // Set the version to 0100 (UUID v4) + $data[6] = chr(ord($data[6]) & 0x0f | 0x40); + + // Set the variant to 10xx (RFC 4122) + $data[8] = chr(ord($data[8]) & 0x3f | 0x80); + + // Convert to the UUID string format + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); + } + + /** + * Returns a dummy value based on the given type. + * + * This method generates a dummy value for the specified type name and parameter name. It handles a variety + * of data types, including primitives, arrays, objects, and enumerations. If the type is an object, it + * recursively populates its properties with dummy values. + * + * @param string $typeName The type name for which a dummy value is needed. + * @param string $paramName The parameter name associated with the type. + * @return mixed A dummy value appropriate for the given type. + * @throws ReflectionException If there is an error instantiating a class using reflection. + */ + private static function getDummyValueForType(string $typeName, string $paramName): mixed + { + // Handle domain specific case + if (isset(self::$classSpecificValue[$paramName])) { + return self::$classSpecificValue[$paramName]; + // Listing + } elseif ($paramName === 'marketplace_ids') { + return [self::$marketPlaceId]; + } + // Handle array and specific object types + if (str_ends_with($typeName, '[]')) { + $elementType = substr($typeName, 0, -2); + return [self::getDummyValueForType($elementType, $paramName)]; + } + if ($typeName === '\DateTime' || $typeName === 'DateTime') { + return new \DateTime(); + } + + if (class_exists($typeName)) { + $reflectionClass = new ReflectionClass($typeName); + $instance = $reflectionClass->newInstance(); + // Enum + if (method_exists($instance, 'getAllowableEnumValues')) { + $allowableValues = $instance::getAllowableEnumValues(); + return reset($allowableValues); + } + // Populate object properties recursively + $openAPITypes = $instance::openAPITypes(); + $setters = $typeName::setters(); + + foreach ($openAPITypes as $propertyName => $propertyType) { + // Skip if the property is nullable + if ($typeName::isNullable($propertyName)) { + continue; + } + + // Generate dummy value based on the type + $dummyValue = self::getDummyValueForType($propertyType, $propertyName); + + // Check if a setter exists for the property + if (array_key_exists($propertyName, $setters)) { + $setterMethod = $setters[$propertyName]; + if (method_exists($instance, $setterMethod)) { + // Call the setter method with the dummy value + $instance->$setterMethod($dummyValue); + } + } + } + + return $instance; + } + + // Handle primitive types + return match ($typeName) { + 'int' => 1, + 'float' => 1.0, + 'bool' => false, + 'string' => 'test', + 'array' => ["1"], + default => null, + }; + } + + /** + * @param $apiInstance + * @param string $methodName + * @return ReflectionMethod + * @throws ReflectionException + */ + private static function getReflectionMethod($apiInstance, string $methodName): ReflectionMethod + { + return new ReflectionMethod($apiInstance, $methodName); + } + + /** + * Extracts the request parameters and expected response from a JSON schema. + * + * This method processes a JSON schema string, decodes it, and extracts the + * request parameters and expected response for a specified operation ID. + * It prepares the request parameters to match the method signature of + * the provided API instance. + * + * @param object $apiInstance The instance of the API class that contains the method to be invoked. + * @param string $jsonSchema The JSON schema string representing the request and response definitions. + * @param string $operationId The operation ID corresponding to the method in the API instance. + * + * @return array An associative array containing: + * - `requestParams` (array): Prepared request parameters for the specified method. + * - `expectedResponse` (mixed): The expected response extracted from the JSON schema. + * + * @throws \InvalidArgumentException If the provided JSON schema is invalid or cannot be decoded. + * @throws ReflectionException If there is an error during the preparation of request parameters + * (e.g., method does not exist in the API instance). + */ + public function extractRequestAndResponse(object $apiInstance, string $jsonSchema, string $operationId): array + { + // Decode HTML entities + $codegenText = html_entity_decode($jsonSchema); + + // Remove unnecessary characters + $codegenText = str_replace(["\r", "\n"], '', $codegenText); + //$codegenText = str_replace(' ', '', $codegenText); + + // Decode JSON + $data = json_decode($codegenText, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \InvalidArgumentException("Invalid JSON schema provided: " . json_last_error_msg()); + } + + // Extract request object + $request = $data['x-amzn-api-sandbox']['static'][0]['request']['parameters'] ?? null; + + // Prepare request parameters + $reflection = self::getReflectionMethod($apiInstance, $operationId); + $params = $reflection->getParameters(); + $requestParams = self::prepareRequestParams($params, $request); + + // Extract expected response + $expectedResponse = $data['x-amzn-api-sandbox']['static'][0]['response'] ?? null; + if ($expectedResponse !== null) { + $returnType = $reflection->getReturnType(); + if (class_exists($returnType)) { + $expectedResponse = + ObjectSerializer::deserialize($expectedResponse, $returnType, []); + } + } + + return [ + 'requestParams' => $requestParams, + 'expectedResponse' => $expectedResponse + ]; + } + + /** + * Builds a request for dynamic sandbox testing. + * + * This method prepares the request parameters needed for the specified API operation in a dynamic sandbox + * environment. It uses reflection to retrieve the parameters of the given operation and prepares them accordingly. + * + * @param object $apiInstance The API instance that contains the operation. + * @param string $operationId The ID of the operation for which the request is being built. + * @return array Returns an array of prepared request parameters for the specified operation. + * @throws ReflectionException + */ + public function buildRequestForDynamicSandBox(object $apiInstance, string $operationId): array + { + // Prepare request parameters + $reflection = self::getReflectionMethod($apiInstance, $operationId); + $params = $reflection->getParameters(); + return self::prepareRequestParams($params, null); + } + + /** + * Array that defines TestCase Name requires specific ateTimeFormat + * @var array + */ + private array $dateTimeFormatSpecificCase = [ + 'FeesApi' => 'D M d H:i:s T Y', + 'ListingsItemsApi' => 'Y-m-d\TH:i:s\Z', + 'OffersApi' => 'Y-m-d\TH:i:s\Z', + 'SellingpartnersApi' => 'Y-m-d\TH:i:s\Z' + ]; + + + /** + * Get required DateTimeFormat for a given testCaseName. + * + * @param string $caseName + * @return string|null + */ + public function getDateTimeFormatForCase(string $caseName): ?string + { + return $this->dateTimeFormatSpecificCase[$caseName] ?? null; + } + + /** + * Array that defines scopes parameter required for grantless operation such as Notification API + * @var array|string[] + */ + private array $scopesRequiredMap = [ + 'testCreateDestination200' => ['sellingpartnerapi::notifications'], + 'testGetDestination200' => ['sellingpartnerapi::notifications'], + 'testDeleteDestination200' => ['sellingpartnerapi::notifications'], + 'testGetDestinations200' => ['sellingpartnerapi::notifications'], + 'testDeleteSubscriptionById200' => ['sellingpartnerapi::notifications'], + 'testGetSubscriptionById200' => ['sellingpartnerapi::notifications'], + ]; + + + /** + * Get required scopes for a given caseName. + * + * @param string $caseName + * @return array + */ + public function getScopesForApi(string $caseName): array + { + return $this->scopesRequiredMap[$caseName] ?? []; + } + + /** + * Array that defines Vendor related API Test class name + * @var array|string[] + */ + private static array $vendorApiTestClassNameList = [ + 'OpenAPI\Client\Test\Api\UpdateInventoryApiTest', + 'OpenAPI\Client\Test\Api\VendorOrdersApiTest', + 'OpenAPI\Client\Test\Api\VendorInvoiceApiTest', + 'OpenAPI\Client\Test\Api\VendorShippingApiTest', + 'OpenAPI\Client\Test\Api\VendorShippingLabelsApiTest', + 'OpenAPI\Client\Test\Api\VendorTransactionApi' + ]; + + /** + * Checks if the given class name is related to a vendor-related API. + * + * This method determines if the specified class name exists in the list + * of predefined vendor-related API test class names. + * + * @param string $className The fully qualified name of the class to check. + * @return bool Returns true if the class name is related to a vendor-related API; otherwise, false. + */ + public static function isVendorRelatedApi(string $className): bool + { + if (in_array($className, TestHelper::$vendorApiTestClassNameList)) { + return true; + } + return false; + } + + /** + * Checks if the test case should be skipped. + * + * @param string $testCaseName + * @param string|null $className + * @return bool + */ + public static function shouldSkipTest(string $testCaseName, ?string $className = null): bool + { + if (!in_array($testCaseName, TestHelper::$testSkipCasesList)) { + return in_array($className, TestHelper::$testSkipCasesList); + } + return true; + } + + /** + * Array that defines test cases which are shippable for now + * @var array|string[] + */ + public static array $testSkipCasesList = [ + // Definition of Test Class which has not been tested + 'DefaultApi', + 'AplusContentApi', // Doesn't support sandbox as of now. + 'AppIntegrationsApi', // No role for my account yet + 'ApplicationsApi', // Doesn't support sandbox as of now. + 'AwdApi', // No role for my account yet + 'CustomerInvoicesApi', + 'InvoicesApi', // No role for my account yet + 'ServiceApi', // No role for my account yet + 'ShipmentInvoiceApi', // No role for my account yet + 'ShippingApi', // No role for my account yet + 'UploadsApi', // Doesn't support sandbox as of now. + // There is critical bug in swagger and API class can not be compiled. need revisit testing. + 'VendorOrdersApi', + 'VendorShipmentsApi', + + // Definition of individual case which is unable to test + // Order API + // Missing required parameter in Request regulatedOrderVerificationStatus and can not be auto filled + //Because there is no difference between 200 case. + 'testUpdateVerificationStatus400', + 'testGetOrderRegulatedInfo200', // Getting 403 due to restricted role required + 'testGetOrderRegulatedInfo400', // Getting 403 due to restricted role required + // NotificationAPI + 'testDeleteSubscriptionById200', // Getting 400 with InvalidInput error + 'testGetSubscriptionById200', // Getting 400 with InvalidInput error + // Feed API + 'testCancelFeed200', // Always 500 will be returned + 'testCreateFeed400', // Request should have mandatory field FeedType + 'testCreateFeedDocument400', // Request should have mandatory field ContentType + // Report API + 'testCancelReport200', // Always 500 will be returned + 'testCancelReportSchedule200', // Always 500 will be returned + 'testCreateReportSchedule400', // Request should have mandatory field MarketplaceIds + // Pricing API + 'testGetCompetitiveSummary200', // Request offerType should be CONSUMER, not Consumer + // FBA Inbound Eligibility API + 'testGetItemEligibilityPreview401', // Always 500 will be returned + 'testGetItemEligibilityPreview503', // Always 500 will be returned + // fulfillmentInbound_2024-03-20 + 'testGenerateShipmentContentUpdatePreviews202', // Sandbox Returns 400 + 'testGenerateTransportationOptions202', // Sandbox Returns 400 + 'testGetInboundPlan200', // Json expected Response of timestamp is in wrong format. Millisecond should be added. + 'testGetShipment200', // Json expected Response of timestamp is in wrong format. Millisecond should be added. + // Json expected Response of timestamp is in wrong format. Millisecond should be added. + 'testScheduleSelfShipAppointment200', + 'testGetShipmentContentUpdatePreview200', // "expiration" at the Json expected Response Json is wrong + 'testListShipmentContentUpdatePreviews200', // "expiration" at the Json expected Response Json is wrong + 'testListInboundPlans200', //Json expected Response of timestamp is in wrong format.Millisecond should be added. + 'testListPrepDetails200', // Sandbox Returns 400 + 'testSetPackingInformation202', // Sandbox Returns 400 + 'testUpdateItemComplianceDetails202', // Sandbox Returns 400 + 'testSetPrepDetails202', // Sandbox Returns 400 + // CatalogItem + 'testGetCatalogItem200', // Response has Invalid value for images.variant such as PT09-PT14, EEGL and EGUS + 'testSearchCatalogItems200', // Response has Invalid value for images.variant such as PT09-PT14, EEGL and EGUS + // ProductFeesAPI + 'testGetMyFeesEstimates200', // Sandbox Returns 400 + 'testGetMyFeesEstimateForASIN400', // Request can not be made because Request is missing mandatory parameters + 'testGetMyFeesEstimateForSKU400', // Request can not be made because Request is missing mandatory parameters + // ListingsItems_2021-08-01 + // Expected response is different from actual response. + // TimeStamp Format is inconsistent within same Expected response Json + 'testGetListingsItem200', + // fbaInventory + 'testAddInventory200', // Create inventory is dependency for this operation + // listingsRestrictions + 'testGetListingsRestrictions400', // Error response can not be handled due to wrong ErrorList definition in Json + // merchantFulfillment + 'testCancelShipment200', // Label.FileContents.FileType is Enum, but “” is returned. + //Response ShippingServiceList.ShippingServiceOptions.LabelFormat should be Enum value or removed + 'testGetEligibleShipmentServices200', + // fulfillmentOutbound + 'testListReturnReasonCodes200', // Due to complexity, skip for now + 'testCancelFulfillmentOrder200', // Due to test execution order, it can not be passed + 'testCreateFulfillmentReturn200', // Due to test execution order, it can not be passed + 'testUpdateFulfillmentOrder200', // Due to test execution order, it can not be passed + 'testGetPackageTrackingDetails200', // Due to test execution order, it can not be passed + 'testSubmitFulfillmentOrderStatusUpdate200', // Due to test execution order, it can not be passed + 'testDeliveryOffers200', // Due to complexity, skip for now + // EasyShip + 'testCreateScheduledPackage400', // Skip due to mandatory filed "slotId" is null in the sample + 'testCreateScheduledPackageBulk200', // packageDimensions.unit must be Cm (Maybe in JP?) + 'testGetScheduledPackage200', // The expected and actual response is not a member of PackageStatus ENUM + 'testListHandoverSlots400', // Due to Json structure is incorrect, need to be fixed + 'testUpdateScheduledPackages400', // Due to Json structure is incorrect, need to be fixed + // DataKiosk + 'testCancelQuery204', // Sandbox Returns 400 + 'testCancelQuery404', // Sandbox Returns 400 + 'testGetQuery200', // Sandbox Returns 400 + 'testGetQuery404', // Sandbox Returns 400 + // Messaging API + // SandBox request timestamp format (2004-12-13T21:39:45.618-08:00) doesn't match with PHP. + // It will require dedicated customization to make 3 digit millisecond and doesn't match with auto Generation + 'testCreateWarranty201', + // Replenishment Api sellingPartners + 'testGetSellingPartnerMetrics400', // Request timestamp millisecond is 2 digits and requires string mutation + // Replenishment Api offers + 'testListOfferMetrics400', // Request eligibility is not member of Enum + // Sellers + 'testGetAccount200', // Need some application which can be succeeded. Access is denied with my account. + 'testGetAccount400', // Need some application which can be succeeded. Access is denied with my account. + // SolicitationsApi + // Need some application which can be succeeded. Access is denied with my account. + 'testGetSolicitationActionsForOrder200', + // SupplySources + 'testGetSupplySource200', // SandBox expected response.ThroughputUnit should be “Order” not “ORDER”. + 'testUpdateSupplySource400', // Expected sandbox response is missing mandatory field "ThroughputUnit". + 'testGetSupplySources400', // Expecting 400 to be returned, but 200 returned + // Tokens API + 'testCreateRestrictedDataToken400', // SandBox request.method is not member of Enum + // vendorDirectFulfillmentPaymentsV1 + 'testSubmitInvoice202', // Expected transactionId response is different from actual. + 'testSubmitInvoice400', // SandBox dateTime Format is invalid for programing language + // vendorDirectFulfillmentShipping_2021-12-28 + // shipmentStatusUpdates is not required in swagger, but SandBox returns error + 'testSubmitShipmentStatusUpdates202', + // shipmentConfirmations is not required in swagger, but SandBox returns error + 'testSubmitShipmentConfirmations202', + 'testGetPackingSlips200', // only allows purchaseOrderNumber as ‘’, but this is not practical. + 'testGetShippingLabel200', // only allows purchaseOrderNumber as ‘’, but this is not practical. + // shippingLabelRequests is not required in swagger, but SandBox returns error + 'testSubmitShippingLabelRequest202', + 'testCreateShippingLabels200', // Access is denied as 403 + 'testGetShippingLabels200', // Getting 400 error and not sure how to call successfully + // vendorDirectFulfillmentTransactions_2021-12-28 + 'testGetTransactionStatus200', // Not sure which value to pass as a key + // FBA Inbound V0 + 'testConfirmPreorder200', // It will be deprecated + 'testConfirmPreorder400', // It will be deprecated + 'testCreateInboundShipmentPlan400', // It will be deprecated + 'testGetTransportDetails200', // It will be deprecated + // FulfillmentInboundApiTest.testGenerateSelfShipAppointmentSlots201 + // OrdersApiTest.testConfirmShipment204 + // ReportsApiTest.testCreateReport202 + // ReportsApiTest.testCreateReportSchedule201 + 'testGenerateSelfShipAppointmentSlots201', + 'testConfirmShipment204', + 'testCreateReport202', + 'testCreateReportSchedule201', + 'testListOffers400', + 'testGetPackingSlip200' + ]; +} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api.mustache new file mode 100644 index 00000000..5c254a58 --- /dev/null +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api.mustache @@ -0,0 +1,764 @@ +partial_header}} +/** + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +namespace {{apiPackage}}; + +use GuzzleHttp\Client; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\ConnectException; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Promise\PromiseInterface; +use GuzzleHttp\Psr7\MultipartStream; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\RequestOptions; +use SpApi\AuthAndAuth\RateLimitConfiguration; +use Symfony\Component\RateLimiter\LimiterInterface; +use Symfony\Component\RateLimiter\Storage\InMemoryStorage; +use Symfony\Component\RateLimiter\RateLimiterFactory; +use {{invokerPackage}}\ApiException; +use {{invokerPackage}}\Configuration; +use {{invokerPackage}}\HeaderSelector; +use {{invokerPackage}}\ObjectSerializer; + +/** + * {{classname}} Class Doc Comment + * + * @category Class + * @package {{invokerPackage}} + * @author OpenAPI Generator team + * @link https://openapi-generator.tech + */ +{{#operations}}class {{classname}} +{ + /** + * @var ClientInterface + */ + protected ClientInterface $client; + + /** + * @var Configuration + */ + protected Configuration $config; + + /** + * @var HeaderSelector + */ + protected HeaderSelector $headerSelector; + + /** + * @var int Host index + */ + protected int $hostIndex; + + /** + * @var ?RateLimitConfiguration + */ + private ?RateLimitConfiguration $rateLimitConfig = null; + + /** + * @var ?LimiterInterface + */ + private ?LimiterInterface $rateLimiter = null; + + /** + * @param Configuration $config + * @param RateLimitConfiguration|null $rateLimitConfig + * @param ClientInterface|null $client + * @param HeaderSelector|null $selector + * @param int $hostIndex (Optional) host index to select the list of hosts if defined in the OpenAPI spec + */ + public function __construct( + Configuration $config, + ?RateLimitConfiguration $rateLimitConfig = null, + ?ClientInterface $client = null, + ?HeaderSelector $selector = null, + int $hostIndex = 0 + ) { + $this->config = $config; + $this->rateLimitConfig = $rateLimitConfig; + if ($rateLimitConfig) { + $type = $rateLimitConfig->getRateLimitType(); + $rateLimitOptions = [ + 'id' => 'spApiCall', + 'policy' => $type, + 'limit' => $rateLimitConfig->getRateLimitTokenLimit(), + ]; + if ($type === "fixed_window" || $type === "sliding_window") { + $rateLimitOptions['interval'] = $rateLimitConfig->getRateLimitToken() . 'seconds'; + } else { + $rateLimitOptions['rate'] = ['interval' => $rateLimitConfig->getRateLimitToken() . 'seconds']; + } + $factory = new RateLimiterFactory($rateLimitOptions, new InMemoryStorage()); + $this->rateLimiter = $factory->create(); + } + + $this->client = $client ?: new Client(); + $this->headerSelector = $selector ?: new HeaderSelector(); + $this->hostIndex = $hostIndex; + } + + /** + * Set the host index + * + * @param int $hostIndex Host index (required) + */ + public function setHostIndex(int $hostIndex): void + { + $this->hostIndex = $hostIndex; + } + + /** + * Get the host index + * + * @return int Host index + */ + public function getHostIndex(): int + { + return $this->hostIndex; + } + + /** + * @return Configuration + */ + public function getConfig(): Configuration + { + return $this->config; + } + +{{#operation}} + /** + * Operation {{{operationId}}} +{{#summary}} + * + * {{.}} +{{/summary}} + * +{{#description}} + * {{.}} + * +{{/description}} +{{#vendorExtensions.x-group-parameters}} + * Note: the input parameter is an associative array with the keys listed as the parameter name below + * +{{/vendorExtensions.x-group-parameters}} +{{#servers}} +{{#-first}} + * This operation contains host(s) defined in the OpenAP spec. Use 'hostIndex' to select the host. +{{/-first}} + * URL: {{{url}}} +{{#-last}} + * +{{/-last}} +{{/servers}} +{{#allParams}} + * @param {{{dataType}}}{{^required}}|null{{/required}} ${{paramName}} + * {{#description}} {{.}}{{/description}}{{^description}} {{paramName}}{{/description}} {{#required}}(required){{/required}}{{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} +{{/allParams}} + * + * @throws \{{invokerPackage}}\ApiException on non-2xx response + * @throws \InvalidArgumentException + * @return {{#returnType}}{{#responses}}{{#-first}}{{{dataType}}}{{/-first}}{{/responses}}{{^responses}}void{{/responses}}{{/returnType}} + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + */ + public function {{operationId}}( + {{^vendorExtensions.x-group-parameters}}{{#allParams}} {{^required}}?{{/required}}{{#isArray}}array{{/isArray}}{{^isArray}}{{{dataType}}}{{/isArray}} ${{paramName}}{{^required}} = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}{{/required}}{{^-last}}, + {{/-last}}{{/allParams}}{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}$associative_array{{/vendorExtensions.x-group-parameters}} + ){{#returnType}}: {{#responses}}{{#-first}}{{#isArray}}array{{/isArray}}{{^isArray}}{{{dataType}}}{{/isArray}}{{/-first}}{{/responses}}{{/returnType}}{{^returnType}}: void{{/returnType}} { + {{#returnType}}list($response) = {{/returnType}}$this->{{operationId}}WithHttpInfo({{^vendorExtensions.x-group-parameters}}{{#allParams}}${{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}$associative_array{{/vendorExtensions.x-group-parameters}});{{#returnType}} + return $response;{{/returnType}} + } + + /** + * Operation {{{operationId}}}WithHttpInfo +{{#summary}} + * + * {{.}} +{{/summary}} + * +{{#description}} + * {{.}} + * +{{/description}} +{{#vendorExtensions.x-group-parameters}} + * Note: the input parameter is an associative array with the keys listed as the parameter name below + * +{{/vendorExtensions.x-group-parameters}} +{{#servers}} +{{#-first}} + * This operation contains host(s) defined in the OpenAP spec. Use 'hostIndex' to select the host. +{{/-first}} + * URL: {{{url}}} +{{#-last}} + * +{{/-last}} +{{/servers}} +{{#allParams}} + * @param {{{dataType}}}{{^required}}|null{{/required}} ${{paramName}} + * {{#description}} {{.}}{{/description}} {{#required}}(required){{/required}}{{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} +{{/allParams}} + * + * @throws \{{invokerPackage}}\ApiException on non-2xx response + * @throws \InvalidArgumentException + * @return array of {{#returnType}}{{#responses}}{{#-first}}{{{dataType}}}{{/-first}}{{/responses}}{{^responses}}void{{/responses}}{{/returnType}}, HTTP status code, HTTP response headers (array of strings) + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + */ + public function {{operationId}}WithHttpInfo( + {{^vendorExtensions.x-group-parameters}}{{#allParams}} {{^required}}?{{/required}}{{#isArray}}array{{/isArray}}{{^isArray}}{{{dataType}}}{{/isArray}} ${{paramName}}{{^required}} = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}{{/required}}{{^-last}}, + {{/-last}}{{/allParams}}{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}$associative_array{{/vendorExtensions.x-group-parameters}} + ): array { + $request = $this->{{operationId}}Request({{^vendorExtensions.x-group-parameters}}{{#allParams}}${{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}$associative_array{{/vendorExtensions.x-group-parameters}}); + $request = $this->config->sign($request); + + try { + $options = $this->createHttpClientOption(); + try { + $this->rateLimitWait(); + $response = $this->client->send($request, $options); + } catch (RequestException $e) { + throw new ApiException( + "[{$e->getCode()}] {$e->getMessage()}", + (int) $e->getCode(), + $e->getResponse() ? $e->getResponse()->getHeaders() : null, + $e->getResponse() ? (string) $e->getResponse()->getBody() : null + ); + } catch (ConnectException $e) { + throw new ApiException( + "[{$e->getCode()}] {$e->getMessage()}", + (int) $e->getCode(), + null, + null + ); + } + + $statusCode = $response->getStatusCode(); + + if ($statusCode < 200 || $statusCode > 299) { + throw new ApiException( + sprintf( + '[%d] Error connecting to the API (%s)', + $statusCode, + (string) $request->getUri() + ), + $statusCode, + $response->getHeaders(), + (string) $response->getBody() + ); + } + {{#returnType}} + {{#responses}} + {{#-first}} + + switch($statusCode) { + {{/-first}} + {{#dataType}} + {{^isWildcard}}case {{code}}:{{/isWildcard}}{{#isWildcard}}default:{{/isWildcard}} + if ('{{{dataType}}}' === '\SplFileObject') { + $content = $response->getBody(); //stream goes to serializer + } else { + $content = (string) $response->getBody(); + if ('{{dataType}}' !== 'string') { + $content = json_decode($content); + } + } + + return [ + ObjectSerializer::deserialize($content, '{{{dataType}}}', []), + $response->getStatusCode(), + $response->getHeaders() + ]; + {{/dataType}} + {{#-last}} + } + {{/-last}} + {{/responses}} + + $returnType = '{{{returnType}}}'; + if ($returnType === '\SplFileObject') { + $content = $response->getBody(); //stream goes to serializer + } else { + $content = (string) $response->getBody(); + if ($returnType !== 'string') { + $content = json_decode($content); + } + } + + return [ + ObjectSerializer::deserialize($content, $returnType, []), + $response->getStatusCode(), + $response->getHeaders() + ]; + {{/returnType}} + {{^returnType}} + + return [null, $statusCode, $response->getHeaders()]; + {{/returnType}} + + } catch (ApiException $e) { + switch ($e->getCode()) { + {{#responses}} + {{#dataType}} + {{^isWildcard}}case {{code}}:{{/isWildcard}}{{#isWildcard}}default:{{/isWildcard}} + $data = ObjectSerializer::deserialize( + $e->getResponseBody(), + '{{{dataType}}}', + $e->getResponseHeaders() + ); + $e->setResponseObject($data); + break; + {{/dataType}} + {{/responses}} + } + throw $e; + } + } + + /** + * Operation {{{operationId}}}Async + * +{{#summary}} + * {{.}} + * +{{/summary}} +{{#description}} + * {{.}} + * +{{/description}} +{{#vendorExtensions.x-group-parameters}} + * Note: the input parameter is an associative array with the keys listed as the parameter name below + * +{{/vendorExtensions.x-group-parameters}} +{{#servers}} +{{#-first}} + * This operation contains host(s) defined in the OpenAP spec. Use 'hostIndex' to select the host. +{{/-first}} + * URL: {{{url}}} +{{#-last}} + * +{{/-last}} +{{/servers}} +{{#allParams}} + * @param {{{dataType}}}{{^required}}|null{{/required}} ${{paramName}} + * {{#description}} {{.}}{{/description}} {{#required}}(required){{/required}}{{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} +{{/allParams}} + * + * @throws \InvalidArgumentException + * @return PromiseInterface + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + */ + public function {{operationId}}Async( + {{^vendorExtensions.x-group-parameters}}{{#allParams}} {{^required}}?{{/required}}{{#isArray}}array{{/isArray}}{{^isArray}}{{{dataType}}}{{/isArray}} ${{paramName}}{{^required}} = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}{{/required}}{{^-last}}, + {{/-last}}{{/allParams}}{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}$associative_array{{/vendorExtensions.x-group-parameters}} + ): PromiseInterface { + return $this->{{operationId}}AsyncWithHttpInfo({{^vendorExtensions.x-group-parameters}}{{#allParams}}${{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}$associative_array{{/vendorExtensions.x-group-parameters}}) + ->then( + function ($response) { + return $response[0]; + } + ); + } + + /** + * Operation {{{operationId}}}AsyncWithHttpInfo + * +{{#summary}} + * {{.}} + * +{{/summary}} +{{#description}} + * {{.}} + * +{{/description}} +{{#vendorExtensions.x-group-parameters}} + * Note: the input parameter is an associative array with the keys listed as the parameter name below + * +{{/vendorExtensions.x-group-parameters}} +{{#servers}} +{{#-first}} + * This operation contains host(s) defined in the OpenAP spec. Use 'hostIndex' to select the host. +{{/-first}} + * URL: {{{url}}} +{{#-last}} + * +{{/-last}} +{{/servers}} +{{#allParams}} + * @param {{{dataType}}}{{^required}}|null{{/required}} ${{paramName}} + * {{#description}} {{.}}{{/description}} {{#required}}(required){{/required}}{{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} +{{/allParams}} + * + * @throws \InvalidArgumentException + * @return PromiseInterface + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + */ + public function {{operationId}}AsyncWithHttpInfo( + {{^vendorExtensions.x-group-parameters}}{{#allParams}} {{^required}}?{{/required}}{{#isArray}}array{{/isArray}}{{^isArray}}{{{dataType}}}{{/isArray}} ${{paramName}}{{^required}} = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}{{/required}}{{^-last}}, + {{/-last}}{{/allParams}}{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}$associative_array{{/vendorExtensions.x-group-parameters}} + ): PromiseInterface { + $returnType = '{{{returnType}}}'; + $request = $this->{{operationId}}Request({{^vendorExtensions.x-group-parameters}}{{#allParams}}${{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}$associative_array{{/vendorExtensions.x-group-parameters}}); + $request = $this->config->sign($request); + $this->rateLimitWait(); + + return $this->client + ->sendAsync($request, $this->createHttpClientOption()) + ->then( + function ($response) use ($returnType) { + {{#returnType}} + if ($returnType === '\SplFileObject') { + $content = $response->getBody(); //stream goes to serializer + } else { + $content = (string) $response->getBody(); + if ($returnType !== 'string') { + $content = json_decode($content); + } + } + + return [ + ObjectSerializer::deserialize($content, $returnType, []), + $response->getStatusCode(), + $response->getHeaders() + ]; + {{/returnType}} + {{^returnType}} + return [null, $response->getStatusCode(), $response->getHeaders()]; + {{/returnType}} + }, + function ($exception) { + $response = $exception->getResponse(); + $statusCode = $response->getStatusCode(); + throw new ApiException( + sprintf( + '[%d] Error connecting to the API (%s)', + $statusCode, + $exception->getRequest()->getUri() + ), + $statusCode, + $response->getHeaders(), + (string) $response->getBody() + ); + } + ); + } + + /** + * Create request for operation '{{{operationId}}}' + * +{{#vendorExtensions.x-group-parameters}} + * Note: the input parameter is an associative array with the keys listed as the parameter name below + * +{{/vendorExtensions.x-group-parameters}} +{{#servers}} +{{#-first}} + * This operation contains host(s) defined in the OpenAP spec. Use 'hostIndex' to select the host. +{{/-first}} + * URL: {{{url}}} +{{#-last}} + * +{{/-last}} +{{/servers}} +{{#allParams}} + * @param {{{dataType}}}{{^required}}|null{{/required}} ${{paramName}} + * {{#description}} {{.}}{{/description}} {{#required}}(required){{/required}}{{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} +{{/allParams}} + * + * @throws \InvalidArgumentException + * @return Request + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + */ + public function {{operationId}}Request( + {{^vendorExtensions.x-group-parameters}}{{#allParams}} {{^required}}?{{/required}}{{#isArray}}array{{/isArray}}{{^isArray}}{{{dataType}}}{{/isArray}} ${{paramName}}{{^required}} = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}{{/required}}{{^-last}}, + {{/-last}}{{/allParams}}{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}$associative_array{{/vendorExtensions.x-group-parameters}} + ): Request { + {{#vendorExtensions.x-group-parameters}} + // unbox the parameters from the associative array + {{#allParams}} + ${{paramName}} = array_key_exists('{{paramName}}', $associative_array) ? $associative_array['{{paramName}}'] : {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}; + {{/allParams}} + + {{/vendorExtensions.x-group-parameters}} + {{#allParams}} + {{#required}} + // verify the required parameter '{{paramName}}' is set + if (${{paramName}} === null || (is_array(${{paramName}}) && count(${{paramName}}) === 0)) { + throw new \InvalidArgumentException( + 'Missing the required parameter ${{paramName}} when calling {{operationId}}' + ); + } + {{/required}} + {{#hasValidation}} + {{#maxLength}} + if ({{^required}}${{paramName}} !== null && {{/required}}strlen(${{paramName}}) > {{maxLength}}) { + throw new \InvalidArgumentException('invalid length for "${{paramName}}" when calling {{classname}}.{{operationId}}, must be smaller than or equal to {{maxLength}}.'); + } + {{/maxLength}} + {{#minLength}} + if ({{^required}}${{paramName}} !== null && {{/required}}strlen(${{paramName}}) < {{minLength}}) { + throw new \InvalidArgumentException('invalid length for "${{paramName}}" when calling {{classname}}.{{operationId}}, must be bigger than or equal to {{minLength}}.'); + } + {{/minLength}} + {{#maximum}} + if ({{^required}}${{paramName}} !== null && {{/required}}${{paramName}} >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{maximum}}) { + throw new \InvalidArgumentException('invalid value for "${{paramName}}" when calling {{classname}}.{{operationId}}, must be smaller than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}{{maximum}}.'); + } + {{/maximum}} + {{#minimum}} + if ({{^required}}${{paramName}} !== null && {{/required}}${{paramName}} <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{minimum}}) { + throw new \InvalidArgumentException('invalid value for "${{paramName}}" when calling {{classname}}.{{operationId}}, must be bigger than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}{{minimum}}.'); + } + {{/minimum}} + {{#pattern}} + if ({{^required}}${{paramName}} !== null && {{/required}}!preg_match("{{{pattern}}}", ${{paramName}})) { + throw new \InvalidArgumentException("invalid value for \"{{paramName}}\" when calling {{classname}}.{{operationId}}, must conform to the pattern {{{pattern}}}."); + } + {{/pattern}} + {{#maxItems}} + if ({{^required}}${{paramName}} !== null && {{/required}}count(${{paramName}}) > {{maxItems}}) { + throw new \InvalidArgumentException('invalid value for "${{paramName}}" when calling {{classname}}.{{operationId}}, number of items must be less than or equal to {{maxItems}}.'); + } + {{/maxItems}} + {{#minItems}} + if ({{^required}}${{paramName}} !== null && {{/required}}count(${{paramName}}) < {{minItems}}) { + throw new \InvalidArgumentException('invalid value for "${{paramName}}" when calling {{classname}}.{{operationId}}, number of items must be greater than or equal to {{minItems}}.'); + } + {{/minItems}} + + {{/hasValidation}} + {{/allParams}} + + $resourcePath = '{{{path}}}'; + $formParams = []; + $queryParams = []; + $headerParams = []; + $httpBody = ''; + $multipart = false; + + {{#queryParams}} + // query params + $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue( + ${{paramName}}, + '{{baseName}}', // param base name + '{{#schema}}{{openApiType}}{{/schema}}', // openApiType + '{{style}}', // style + {{#isExplode}}true{{/isExplode}}{{^isExplode}}false{{/isExplode}}, // explode + {{required}} // required + ) ?? []); + {{/queryParams}} + + {{#headerParams}} + // header params + {{#collectionFormat}} + if (is_array(${{paramName}})) { + ${{paramName}} = ObjectSerializer::serializeCollection(${{paramName}}, '{{collectionFormat}}'); + } + {{/collectionFormat}} + if (${{paramName}} !== null) { + $headerParams['{{baseName}}'] = ObjectSerializer::toHeaderValue(${{paramName}}); + } + {{/headerParams}} + + {{#pathParams}} + // path params + {{#collectionFormat}} + if (is_array(${{paramName}})) { + ${{paramName}} = ObjectSerializer::serializeCollection(${{paramName}}, '{{collectionFormat}}'); + } + {{/collectionFormat}} + if (${{paramName}} !== null) { + $resourcePath = str_replace( + '{' . '{{baseName}}' . '}', + ObjectSerializer::toPathValue(${{paramName}}), + $resourcePath + ); + } + {{/pathParams}} + + {{#formParams}} + // form params + if (${{paramName}} !== null) { + {{#isFile}} + $multipart = true; + $formParams['{{baseName}}'] = []; + $paramFiles = is_array(${{paramName}}) ? ${{paramName}} : [${{paramName}}]; + foreach ($paramFiles as $paramFile) { + $formParams['{{baseName}}'][] = \GuzzleHttp\Psr7\Utils::tryFopen( + ObjectSerializer::toFormValue($paramFile), + 'rb' + ); + } + {{/isFile}} + {{^isFile}} + $formParams['{{baseName}}'] = ObjectSerializer::toFormValue(${{paramName}}); + {{/isFile}} + } + {{/formParams}} + + if ($multipart) { + $headers = $this->headerSelector->selectHeadersForMultipart( + [{{#produces}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/produces}}] + ); + } else { + $headers = $this->headerSelector->selectHeaders( + [{{#produces}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/produces}}], + {{#consumes}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/consumes}} + {{^consumes}}''{{/consumes}}, + false + ); + } + + // for model (json/xml) + {{#bodyParams}} + if (isset(${{paramName}})) { + if ($headers['Content-Type'] === 'application/json') { + $httpBody = \GuzzleHttp\json_encode(ObjectSerializer::sanitizeForSerialization(${{paramName}})); + } else { + $httpBody = ${{paramName}}; + } + } elseif (count($formParams) > 0) { + {{/bodyParams}} + {{^bodyParams}} + if (count($formParams) > 0) { + {{/bodyParams}} + if ($multipart) { + $multipartContents = []; + foreach ($formParams as $formParamName => $formParamValue) { + $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue]; + foreach ($formParamValueItems as $formParamValueItem) { + $multipartContents[] = [ + 'name' => $formParamName, + 'contents' => $formParamValueItem + ]; + } + } + // for HTTP post (form) + $httpBody = new MultipartStream($multipartContents); + + } elseif ($headers['Content-Type'] === 'application/json') { + $httpBody = \GuzzleHttp\json_encode($formParams); + + } else { + // for HTTP post (form) + $httpBody = ObjectSerializer::buildQuery($formParams, $this->config); + } + } + + {{#authMethods}} + {{#isApiKey}} + // this endpoint requires API key authentication + $apiKey = $this->config->getApiKeyWithPrefix('{{keyParamName}}'); + if ($apiKey !== null) { + {{#isKeyInHeader}}$headers['{{keyParamName}}'] = $apiKey;{{/isKeyInHeader}}{{#isKeyInQuery}}$queryParams['{{keyParamName}}'] = $apiKey;{{/isKeyInQuery}} + } + {{/isApiKey}} + {{#isBasic}} + {{#isBasicBasic}} + // this endpoint requires HTTP basic authentication + if (!empty($this->config->getUsername()) || !(empty($this->config->getPassword()))) { + $headers['Authorization'] = 'Basic ' . base64_encode($this->config->getUsername() . ":" . $this->config->getPassword()); + } + {{/isBasicBasic}} + {{#isBasicBearer}} + // this endpoint requires Bearer{{#bearerFormat}} ({{{.}}}){{/bearerFormat}} authentication (access token) + if (!empty($this->config->getAccessToken())) { + $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken(); + } + {{/isBasicBearer}} + {{/isBasic}} + {{#isOAuth}} + // this endpoint requires OAuth (access token) + if (!empty($this->config->getAccessToken())) { + $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken(); + } + {{/isOAuth}} + {{/authMethods}} + + $defaultHeaders = []; + if ($this->config->getUserAgent()) { + $defaultHeaders['User-Agent'] = $this->config->getUserAgent(); + } + + $headers = array_merge( + $defaultHeaders, + $headerParams, + $headers + ); + + {{#servers.0}} + $operationHosts = [{{#servers}}"{{{url}}}"{{^-last}}, {{/-last}}{{/servers}}]; + if ($this->hostIndex < 0 || $this->hostIndex >= sizeof($operationHosts)) { + throw new \InvalidArgumentException("Invalid index {$this->hostIndex} when selecting the host. Must be less than ".sizeof($operationHosts)); + } + $operationHost = $operationHosts[$this->hostIndex]; + + {{/servers.0}} + $query = ObjectSerializer::buildQuery($queryParams, $this->config); + return new Request( + '{{httpMethod}}', + {{^servers.0}}$this->config->getHost(){{/servers.0}}{{#servers.0}}$operationHost{{/servers.0}} . $resourcePath . ($query ? "?{$query}" : ''), + $headers, + $httpBody + ); + } + + {{/operation}} + /** + * Create http client option + * + * @throws \RuntimeException on file opening failure + * @return array of http client options + */ + protected function createHttpClientOption(): array + { + $options = []; + if ($this->config->getDebug()) { + $options[RequestOptions::DEBUG] = fopen($this->config->getDebugFile(), 'a'); + if (!$options[RequestOptions::DEBUG]) { + throw new \RuntimeException('Failed to open the debug file: ' . $this->config->getDebugFile()); + } + } + + return $options; + } + + /** + * Rate Limiter waits for tokens + * + * @return void + */ + public function rateLimitWait(): void + { + if ($this->rateLimiter) { + $type = $this->rateLimitConfig->getRateLimitType(); + if ($this->rateLimitConfig->getTimeOut() != 0 && ($type == "token_bucket" || $type == "fixed_window")) { + $this->rateLimiter->reserve(1, ($this->rateLimitConfig->getTimeOut()) / 1000)->wait(); + } else { + $this->rateLimiter->consume()->wait(); + } + } + } +} +{{/operations}} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api_test.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api_test.mustache new file mode 100644 index 00000000..2efe8c05 --- /dev/null +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api_test.mustache @@ -0,0 +1,220 @@ +partial_header}} +/** + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Please update the test case below to test the endpoint. + */ + +namespace {{invokerPackage}}\Test\Api; + +use {{invokerPackage}}\Configuration; +use {{invokerPackage}}\ApiException; +use PHPUnit\Framework\TestCase; +use {{invokerPackage}}\Api\{{classname}}; +use OpenAPI\Client\Test\TestHelper; +use SpApi\AuthAndAuth\LWAAuthorizationCredentials; +use {{invokerPackage}}\ObjectSerializer; +use Dotenv\Dotenv; + +$dotenv = Dotenv::createImmutable('../../../sdk'); +$dotenv->load(); + +/** + * {{classname}}Test Class Doc Comment + * + * @category Class + * @package {{invokerPackage}} + * @author OpenAPI Generator team + * @link https://openapi-generator.tech + */ +{{#operations}} +class {{classname}}Test extends TestCase +{ + private {{classname}} $apiInstance; + private TestHelper $testHelper; + + public function setUp(): void + { + $this->testHelper = new TestHelper(); + $credentialsConfig = [ + "clientId" => $_ENV['SP_API_CLIENT_ID'], + "clientSecret" => $_ENV['SP_API_CLIENT_SECRET'], + "refreshToken" => $_ENV['SP_API_REFRESH_TOKEN'], + "endpoint" => $_ENV['SP_API_ENDPOINT'] ?: "https://api.amazon.com/auth/o2/token" + ]; + $scopes = $this->testHelper->getScopesForApi($this->getName()); + if (!empty($scopes)) { + $credentialsConfig['scopes'] = $scopes; + } + $lwaAuthorizationCredentials = new LWAAuthorizationCredentials($credentialsConfig); + $config = new Configuration([], $lwaAuthorizationCredentials); + $config->setHost($_ENV['SP_API_ENDPOINT_HOST'] ?: 'https://sandbox.sellingpartnerapi-fe.amazon.com'); + $this->apiInstance = new {{classname}}($config, null, null); + } + + /** + * Handles the response based on the expected HTTP status code. + * + * @param mixed $response The API response. + * @param int $statusCode The actual HTTP status code. + * @param int $expectedStatusCode The expected HTTP status code. + * @throws \ReflectionException + */ + private function handleResponse(mixed $response, int $statusCode, int $expectedStatusCode, $responseParams): void + { + switch ($expectedStatusCode) { + case 200: + case 201: + case 202: + $actual = json_decode($response, true); + $this->assertEquals($responseParams, $actual); + break; + + case 204: + $this->assertTrue(true); + echo "Response is empty as expected for status code 204."; + break; + + case 400: + $this->assertArrayHasKey('errors', $responseParams); + $this->assertEquals($responseParams['errors'], $response->getErrors()); + break; + + default: + $this->fail("Unhandled response code: $expectedStatusCode"); + break; + } + } + + /** + * Handles exceptions thrown during the API call. + * + * @param ApiException $e The exception thrown by the API. + */ + private function handleApiException(ApiException $e, int $expectedCode): void + { + if ($e->getCode() == $expectedCode) { + $this->assertTrue(true); + } else { + $this->fail('Unexpected error code: ' . $e->getCode()); + } + } + + /** + * Asserts the HTTP status code. + * + * @param int $expectedStatusCode + * @param int $actualStatusCode + */ + private function assertHttpStatusCode(int $expectedStatusCode, int $actualStatusCode): void + { + $this->assertEquals( + $expectedStatusCode, + $actualStatusCode, + "Expected HTTP status code $expectedStatusCode but got $actualStatusCode." + ); + } + + {{#operation}} + {{#responses}} + /** + * Test case for {{{operationId}}}_{{code}} + */ + public function test{{operationIdCamelCase}}{{code}}() + { + {{^vendorExtensions.x-amzn-api-sandbox.static}} + {{^vendorExtensions}} + // Skip this test if no static sandbox extension is present + $this->markTestSkipped('Static sandbox is not defined for this operation.'); + {{/vendorExtensions}} + {{#vendorExtensions}} + {{^is2xx}} + // Skip this test + $this->markTestSkipped('Skip test for this operation.'); + {{/is2xx}} + {{#is2xx}} + // Dynamic sandbox case + try { + // Skip test if it is in the skip list + if ($this->testHelper->shouldSkipTest('test{{operationIdCamelCase}}{{code}}', '{{classname}}')) { + $this->assertTrue(true); + return; + } + // Skip entire class + if ($this->testHelper->shouldSkipTest('{{classname}}')) { + $this->assertTrue(true); + return; + } + $result = $this->testHelper->buildRequestForDynamicSandBox( + $this->apiInstance, + '{{operationId}}' + ); + $requestParams = $result; + + // Act: Call API + list($response, $statusCode, $headers) = + $this->apiInstance->{{operationId}}WithHttpInfo(...array_values($requestParams)); + + // Assert the response code + $this->assertHttpStatusCode({{code}}, $statusCode); + } catch (ApiException $e) { + $this->handleApiException($e, {{code}}); + } catch (\ReflectionException $e) { + $this->fail("Reflection exception: " . $e->getMessage()); + } + {{/is2xx}} + {{/vendorExtensions}} + {{/vendorExtensions.x-amzn-api-sandbox.static}} + {{#vendorExtensions.x-amzn-api-sandbox.static.0}} + try { + // Skip test if it is in the skip list + if ($this->testHelper->shouldSkipTest('test{{operationIdCamelCase}}{{code}}', '{{classname}}')) { + $this->assertTrue(true); + return; + } + $jsonSchema = '{{jsonSchema}}'; + $result = $this->testHelper->extractRequestAndResponse( + $this->apiInstance, + $jsonSchema, + '{{operationId}}' + ); + $requestParams = $result['requestParams']; + $expectedResponse = $result['expectedResponse']; + + // Change Time Format if it requires + $specificTimeFormat = $this->testHelper->getDateTimeFormatForCase('{{classname}}'); + if ($specificTimeFormat) { + ObjectSerializer::setDateTimeFormat($specificTimeFormat); + } + + // Act: Call API + list($response, $statusCode, $headers) = + $this->apiInstance->{{operationId}}WithHttpInfo(...array_values($requestParams)); + + // Assert the response code + $this->assertHttpStatusCode({{code}}, $statusCode); + + // Handle different response codes + $this->handleResponse($response, $statusCode, {{code}}, $expectedResponse); + } catch (ApiException $e) { + $this->handleApiException($e, {{code}}); + } catch (\ReflectionException $e) { + $this->fail("Reflection exception: " . $e->getMessage()); + } + {{/vendorExtensions.x-amzn-api-sandbox.static.0}} + } + {{/responses}} + {{/operation}} +} +{{/operations}} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/composer.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/composer.mustache new file mode 100644 index 00000000..1cf15b41 --- /dev/null +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/composer.mustache @@ -0,0 +1,49 @@ +{ + {{#composerPackageName}} + "name": "{{.}}", + {{/composerPackageName}} + {{#artifactVersion}} + "version": "{{.}}", + {{/artifactVersion}} + "description": "{{{appDescription}}}", + "keywords": [ + "openapitools", + "openapi-generator", + "openapi", + "php", + "sdk", + "rest", + "api" + ], + "homepage": "https://openapi-generator.tech", + "license": "unlicense", + "authors": [ + { + "name": "OpenAPI-Generator contributors", + "homepage": "https://openapi-generator.tech" + } + ], + "require": { + "php": "^7.4 || ^8.0", + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/guzzle": "^7.3", + "guzzlehttp/psr7": "^1.7 || ^2.0", + "symfony/rate-limiter": "^6.1", + "aws/aws-sdk-php": "^3.228", + "spapi/auth-and-auth": "1.0", + "vlucas/phpdotenv": "^5.6", + "ext-openssl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^8.0 || ^9.0", + "friendsofphp/php-cs-fixer": "^3.5" + }, + "autoload": { + "psr-4": { "{{escapedInvokerPackage}}\\" : "{{srcBasePath}}/" } + }, + "autoload-dev": { + "psr-4": { "{{escapedInvokerPackage}}\\Test\\" : "{{testBasePath}}/" } + } +} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model.mustache new file mode 100644 index 00000000..2cec99bb --- /dev/null +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model.mustache @@ -0,0 +1,48 @@ +partial_header}} +/** + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +namespace {{modelPackage}}; +{{^isEnum}} +{{^parentSchema}} + +use +ArrayAccess; +{{/parentSchema}} +{{/isEnum}} +use {{invokerPackage}}\ObjectSerializer; +use {{invokerPackage}}\Model\ModelInterface; + +/** + * {{classname}} Class Doc Comment + * + * @category Class +{{#description}} + * @description {{.}} +{{/description}} + * @package {{invokerPackage}} + * @author OpenAPI Generator team + * @link https://openapi-generator.tech +{{^isEnum}} + * @implements \ArrayAccess +{{/isEnum}} + */ +{{#isEnum}}{{>model_enum}}{{/isEnum}}{{^isEnum}}{{>model_generic}}{{/isEnum}} +{{/model}}{{/models}} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_enum.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_enum.mustache new file mode 100644 index 00000000..ce58886b --- /dev/null +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_enum.mustache @@ -0,0 +1,33 @@ +class {{classname}} +{ + /** + * Possible values of this enum + */ + {{#allowableValues}} + {{#enumVars}} + {{#enumDescription}} + /** + * {{enumDescription}} + */ + {{/enumDescription}} + public const {{{name}}} = {{{value}}}; + + {{/enumVars}} + {{/allowableValues}} + /** + * Gets allowable values of the enum + * @return string[] + */ + public static function getAllowableEnumValues(): array + { + return [ + {{#allowableValues}} + {{#enumVars}} + self::{{{name}}}{{^-last}}, + {{/-last}} + {{/enumVars}} + {{/allowableValues}} + + ]; + } +} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_generic.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_generic.mustache new file mode 100644 index 00000000..9e2d5c97 --- /dev/null +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_generic.mustache @@ -0,0 +1,692 @@ +class {{classname}} {{#parentSchema}}extends {{{parent}}}{{/parentSchema}}{{^parentSchema}}implements ModelInterface, ArrayAccess, \JsonSerializable{{/parentSchema}} +{ + public const DISCRIMINATOR = {{#discriminator}}'{{discriminatorName}}'{{/discriminator}}{{^discriminator}}null{{/discriminator}}; + + /** + * The original name of the model. + * + * @var string + */ + protected static string $openAPIModelName = '{{name}}'; + + /** + * Array of property to type mappings. Used for (de)serialization + * + * @var string[] + */ + protected static array $openAPITypes = [ + {{#vars}} + '{{name}}' => '{{{dataType}}}'{{^-last}}, + {{/-last}} + {{/vars}} + {{^vars}} + {{#isArray}}{{#items}}'{{name}}' => '{{{dataType}}}[]'{{/items}}{{/isArray}} + {{/vars}} + ]; + + /** + * Array of property to format mappings. Used for (de)serialization + * + * @var string[] + * @phpstan-var array + * @psalm-var array + */ + protected static array $openAPIFormats = [ + {{#vars}} + '{{name}}' => {{#dataFormat}}'{{{.}}}'{{/dataFormat}}{{^dataFormat}}null{{/dataFormat}}{{^-last}}, + {{/-last}} + {{/vars}} + {{^vars}} + {{#isArray}}{{#items}}'{{name}}' => {{#dataFormat}}'{{{.}}}'{{/dataFormat}}{{^dataFormat}}null{{/dataFormat}}{{/items}}{{/isArray}} + {{/vars}} + ]; + + /** + * Array of nullable properties. Used for (de)serialization + * + * @var boolean[] + */ + protected static array $openAPINullables = [ + {{#vars}}'{{name}}' => {{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{^-last}}, + {{/-last}}{{/vars}} + {{^vars}} + {{#isArray}}{{#items}}'{{name}}' => {{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/items}}{{/isArray}} + {{/vars}} + ]; + + /** + * If a nullable field gets set to null, insert it here + * + * @var boolean[] + */ + protected array $openAPINullablesSetToNull = []; + + /** + * Array of property to type mappings. Used for (de)serialization + * + * @return array + */ + public static function openAPITypes(): array + { + return self::$openAPITypes{{#parentSchema}} + parent::openAPITypes(){{/parentSchema}}; + } + + /** + * Array of property to format mappings. Used for (de)serialization + * + * @return array + */ + public static function openAPIFormats(): array + { + return self::$openAPIFormats{{#parentSchema}} + parent::openAPIFormats(){{/parentSchema}}; + } + + /** + * Array of nullable properties + * + * @return array + */ + protected static function openAPINullables(): array + { + return self::$openAPINullables{{#parentSchema}} + parent::openAPINullables(){{/parentSchema}}; + } + + /** + * Array of nullable field names deliberately set to null + * + * @return boolean[] + */ + private function getOpenAPINullablesSetToNull(): array + { + return $this->openAPINullablesSetToNull; + } + + /** + * Setter - Array of nullable field names deliberately set to null + * + * @param boolean[] $openAPINullablesSetToNull + */ + private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void + { + $this->openAPINullablesSetToNull = $openAPINullablesSetToNull; + } + + /** + * Checks if a property is nullable + * + * @param string $property + * @return bool + */ + public static function isNullable(string $property): bool + { + return self::openAPINullables()[$property] ?? false; + } + + /** + * Checks if a nullable property is set to null. + * + * @param string $property + * @return bool + */ + public function isNullableSetToNull(string $property): bool + { + return in_array($property, $this->getOpenAPINullablesSetToNull(), true); + } + + /** + * Array of attributes where the key is the local name, + * and the value is the original name + * + * @var string[] + */ + protected static array $attributeMap = [ + {{#vars}} + '{{name}}' => '{{baseName}}'{{^-last}}, + {{/-last}}{{/vars}} + {{^vars}}{{#isArray}}{{#items}}'{{name}}' => '{{baseName}}'{{/items}}{{/isArray}}{{/vars}} + ]; + + /** + * Array of attributes to setter functions (for deserialization of responses) + * + * @var string[] + */ + protected static array $setters = [ + {{#vars}}'{{name}}' => '{{setter}}'{{^-last}}, + {{/-last}}{{/vars}} + {{^vars}} + {{#isArray}}{{#items}}'{{name}}' => '{{setter}}'{{/items}}{{/isArray}} + {{/vars}} + ]; + + /** + * Array of attributes to getter functions (for serialization of requests) + * + * @var string[] + */ + protected static array $getters = [ + {{#vars}}'{{name}}' => '{{getter}}'{{^-last}}, + {{/-last}}{{/vars}} + {{^vars}} + {{#isArray}}{{#items}}'{{name}}' => '{{getter}}'{{/items}}{{/isArray}} + {{/vars}} + ]; + + /** + * Array of attributes where the key is the local name, + * and the value is the original name + * + * @return array + */ + public static function attributeMap(): array + { + return {{#parentSchema}}parent::attributeMap() + {{/parentSchema}}self::$attributeMap; + } + + /** + * Array of attributes to setter functions (for deserialization of responses) + * + * @return array + */ + public static function setters(): array + { + return {{#parentSchema}}parent::setters() + {{/parentSchema}}self::$setters; + } + + /** + * Array of attributes to getter functions (for serialization of requests) + * + * @return array + */ + public static function getters(): array + { + return {{#parentSchema}}parent::getters() + {{/parentSchema}}self::$getters; + } + + /** + * The original name of the model. + * + * @return string + */ + public function getModelName(): string + { + return self::$openAPIModelName; + } + + {{#vars}} + {{#isEnum}} + {{#allowableValues}} + {{#enumVars}} + public const {{enumName}}_{{{name}}} = {{{value}}}; + {{/enumVars}} + {{/allowableValues}} + {{/isEnum}} + {{/vars}} + + {{#vars}} + {{#isEnum}} + /** + * Gets allowable values of the enum + * + * @return string[] + */ + public function {{getter}}AllowableValues(): array + { + return [ + {{#allowableValues}}{{#enumVars}}self::{{enumName}}_{{{name}}},{{^-last}} + {{/-last}}{{/enumVars}}{{/allowableValues}} + ]; + } + + {{/isEnum}} + {{/vars}} + {{^parentSchema}} + /** + * Associative array for storing property values + * + * @var array + */ + protected array $container = []; + {{/parentSchema}} + + /** + * Constructor + * + * @param array|null $data Associated array of property values + * initializing the model + */ + public function __construct(?array $data = null) + { + {{#parentSchema}} + parent::__construct($data); + + {{/parentSchema}} + {{#vars}} + $this->setIfExists('{{name}}', $data ?? [], {{#defaultValue}}{{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}); + {{/vars}} + {{#discriminator}} + + // Initialize discriminator property with the model name. + $this->container['{{discriminatorName}}'] = static::$openAPIModelName; + {{/discriminator}} + } + + /** + * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName + * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the + * $this->openAPINullablesSetToNull array + * + * @param string $variableName + * @param array $fields + * @param mixed $defaultValue + */ + private function setIfExists(string $variableName, array $fields, $defaultValue): void + { + if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) { + $this->openAPINullablesSetToNull[] = $variableName; + } + + $this->container[$variableName] = $fields[$variableName] ?? $defaultValue; + } + + /** + * Show all the invalid properties with reasons. + * + * @return array invalid properties with reasons + */ + public function listInvalidProperties(): array + { + {{#parentSchema}} + $invalidProperties = parent::listInvalidProperties(); + {{/parentSchema}} + {{^parentSchema}} + $invalidProperties = []; + {{/parentSchema}} + + {{#vars}} + {{#required}} + if ($this->container['{{name}}'] === null) { + $invalidProperties[] = "'{{name}}' can't be null"; + } + {{/required}} + {{#isEnum}} + {{^isContainer}} + $allowedValues = $this->{{getter}}AllowableValues(); + if (!is_null($this->container['{{name}}']) && !in_array($this->container['{{name}}'], $allowedValues, true)) { + $invalidProperties[] = sprintf( + "invalid value '%s' for '{{name}}', must be one of '%s'", + $this->container['{{name}}'], + implode("', '", $allowedValues) + ); + } + + {{/isContainer}} + {{/isEnum}} + {{#hasValidation}} + {{#maxLength}} + if ({{^required}}!is_null($this->container['{{name}}']) && {{/required}}(mb_strlen($this->container['{{name}}']) > {{maxLength}})) { + $invalidProperties[] = "invalid value for '{{name}}', the character length must be smaller than or equal to {{{maxLength}}}."; + } + + {{/maxLength}} + {{#minLength}} + if ({{^required}}!is_null($this->container['{{name}}']) && {{/required}}(mb_strlen($this->container['{{name}}']) < {{minLength}})) { + $invalidProperties[] = "invalid value for '{{name}}', the character length must be bigger than or equal to {{{minLength}}}."; + } + + {{/minLength}} + {{#maximum}} + if ({{^required}}!is_null($this->container['{{name}}']) && {{/required}}($this->container['{{name}}'] >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{maximum}})) { + $invalidProperties[] = "invalid value for '{{name}}', must be smaller than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}{{maximum}}."; + } + + {{/maximum}} + {{#minimum}} + if ({{^required}}!is_null($this->container['{{name}}']) && {{/required}}($this->container['{{name}}'] <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{minimum}})) { + $invalidProperties[] = "invalid value for '{{name}}', must be bigger than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}{{minimum}}."; + } + + {{/minimum}} + {{#pattern}} + if ({{^required}}!is_null($this->container['{{name}}']) && {{/required}}!preg_match("{{{pattern}}}", $this->container['{{name}}'])) { + $invalidProperties[] = "invalid value for '{{name}}', must be conform to the pattern {{{pattern}}}."; + } + + {{/pattern}} + {{#maxItems}} + if ({{^required}}!is_null($this->container['{{name}}']) && {{/required}}(count($this->container['{{name}}']) > {{maxItems}})) { + $invalidProperties[] = "invalid value for '{{name}}', number of items must be less than or equal to {{{maxItems}}}."; + } + + {{/maxItems}} + {{#minItems}} + if ({{^required}}!is_null($this->container['{{name}}']) && {{/required}}(count($this->container['{{name}}']) < {{minItems}})) { + $invalidProperties[] = "invalid value for '{{name}}', number of items must be greater than or equal to {{{minItems}}}."; + } + + {{/minItems}} + {{/hasValidation}} + {{/vars}} + return $invalidProperties; + } + + /** + * Validate all the properties in the model + * return true if all passed + * + * @return bool True if all properties are valid + */ + public function valid(): bool + { + return count($this->listInvalidProperties()) === 0; + } + + {{#vars}} + + /** + * Gets {{name}} + * + * @return {{#isArray}}arrayA{{/isArray}}{{^isArray}}{{#isEnum}}string{{/isEnum}}{{^isEnum}}{{#isEnumRef}}string{{/isEnumRef}}{{^isEnumRef}}{{{dataType}}}{{/isEnumRef}}{{/isEnum}}{{/isArray}}{{^required}}|null{{/required}} + {{#deprecated}} + * @deprecated + {{/deprecated}} + */ + public function {{getter}}(): {{^required}}?{{/required}}{{#isArray}}array{{/isArray}}{{^isArray}}{{#isMap}}array{{/isMap}}{{#isEnum}}string{{/isEnum}}{{^isMap}}{{^isEnum}}{{#isEnumRef}}string{{/isEnumRef}}{{^isEnumRef}}{{{dataType}}}{{/isEnumRef}}{{/isEnum}}{{/isMap}}{{/isArray}} + { + return $this->container['{{name}}']; + } + + /** + * Sets {{name}} + * + * @param {{#isArray}}array{{/isArray}}{{^isArray}}{{#isEnum}}string{{/isEnum}}{{^isEnum}}{{#isEnumRef}}string{{/isEnumRef}}{{^isEnumRef}}{{{dataType}}}{{/isEnumRef}}{{/isEnum}}{{/isArray}}{{^required}}|null{{/required}} ${{name}}{{#description}} {{{.}}}{{/description}}{{^description}} {{{name}}}{{/description}} + * + * @return {{^returnType}}self{{/returnType}}{{#returnType}}{{returnType}}{{/returnType}} + {{#deprecated}} + * @deprecated + {{/deprecated}} + */ + public function {{setter}}({{^required}}?{{/required}}{{#isArray}}array{{/isArray}}{{^isArray}}{{#isMap}}array{{/isMap}}{{#isEnum}}string{{/isEnum}}{{^isMap}}{{^isEnum}}{{#isEnumRef}}string{{/isEnumRef}}{{^isEnumRef}}{{{dataType}}}{{/isEnumRef}}{{/isEnum}}{{/isMap}}{{/isArray}} ${{name}}): {{^returnType}}self{{/returnType}}{{#returnType}}{{returnType}}{{/returnType}} + { + {{#isNullable}} + if (is_null(${{name}})) { + array_push($this->openAPINullablesSetToNull, '{{name}}'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('{{name}}', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + {{/isNullable}} + {{^isNullable}} + if (is_null(${{name}})) { + throw new \InvalidArgumentException('non-nullable {{name}} cannot be null'); + } + {{/isNullable}} + {{#isEnum}} + $allowedValues = $this->{{getter}}AllowableValues(); + {{^isContainer}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}!in_array(${{{name}}}, $allowedValues, true)) { + throw new \InvalidArgumentException( + sprintf( + "Invalid value '%s' for '{{name}}', must be one of '%s'", + ${{{name}}}, + implode("', '", $allowedValues) + ) + ); + } + {{/isContainer}} + {{#isContainer}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}array_diff(${{{name}}}, $allowedValues)) { + throw new \InvalidArgumentException( + sprintf( + "Invalid value for '{{name}}', must be one of '%s'", + implode("', '", $allowedValues) + ) + ); + } + {{/isContainer}} + {{/isEnum}} + {{#hasValidation}} + {{#maxLength}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(mb_strlen(${{name}}) > {{maxLength}})) { + throw new \InvalidArgumentException('invalid length for ${{name}} when calling {{classname}}.{{operationId}}, must be smaller than or equal to {{maxLength}}.'); + }{{/maxLength}} + {{#minLength}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(mb_strlen(${{name}}) < {{minLength}})) { + throw new \InvalidArgumentException('invalid length for ${{name}} when calling {{classname}}.{{operationId}}, must be bigger than or equal to {{minLength}}.'); + } + {{/minLength}} + {{#maximum}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(${{name}} >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{maximum}})) { + throw new \InvalidArgumentException('invalid value for ${{name}} when calling {{classname}}.{{operationId}}, must be smaller than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}{{maximum}}.'); + } + {{/maximum}} + {{#minimum}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(${{name}} <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{minimum}})) { + throw new \InvalidArgumentException('invalid value for ${{name}} when calling {{classname}}.{{operationId}}, must be bigger than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}{{minimum}}.'); + } + {{/minimum}} + {{#pattern}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(!preg_match("{{{pattern}}}", ObjectSerializer::toString(${{name}})))) { + throw new \InvalidArgumentException("invalid value for \${{name}} when calling {{classname}}.{{operationId}}, must conform to the pattern {{{pattern}}}."); + } + {{/pattern}} + {{#maxItems}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(count(${{name}}) > {{maxItems}})) { + throw new \InvalidArgumentException('invalid value for ${{name}} when calling {{classname}}.{{operationId}}, number of items must be less than or equal to {{maxItems}}.'); + }{{/maxItems}} + {{#minItems}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(count(${{name}}) < {{minItems}})) { + throw new \InvalidArgumentException('invalid length for ${{name}} when calling {{classname}}.{{operationId}}, number of items must be greater than or equal to {{minItems}}.'); + } + {{/minItems}} + {{/hasValidation}} + $this->container['{{name}}'] = ${{name}}; + + return $this; + } + {{/vars}} + + {{^vars}} + {{#isArray}}{{#items}} + /** + * Gets {{name}} + * + * @return {{{dataType}}}{{^required}}|null{{/required}} + {{#deprecated}} + * @deprecated + {{/deprecated}} + */ + public function {{getter}}(): {{^required}}?{{/required}}{{#isArray}}array{{/isArray}}{{^isArray}}{{{dataType}}}{{/isArray}} + { + return $this->container['{{name}}']; + } + + /** + * Sets {{name}} + * + * @param {{{dataType}}}{{^required}}|null{{/required}} ${{name}}{{#description}} {{{.}}}{{/description}}{{^description}} {{{name}}}{{/description}} + * + * @return {{^returnType}}self{{/returnType}}{{#returnType}}{{returnType}}{{/returnType}} + {{#deprecated}} + * @deprecated + {{/deprecated}} + */ + public function {{setter}}(${{name}}): {{^returnType}}self{{/returnType}}{{#returnType}}{{returnType}}{{/returnType}} + { + {{#isNullable}} + if (is_null(${{name}})) { + array_push($this->openAPINullablesSetToNull, '{{name}}'); + } else { + $nullablesSetToNull = $this->getOpenAPINullablesSetToNull(); + $index = array_search('{{name}}', $nullablesSetToNull); + if ($index !== FALSE) { + unset($nullablesSetToNull[$index]); + $this->setOpenAPINullablesSetToNull($nullablesSetToNull); + } + } + {{/isNullable}} + {{^isNullable}} + if (is_null(${{name}})) { + throw new \InvalidArgumentException('non-nullable {{name}} cannot be null'); + } + {{/isNullable}} + {{#isEnum}} + $allowedValues = $this->{{getter}}AllowableValues(); + {{^isContainer}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}!in_array(${{{name}}}, $allowedValues, true)) { + throw new \InvalidArgumentException( + sprintf( + "Invalid value '%s' for '{{name}}', must be one of '%s'", + ${{{name}}}, + implode("', '", $allowedValues) + ) + ); + } + {{/isContainer}} + {{#isContainer}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}array_diff(${{{name}}}, $allowedValues)) { + throw new \InvalidArgumentException( + sprintf( + "Invalid value for '{{name}}', must be one of '%s'", + implode("', '", $allowedValues) + ) + ); + } + {{/isContainer}} + {{/isEnum}} + {{#hasValidation}} + {{#maxLength}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(mb_strlen(${{name}}) > {{maxLength}})) { + throw new \InvalidArgumentException('invalid length for ${{name}} when calling {{classname}}.{{operationId}}, must be smaller than or equal to {{maxLength}}.'); + }{{/maxLength}} + {{#minLength}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(mb_strlen(${{name}}) < {{minLength}})) { + throw new \InvalidArgumentException('invalid length for ${{name}} when calling {{classname}}.{{operationId}}, must be bigger than or equal to {{minLength}}.'); + } + {{/minLength}} + {{#maximum}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(${{name}} >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{maximum}})) { + throw new \InvalidArgumentException('invalid value for ${{name}} when calling {{classname}}.{{operationId}}, must be smaller than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}{{maximum}}.'); + } + {{/maximum}} + {{#minimum}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(${{name}} <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{minimum}})) { + throw new \InvalidArgumentException('invalid value for ${{name}} when calling {{classname}}.{{operationId}}, must be bigger than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}{{minimum}}.'); + } + {{/minimum}} + {{#pattern}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(!preg_match("{{{pattern}}}", ObjectSerializer::toString(${{name}})))) { + throw new \InvalidArgumentException("invalid value for \${{name}} when calling {{classname}}.{{operationId}}, must conform to the pattern {{{pattern}}}."); + } + {{/pattern}} + {{#maxItems}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(count(${{name}}) > {{maxItems}})) { + throw new \InvalidArgumentException('invalid value for ${{name}} when calling {{classname}}.{{operationId}}, number of items must be less than or equal to {{maxItems}}.'); + }{{/maxItems}} + {{#minItems}} + if ({{#isNullable}}!is_null(${{name}}) && {{/isNullable}}(count(${{name}}) < {{minItems}})) { + throw new \InvalidArgumentException('invalid length for ${{name}} when calling {{classname}}.{{operationId}}, number of items must be greater than or equal to {{minItems}}.'); + } + {{/minItems}} + {{/hasValidation}} + $this->container['{{name}}'] = ${{name}}; + + return $this; + } + {{/items}}{{/isArray}} + {{/vars}} + + /** + * Returns true if offset exists. False otherwise. + * + * @param integer $offset Offset + * + * @return boolean + */ + public function offsetExists($offset): bool + { + return isset($this->container[$offset]); + } + + /** + * Gets offset. + * + * @param integer $offset Offset + * + * @return mixed|null + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset): mixed + { + return $this->container[$offset] ?? null; + } + + /** + * Sets value based on offset. + * + * @param int|null $offset Offset + * @param mixed $value Value to be set + * + * @return void + */ + public function offsetSet($offset, mixed $value): void + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + /** + * Unsets offset. + * + * @param integer $offset Offset + * + * @return void + */ + public function offsetUnset($offset): void + { + unset($this->container[$offset]); + } + + /** + * Serializes the object to a value that can be serialized natively by json_encode(). + * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php + * + * @return mixed Returns data which can be serialized by json_encode(), which is a value + * of any type other than a resource. + */ + #[\ReturnTypeWillChange] + public function jsonSerialize(): mixed + { + return ObjectSerializer::sanitizeForSerialization($this); + } + + /** + * Gets the string presentation of the object + * + * @return string + */ + public function __toString() + { + return json_encode( + ObjectSerializer::sanitizeForSerialization($this), + JSON_PRETTY_PRINT + ); + } + + /** + * Gets a header-safe presentation of the object + * + * @return string + */ + public function toHeaderValue(): string + { + return json_encode(ObjectSerializer::sanitizeForSerialization($this)); + } +} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_test.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_test.mustache new file mode 100644 index 00000000..5316c94d --- /dev/null +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_test.mustache @@ -0,0 +1,98 @@ +model = new {{classname}}(); + } + + /** + * Clean up after running each test case + */ + public function tearDown(): void + { + unset($this->model); + } + + /** + * Test "{{classname}}" + */ + public function test{{classname}}() + { + $this->assertInstanceOf({{classname}}::class, $this->model); + } +{{#vars}} + + /** + * Test attribute "{{name}}" + */ + public function testProperty{{nameInPascalCase}}() + { + {{#isEnum}} + $enumInstance = new {{classname}}(); + $allowedValues = $enumInstance->{{getter}}AllowableValues(); + $testValue = reset($allowedValues); + {{/isEnum}} + {{^isEnum}} + {{#isEnumRef}} + $enumInstance = new {{dataType}}(); + $allowedValues = $enumInstance->getAllowableEnumValues(); + $testValue = reset($allowedValues); + {{/isEnumRef}} + {{#isString}} + $testValue = 'test'; + {{/isString}} + {{#isInteger}} + $testValue = 123; + {{/isInteger}} + {{#isNumber}} + $testValue = 1; + {{/isNumber}} + {{#isBoolean}} + $testValue = true; + {{/isBoolean}} + {{#isArray}} + $testValue = []; + {{/isArray}} + {{^isEnumRef}}{{^isString}}{{^isInteger}}{{^isNumber}}{{^isBoolean}}{{^isArray}} + $testValue = new {{{dataType}}}(); + {{/isArray}}{{/isBoolean}}{{/isNumber}}{{/isInteger}}{{/isString}}{{/isEnumRef}} + {{/isEnum}} + $this->model->set{{nameInPascalCase}}($testValue); + $this->assertEquals($testValue, $this->model->get{{nameInPascalCase}}()); + } +{{/vars}} +} +{{/model}} +{{/models}} diff --git a/clients/sellingpartner-api-aa-php/src/authandauth/LWAAccessTokenCache.php b/clients/sellingpartner-api-aa-php/src/authandauth/LWAAccessTokenCache.php new file mode 100644 index 00000000..c42bedcf --- /dev/null +++ b/clients/sellingpartner-api-aa-php/src/authandauth/LWAAccessTokenCache.php @@ -0,0 +1,45 @@ + $value, + static::EXPIRED_TIME => $accessTokenExpiredTimeMillis + ]; + $this->tokenStorage[$key] = $accessTokenCacheItem; + } + + public function get($key): ?string + { + if (array_key_exists($key, $this->tokenStorage)) { + $accessTokenValue = $this->tokenStorage[$key]; + $accessToken = $accessTokenValue[static::ACCESS_TOKEN]; + $accessTokenExpiredTimeMillis = $accessTokenValue[static::EXPIRED_TIME]; + $currTimeMillis = floor(microtime(true) * 1000); + $accessTokenExpiredWindow = $accessTokenExpiredTimeMillis - static::EXPIRATION_BUFFER; + if ($currTimeMillis < $accessTokenExpiredWindow) { + return $accessToken; + } + } + return null; + } + + public function remove($key) + { + unset($this->tokenStorage[$key]); + } +} diff --git a/clients/sellingpartner-api-aa-php/src/authandauth/LWAAccessTokenRequestMeta.php b/clients/sellingpartner-api-aa-php/src/authandauth/LWAAccessTokenRequestMeta.php new file mode 100644 index 00000000..79f31e8f --- /dev/null +++ b/clients/sellingpartner-api-aa-php/src/authandauth/LWAAccessTokenRequestMeta.php @@ -0,0 +1,99 @@ +refreshToken = $lwaAuthorizationCredentials->getRefreshToken(); + $this->clientId = $lwaAuthorizationCredentials->getClientId(); + $this->clientSecret = $lwaAuthorizationCredentials->getClientSecret(); + $this->scopes = $lwaAuthorizationCredentials->getScopes(); + + if (!empty($lwaAuthorizationCredentials->getScopes())) { + $this->grantType = "client_credentials"; + } else { + $this->grantType = "refresh_token"; + } + } + + public function jsonSerialize(): array + { + return [ + static::GRANT_TYPE_SERIALIZED => $this->grantType, + static::CLIENT_ID_SERIALIZED => $this->clientId, + static::CLIENT_SECRET_SERIALIZED => $this->clientSecret, + static::REFRESH_TOKEN_SERIALIZED => $this->refreshToken, + static::SCOPE_SERIALIZED => $this->scopes ? implode(" ", $this->scopes) : null + ]; + } + + public function getGrantType(): string + { + return $this->grantType; + } + + public function setGrantType(string $grantType) + { + $this->grantType = $grantType; + } + + public function getRefreshToken(): ?string + { + return $this->refreshToken; + } + + public function setRefreshToken(string $refreshToken) + { + $this->refreshToken = $refreshToken; + } + + public function getClientId(): string + { + return $this->clientId; + } + + public function setClientId(string $clientId) + { + $this->clientId = $clientId; + } + + public function getClientSecret(): string + { + return $this->clientSecret; + } + + public function setClientSecret(string $clientSecret) + { + $this->clientSecret = $clientSecret; + } + + public function getScopes(): ?array + { + return $this->scopes; + } + + public function setScopes(?array $scopes) + { + $this->scopes = $scopes; + } +} diff --git a/clients/sellingpartner-api-aa-php/src/authandauth/LWAAuthorizationCredentials.php b/clients/sellingpartner-api-aa-php/src/authandauth/LWAAuthorizationCredentials.php new file mode 100644 index 00000000..9285c8bf --- /dev/null +++ b/clients/sellingpartner-api-aa-php/src/authandauth/LWAAuthorizationCredentials.php @@ -0,0 +1,105 @@ +clientId = $config["clientId"]; + $this->clientSecret = $config["clientSecret"]; + $this->refreshToken = $config["refreshToken"] ?? null; + $this->endpoint = $config["endpoint"]; + $this->scopes = $config["scopes"] ?? null; + } + + public function getClientId(): string + { + return $this->clientId; + } + + public function setClientId(string $clientId): LWAAuthorizationCredentials + { + $this->clientId = $clientId; + return $this; + } + + public function getClientSecret(): string + { + return $this->clientSecret; + } + + public function setClientSecret(string $clientSecret): LWAAuthorizationCredentials + { + $this->clientSecret = $clientSecret; + return $this; + } + + public function getRefreshToken(): ?string + { + return $this->refreshToken; + } + + public function setRefreshToken(?string $refreshToken): LWAAuthorizationCredentials + { + $this->refreshToken = $refreshToken; + return $this; + } + + public function getEndpoint(): string + { + return $this->endpoint; + } + + public function setEndpoint(string $endpoint): LWAAuthorizationCredentials + { + $this->endpoint = $endpoint; + return $this; + } + + public function getScopes(): ?array + { + return $this->scopes; + } + + public function setScopes(?array $scopes): LWAAuthorizationCredentials + { + $this->scopes = $scopes; + return $this; + } +} diff --git a/clients/sellingpartner-api-aa-php/src/authandauth/LWAAuthorizationSigner.php b/clients/sellingpartner-api-aa-php/src/authandauth/LWAAuthorizationSigner.php new file mode 100644 index 00000000..d9a258f6 --- /dev/null +++ b/clients/sellingpartner-api-aa-php/src/authandauth/LWAAuthorizationSigner.php @@ -0,0 +1,40 @@ +lwaClient = new LWAClient($lwaAuthorizationCredentials->getEndpoint()); + $this->lwaAccessTokenRequestMeta = new LWAAccessTokenRequestMeta($lwaAuthorizationCredentials); + $this->lwaClient->setLWAAccessTokenCache($lwaAccessTokenCache); + } + + public function sign(Request $request): Request + { + $accessToken = $this->lwaClient->getAccessToken($this->lwaAccessTokenRequestMeta); + $request = $request->withHeader(static::SIGNED_ACCESS_TOKEN_HEADER_NAME, $accessToken); + return $request; + } + + public function getLwaClient(): LWAClient + { + return $this->lwaClient; + } + + public function setLwaClient(LWAClient $lwaClient): void + { + $this->lwaClient = $lwaClient; + } +} diff --git a/clients/sellingpartner-api-aa-php/src/authandauth/LWAClient.php b/clients/sellingpartner-api-aa-php/src/authandauth/LWAClient.php new file mode 100644 index 00000000..da2c7af7 --- /dev/null +++ b/clients/sellingpartner-api-aa-php/src/authandauth/LWAClient.php @@ -0,0 +1,103 @@ +client = new Client(); + $this->endpoint = $endpoint; + } + + public function setLWAAccessTokenCache(?LWAAccessTokenCache $tokenCache): void + { + $this->lwaAccessTokenCache = $tokenCache; + } + + public function getAccessToken(LWAAccessTokenRequestMeta &$lwaAccessTokenRequestMeta): string + { + if ($this->lwaAccessTokenCache !== null) { + return $this->getAccessTokenFromCache($lwaAccessTokenRequestMeta); + } else { + return $this->getAccessTokenFromEndpoint($lwaAccessTokenRequestMeta); + } + } + + public function getAccessTokenFromCache(LWAAccessTokenRequestMeta &$lwaAccessTokenRequestMeta) + { + $requestBody = json_encode($lwaAccessTokenRequestMeta); + if (!$requestBody) { + throw new RuntimeException("Request body could not be encoded"); + } + $accessTokenCacheData = $this->lwaAccessTokenCache->get($requestBody); + if ($accessTokenCacheData !== null) { + return $accessTokenCacheData; + } else { + return $this->getAccessTokenFromEndpoint($lwaAccessTokenRequestMeta); + } + } + + public function getAccessTokenFromEndpoint(LWAAccessTokenRequestMeta &$lwaAccessTokenRequestMeta) + { + $requestBody = json_encode($lwaAccessTokenRequestMeta); + + if (!$requestBody) { + throw new RuntimeException("Request body could not be encoded"); + } + + $contentHeader = [ + "Content-Type" => "application/json", + ]; + + try { + $lwaRequest = new Request("POST", $this->endpoint, $contentHeader, $requestBody); + + $lwaResponse = $this->client->send($lwaRequest); + $responseJson = json_decode($lwaResponse->getBody(), true); + + if (!$responseJson["access_token"] || !$responseJson["expires_in"]) { + throw new RuntimeException("Response did not have required body"); + } + + $accessToken = $responseJson["access_token"]; + + if ($this->lwaAccessTokenCache !== null) { + $timeToTokenExpire = (float)$responseJson["expires_in"]; + $this->lwaAccessTokenCache->set($requestBody, $accessToken, $timeToTokenExpire); + } + } catch (BadResponseException $e) { + //Catches 400 and 500 level error codes + throw new RuntimeException("Unsuccessful LWA token exchange", $e->getCode()); + } catch (Exception $e) { + throw new RuntimeException("Error getting LWA Access Token", $e->getCode()); + } catch (GuzzleException $e) { + throw new RuntimeException("Error getting LWA Access Token", $e->getCode()); + } + + return $accessToken; + } + + public function setClient(Client $client): void + { + $this->client = $client; + } + + public function getEndpoint(): string + { + return $this->endpoint; + } +} diff --git a/clients/sellingpartner-api-aa-php/src/authandauth/RateLimitConfiguration.php b/clients/sellingpartner-api-aa-php/src/authandauth/RateLimitConfiguration.php new file mode 100644 index 00000000..1d00200f --- /dev/null +++ b/clients/sellingpartner-api-aa-php/src/authandauth/RateLimitConfiguration.php @@ -0,0 +1,14 @@ +rateLimitType = $config["rateLimitType"] ?? "token_bucket"; + $this->rateLimitToken = $config["rateLimitToken"]; + $this->rateLimitTokenLimit = $config["rateLimitTokenLimit"]; + + $this->waitTimeOutInMilliSeconds = $config["waitTimeOutInMilliSeconds"] ?? 0; + if ($this->rateLimitType === "sliding_window" && $this->waitTimeOutInMilliSeconds != 0) { + throw new InvalidArgumentException("Sliding Window RateLimiter cannot reserve tokens"); + } + } + + public function getRateLimitType(): string + { + return $this->rateLimitType; + } + + public function setRateLimitType(string $rateLimitType): RateLimitConfigurationOnRequests + { + $this->rateLimitType = $rateLimitType; + return $this; + } + + public function getRateLimitToken(): int + { + return $this->rateLimitToken; + } + + public function setRateLimitToken(int $rateLimitToken): RateLimitConfigurationOnRequests + { + $this->rateLimitToken = $rateLimitToken; + return $this; + } + + public function getRateLimitTokenLimit(): int + { + return $this->rateLimitTokenLimit; + } + + public function setRateLimitTokenLimit(int $rateLimitTokenLimit): RateLimitConfigurationOnRequests + { + $this->rateLimitTokenLimit = $rateLimitTokenLimit; + return $this; + } + + public function getTimeOut(): float + { + return $this->waitTimeOutInMilliSeconds; + } + + public function setTimeOut(float $waitTimeOutInMilliSeconds): RateLimitConfigurationOnRequests + { + $this->waitTimeOutInMilliSeconds = $waitTimeOutInMilliSeconds; + return $this; + } +} diff --git a/clients/sellingpartner-api-aa-php/src/authandauth/ScopeConstants.php b/clients/sellingpartner-api-aa-php/src/authandauth/ScopeConstants.php new file mode 100644 index 00000000..26e117fd --- /dev/null +++ b/clients/sellingpartner-api-aa-php/src/authandauth/ScopeConstants.php @@ -0,0 +1,9 @@ + self::TEST_CLIENT_ID, + "clientSecret" => self::TEST_CLIENT_SECRET, + "refreshToken" => self::TEST_REFRESH_TOKEN, + "endpoint" => self::TEST_ENDPOINT + ]) + ); + + self::$underTestSellerless = new LWAAuthorizationSigner( + new LWAAuthorizationCredentials([ + "clientId" => self::TEST_CLIENT_ID, + "clientSecret" => self::TEST_CLIENT_SECRET, + "scopes" => [ + self::SCOPE_NOTIFICATIONS_API, + self::SCOPE_MIGRATION_API + ], + "endpoint" => self::TEST_ENDPOINT + ]) + ); + } + + protected function setUp(): void + { + $this->request = new Request("GET", "https://www.amazon.com/api"); + } + + public function lwaSignerProvider(): array + { + return [ + [self::SELLER_TYPE_SELLER, &self::$underTestSeller], + [self::SELLER_TYPE_SELLERLESS, &self::$underTestSellerless] + ]; + } + + /** + * @dataProvider lwaSignerProvider + */ + public function testInitializeLWAClientWithConfiguredEndpoint( + string $sellerType, + LWAAuthorizationSigner $testAuthSigner + ): void { + $this->assertSame( + self::TEST_ENDPOINT, + $testAuthSigner->getLwaClient()->getEndpoint() + ); + } + + /** + * @dataProvider lwaSignerProvider + */ + public function testRequestLWAAccessTokenFromConfiguration( + string $sellerType, + LWAAuthorizationSigner $testAuthSigner + ) { + $mockLWAClient = $this->createMock(LWAClient::class); + $mockLWAClient + ->method("getAccessToken") + ->willReturnCallback(function (LWAAccessTokenRequestMeta &$lwaAccessTokenRequestMeta) use ($sellerType) { + $this->assertEquals( + self::TEST_CLIENT_SECRET, + $lwaAccessTokenRequestMeta->getClientSecret() + ); + $this->assertEquals( + self::TEST_CLIENT_ID, + $lwaAccessTokenRequestMeta->getClientId() + ); + + if ($sellerType == self::SELLER_TYPE_SELLER) { + $this->assertEmpty($lwaAccessTokenRequestMeta->getScopes()); + $this->assertEquals("refresh_token", $lwaAccessTokenRequestMeta->getGrantType()); + $this->assertEquals( + self::TEST_REFRESH_TOKEN, + $lwaAccessTokenRequestMeta->getRefreshToken() + ); + } elseif ($sellerType == self::SELLER_TYPE_SELLERLESS) { + $this->assertEquals( + [ + self::SCOPE_NOTIFICATIONS_API, + self::SCOPE_MIGRATION_API + ], + $lwaAccessTokenRequestMeta->getScopes() + ); + $this->assertEquals("client_credentials", $lwaAccessTokenRequestMeta->getGrantType()); + $this->assertNull($lwaAccessTokenRequestMeta->getRefreshToken()); + } + return "foo"; + }); + + $testAuthSigner->setLwaClient($mockLWAClient); + $testAuthSigner->sign($this->request); + } + + /** + * @dataProvider lwaSignerProvider + */ + public function testReturnSignedRequestWithAccessTokenFromLWAClient( + string $sellerType, + LWAAuthorizationSigner $testAuthSigner + ) { + $mockLWAClient = $this->createMock(LWAClient::class); + + $mockLWAClient->method("getAccessToken")->willReturn("Azta|Foo"); + + $testAuthSigner->setLwaClient($mockLWAClient); + $actualSignedRequest = $testAuthSigner->sign($this->request); + $this->assertEquals("Azta|Foo", $actualSignedRequest->getHeader("x-amz-access-token")[0]); + } + + /** + * @dataProvider lwaSignerProvider + */ + public function testOriginalRequestIsImmutable(string $sellerType, LWAAuthorizationSigner $testAuthSigner) + { + $mockLWAClient = $this->createMock(LWAClient::class); + + $mockLWAClient->method("getAccessToken")->willReturn("Azta|Foo"); + + $testAuthSigner->setLwaClient($mockLWAClient); + $this->assertNotSame($this->request, $testAuthSigner->sign($this->request)); + } + + public function testReturnSignedRequestWithAccessTokenFromLWACache() + { + $mockHandler = new MockHandler([ + $this->buildResponse(200, "Azta|foo", "120"), + $this->buildResponse(200, "Azta|foo1", "1") + ]); + + $handlerStack = HandlerStack::create($mockHandler); + $mockGuzzleClient = new Client(["handler" => $handlerStack]); + + $testLWAClient = new LWAClient(self::TEST_ENDPOINT); + $testLWAClient->setClient($mockGuzzleClient); + + $testLWACache = new LWAAccessTokenCache(); + + $testlwaSigner = new LWAAuthorizationSigner( + new LWAAuthorizationCredentials([ + "clientId" => self::TEST_CLIENT_ID, + "clientSecret" => self::TEST_CLIENT_SECRET, + "refreshToken" => self::TEST_REFRESH_TOKEN, + "endpoint" => self::TEST_ENDPOINT + ]), + $testLWACache + ); + + $testlwaSigner->setLwaClient($testLWAClient); + $testLWAClient->setLWAAccessTokenCache($testLWACache); + $actualSignedRequest = $testlwaSigner->sign($this->request); + $actualSignedSecondRequest = $testlwaSigner->sign($this->request); + + $this->assertEquals("Azta|foo", $actualSignedSecondRequest->getHeader("x-amz-access-token")[0]); + } + + private function buildResponse(int $code, string $accessToken, string $expiryInSeconds = "3600"): ResponseInterface + { + $contentHeader = [ + "Content-Type" => "application/json", + ]; + return new Response( + $code, + $contentHeader, + json_encode(["access_token" => $accessToken, "expires_in" => $expiryInSeconds]) + ); + } +} diff --git a/clients/sellingpartner-api-aa-php/tests/authandauth/LWAClientTest.php b/clients/sellingpartner-api-aa-php/tests/authandauth/LWAClientTest.php new file mode 100644 index 00000000..05dfde4b --- /dev/null +++ b/clients/sellingpartner-api-aa-php/tests/authandauth/LWAClientTest.php @@ -0,0 +1,209 @@ + self::TEST_CLIENT_ID, + "clientSecret" => self::TEST_CLIENT_SECRET, + "refreshToken" => self::TEST_REFRESH_TOKEN, + "endpoint" => self::TEST_ENDPOINT + ]); + $lwaSellerlessCredentials = new LWAAuthorizationCredentials([ + "clientId" => self::TEST_CLIENT_ID, + "clientSecret" => self::TEST_CLIENT_SECRET, + "scopes" => [self::SCOPE_NOTIFICATIONS_API, self::SCOPE_MIGRATION_API], + "endpoint" => self::TEST_ENDPOINT + ]); + self::$lwaAccessTokenRequestMetaSeller = new LWAAccessTokenRequestMeta($lwaSellerCredentials); + self::$lwaAccessTokenRequestMetaSellerless = new LWAAccessTokenRequestMeta($lwaSellerlessCredentials); + } + + protected function setUp(): void + { + $this->mockGuzzleClient = $this->createMock(Client::class); + $this->underTest = new LWAClient(self::TEST_ENDPOINT); + $this->underTest->setClient($this->mockGuzzleClient); + } + + public function lwaClientProvider(): array + { + return [ + [self::SELLER_TYPE_SELLER, &self::$lwaAccessTokenRequestMetaSeller], + [self::SELLER_TYPE_SELLERLESS, &self::$lwaAccessTokenRequestMetaSellerless] + ]; + } + + public function testInitializeEndpoint() + { + $this->assertEquals(self::TEST_ENDPOINT, $this->underTest->getEndpoint()); + } + + /** + * @dataProvider lwaClientProvider + */ + public function testMakeRequestFromMeta( + string $sellerType, + LWAAccessTokenRequestMeta $testLwaAccessTokenRequestMeta + ) { + $this->mockGuzzleClient + ->expects($this->once()) + ->method("send") + ->willReturnCallback(function (Request $requestInput) use ($sellerType) { + $this->assertEquals(self::TEST_ENDPOINT, $requestInput->getUri()); + $this->assertEquals("POST", $requestInput->getMethod()); + $requestJson = json_decode((string)$requestInput->getBody(), true); + if ($sellerType == self::SELLER_TYPE_SELLER) { + $this->assertEquals("rToken", $requestJson["refresh_token"]); + } elseif ($sellerType == self::SELLER_TYPE_SELLERLESS) { + $this->assertNotNull($requestJson["scope"]); + } + $this->assertEquals("cId", $requestJson["client_id"]); + $this->assertEquals("cSecret", $requestJson["client_secret"]); + $this->assertEquals("application/json", $requestInput->getHeader("Content-Type")[0]); + return $this->buildResponse(200, "foo"); + }); + + $this->underTest->setClient($this->mockGuzzleClient); + $this->underTest->getAccessToken($testLwaAccessTokenRequestMeta); + } + + /** + * @dataProvider lwaClientProvider + */ + public function testReturnAccessTokenFromResponse( + string $sellerType, + LWAAccessTokenRequestMeta $testLwaAccessTokenRequestMeta + ) { + $this->mockGuzzleClient + ->method("send") + ->willReturn( + $this->buildResponse(200, "Azta|foo") + ); + + $this->assertEquals("Azta|foo", $this->underTest->getAccessToken($testLwaAccessTokenRequestMeta)); + } + + /** + * @dataProvider lwaClientProvider + */ + public function testUnsuccessfulPostThrowsException( + string $sellerType, + LWAAccessTokenRequestMeta $testLwaAccessTokenRequestMeta + ) { + //Guzzlehttp automatically throws exceptions for 400/500 level errors + //Using phpunit mocking doesn't mock that behavior + $mockHandler = new MockHandler([ + $this->buildResponse(400, "Azta|foo") + ]); + + $handlerStack = HandlerStack::create($mockHandler); + $mockGuzzleExceptionClient = new Client(["handler" => $handlerStack]); + + $exceptionTest = new LWAClient(self::TEST_ENDPOINT); + $exceptionTest->setClient($mockGuzzleExceptionClient); + + $this->expectException(RuntimeException::class); + + $exceptionTest->getAccessToken($testLwaAccessTokenRequestMeta); + } + + /** + * @dataProvider lwaClientProvider + */ + public function testMissingAccessTokenInResponseThrowsException( + string $sellerType, + LWAAccessTokenRequestMeta $testLwaAccessTokenRequestMeta + ) { + $this->mockGuzzleClient + ->method("send") + ->willReturn( + $this->buildResponse(200, "") + ); + + $this->expectException(RuntimeException::class); + + $this->underTest->getAccessToken($testLwaAccessTokenRequestMeta); + } + + public function testReturnAccessTokenFromCache() + { + $this->mockGuzzleClient + ->method("send") + ->willThrowException(new Exception()) + ->willReturn( + $this->buildResponse(200, "Azta|foo", "120") + ); + + $this->underTest->setLWAAccessTokenCache(new LWAAccessTokenCache()); + + //First call should get from Endpoint + $this->assertEquals("Azta|foo", $this->underTest->getAccessToken(self::$lwaAccessTokenRequestMetaSeller)); + //Second call when the cache is still valid, if it goes to Endpoint it will return Exception + $this->assertEquals("Azta|foo", $this->underTest->getAccessToken(self::$lwaAccessTokenRequestMetaSeller)); + } + + public function testReturnAccessTokenFromCacheWithExpiry() + { + $client = new LWAClient(self::TEST_ENDPOINT); + $client->setClient($this->mockGuzzleClient); + + $this->mockGuzzleClient + ->method("send") + ->willReturnOnConsecutiveCalls( + $this->buildResponse(200, "Azta|foo", "1"), + $this->buildResponse(200, "Azta|foo1", "1") + ); + + //First call should get from Endpoint + $this->assertEquals("Azta|foo", $client->getAccessToken(self::$lwaAccessTokenRequestMetaSeller)); + //Second call should again go to the Endpoint because accessToken is expired after expiry adjustment + $this->assertEquals("Azta|foo1", $client->getAccessToken(self::$lwaAccessTokenRequestMetaSeller)); + } + + private function buildResponse(int $code, string $accessToken, string $expiryInSeconds = "3600"): ResponseInterface + { + $contentHeader = [ + "Content-Type" => "application/json", + ]; + return new Response( + $code, + $contentHeader, + json_encode(["access_token" => $accessToken, "expires_in" => $expiryInSeconds]) + ); + } +} From abc50dfe3c0448ae897c170a47736a270f75b08d Mon Sep 17 00:00:00 2001 From: nakagleo Date: Wed, 29 Jan 2025 10:53:02 -0300 Subject: [PATCH 2/3] Removing test cases and user-agent fix --- .../resources/openapi-generator/config.json | 8 - .../templates/Configuration.mustache | 2 +- .../templates/TestHelper.mustache | 678 ------------------ .../templates/api_test.mustache | 220 ------ .../templates/model_test.mustache | 98 --- 5 files changed, 1 insertion(+), 1005 deletions(-) delete mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/TestHelper.mustache delete mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api_test.mustache delete mode 100644 clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_test.mustache diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/config.json b/clients/sellingpartner-api-aa-php/resources/openapi-generator/config.json index 07bd3f9f..cac57f1a 100644 --- a/clients/sellingpartner-api-aa-php/resources/openapi-generator/config.json +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/config.json @@ -4,17 +4,9 @@ "common_header" : "/**\n * Selling Partner API\n *\n * The Selling Partner API enables developers to programmatically retrieve information from various domains.\n * These APIs provide tools for building fast, flexible, and custom applications,\n * as well as demand-based decision support systems.\n *\n * The version of the OpenAPI document: v0\n * Generated by: https://openapi-generator.tech\n * Generator version: 7.9.0\n */" }, "files": { - "TestHelper.mustache": { - "templateFile": "TestHelper.mustache", - "destinationFilename": "test/TestHelper.php" - }, "ModelInterface.mustache": { "templateFile": "ModelInterface.mustache", "destinationFilename": "lib/Model/ModelInterface.php" - }, - "BaseTestCase.mustache": { - "templateFile": "BaseTestCase.mustache", - "destinationFilename": "test/Api/BaseTestCase.php" } } } diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/Configuration.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/Configuration.mustache index 8329bcde..43b773d1 100644 --- a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/Configuration.mustache +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/Configuration.mustache @@ -63,7 +63,7 @@ class Configuration * * @var string */ - protected string $userAgent = 'selling-partner-api-sdk/PHP/{{{artifactVersion}}}{{^artifactVersion}}1.0.0{{/artifactVersion}}'; + protected string $userAgent = 'OpenAPI-Generator/{{{artifactVersion}}}{{^artifactVersion}}1.0.0{{/artifactVersion}}/PHP'; /** * Debug switch (default set to false) diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/TestHelper.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/TestHelper.mustache deleted file mode 100644 index 9e4c7089..00000000 --- a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/TestHelper.mustache +++ /dev/null @@ -1,678 +0,0 @@ -getName(); - - // Handle special case for 'payload' parameter. - if ( - $paramName === 'payload' - || $paramName === 'body' - || $paramName === 'requests' - || $paramName === 'get_featured_offer_expected_price_batch_request_body' - || $paramName === 'get_item_offers_batch_request_body' - || $paramName === 'get_listing_offers_batch_request_body' - || $paramName === 'create_inventory_item_request_body' - || $paramName === 'create_scheduled_package_request' - || $paramName === 'list_handover_slots_request' - || $paramName === 'update_scheduled_packages_request' - ) { - $typeName = $param->getType()->getName(); - if (class_exists($typeName)) { - $requestInstance = new $typeName(); - if (isset($requestParameters['body']['value'])) { - $requestInstance = - ObjectSerializer::deserialize($requestParameters['body']['value'], $typeName, []); - } elseif ( - // TODO This condition should be removed when EasyShip swagger is fixed - isset($requestParameters['body']) - && ($paramName === 'update_scheduled_packages_request' - || $paramName === 'list_handover_slots_request') - ) { - $requestInstance = - ObjectSerializer::deserialize($requestParameters['body'], $typeName, []); - } elseif (!$param->isOptional()) { - // Insert Dummy object - $openAPITypes = $typeName::openAPITypes(); - $setters = $typeName::setters(); - - foreach ($openAPITypes as $propertyName => $propertyType) { - // Skip if the property is nullable - if ($typeName::isNullable($propertyName)) { - continue; - } - - // Generate dummy value based on the type - $dummyValue = self::getDummyValueForType($propertyType, $propertyName); - - // Check if a setter exists for the property - if (array_key_exists($propertyName, $setters)) { - $setterMethod = $setters[$propertyName]; - if (method_exists($requestInstance, $setterMethod)) { - // Call the setter method with the dummy value - $requestInstance->$setterMethod($dummyValue); - } - } - } - } - - $requestParams['payload'] = $requestInstance; - } elseif ($typeName === 'array') { - $requestParams['payload'] = $requestParameters['body']['value']; - } - continue; - } - - // Process regular parameters with snake_case to camelCase conversion. - $value = null; - if ($requestParameters) { - foreach ([false, true] as $capitalizeFirst) { - $camelCaseName = self::snakeToCamelCase($paramName, $capitalizeFirst); - // Check for standard camelCase name - $subArrayValue = self::extractValue($camelCaseName, $requestParameters); - if ($subArrayValue !== null) { - $value = $subArrayValue; - break; - } - // Special handling for 'Sku' - if (str_contains($camelCaseName, 'Sku')) { - $camelCaseName = str_replace('Sku', 'SKU', $camelCaseName); - $subArrayValue = self::extractValue($camelCaseName, $requestParameters); - if ($subArrayValue !== null) { - $value = $subArrayValue; - break; - } - // Special handling for 'ASINList' - } elseif ($camelCaseName === 'asinList') { - $camelCaseName = 'ASINList'; - $subArrayValue = self::extractValue($camelCaseName, $requestParameters); - if ($subArrayValue !== null) { - $value = $subArrayValue; - break; - } - } - } - } - if (empty($value) && !$param->isOptional()) { - $typeName = $param->getType()->getName(); - // Insert Dummy parameter - $value = self::getDummyValueForType($typeName, $paramName); - } - $requestParams[$paramName] = $value; - } - - return $requestParams; - } - - /** - * Helper method to extract a value from the request parameters. - * - * @param string $Name - * @param array $requestParameters The array of request parameters to search within. - * @return string|null The value if found and valid, otherwise null. - */ - private static function extractValue(string $Name, array $requestParameters): mixed - { - if (isset($requestParameters[$Name])) { - $subArray = $requestParameters[$Name]; - if (is_array($subArray) && isset($subArray['value'])) { - return $subArray['value']; - } - } - - return null; - } - - /** - * Generates a UUID (Universally Unique Identifier) version 4. - * - * This method generates a random UUID (v4) in compliance with RFC 4122. It uses random bytes for - * entropy and ensures the version and variant fields are correctly set according to the specification. - * - * UUID v4 format example: 123e4567-e89b-12d3-a456-426614174000 - * - * @return string A randomly generated UUID v4 as a string. - */ - private static function generateUuidV4(): string - { - $data = openssl_random_pseudo_bytes(16); - - // Set the version to 0100 (UUID v4) - $data[6] = chr(ord($data[6]) & 0x0f | 0x40); - - // Set the variant to 10xx (RFC 4122) - $data[8] = chr(ord($data[8]) & 0x3f | 0x80); - - // Convert to the UUID string format - return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); - } - - /** - * Returns a dummy value based on the given type. - * - * This method generates a dummy value for the specified type name and parameter name. It handles a variety - * of data types, including primitives, arrays, objects, and enumerations. If the type is an object, it - * recursively populates its properties with dummy values. - * - * @param string $typeName The type name for which a dummy value is needed. - * @param string $paramName The parameter name associated with the type. - * @return mixed A dummy value appropriate for the given type. - * @throws ReflectionException If there is an error instantiating a class using reflection. - */ - private static function getDummyValueForType(string $typeName, string $paramName): mixed - { - // Handle domain specific case - if (isset(self::$classSpecificValue[$paramName])) { - return self::$classSpecificValue[$paramName]; - // Listing - } elseif ($paramName === 'marketplace_ids') { - return [self::$marketPlaceId]; - } - // Handle array and specific object types - if (str_ends_with($typeName, '[]')) { - $elementType = substr($typeName, 0, -2); - return [self::getDummyValueForType($elementType, $paramName)]; - } - if ($typeName === '\DateTime' || $typeName === 'DateTime') { - return new \DateTime(); - } - - if (class_exists($typeName)) { - $reflectionClass = new ReflectionClass($typeName); - $instance = $reflectionClass->newInstance(); - // Enum - if (method_exists($instance, 'getAllowableEnumValues')) { - $allowableValues = $instance::getAllowableEnumValues(); - return reset($allowableValues); - } - // Populate object properties recursively - $openAPITypes = $instance::openAPITypes(); - $setters = $typeName::setters(); - - foreach ($openAPITypes as $propertyName => $propertyType) { - // Skip if the property is nullable - if ($typeName::isNullable($propertyName)) { - continue; - } - - // Generate dummy value based on the type - $dummyValue = self::getDummyValueForType($propertyType, $propertyName); - - // Check if a setter exists for the property - if (array_key_exists($propertyName, $setters)) { - $setterMethod = $setters[$propertyName]; - if (method_exists($instance, $setterMethod)) { - // Call the setter method with the dummy value - $instance->$setterMethod($dummyValue); - } - } - } - - return $instance; - } - - // Handle primitive types - return match ($typeName) { - 'int' => 1, - 'float' => 1.0, - 'bool' => false, - 'string' => 'test', - 'array' => ["1"], - default => null, - }; - } - - /** - * @param $apiInstance - * @param string $methodName - * @return ReflectionMethod - * @throws ReflectionException - */ - private static function getReflectionMethod($apiInstance, string $methodName): ReflectionMethod - { - return new ReflectionMethod($apiInstance, $methodName); - } - - /** - * Extracts the request parameters and expected response from a JSON schema. - * - * This method processes a JSON schema string, decodes it, and extracts the - * request parameters and expected response for a specified operation ID. - * It prepares the request parameters to match the method signature of - * the provided API instance. - * - * @param object $apiInstance The instance of the API class that contains the method to be invoked. - * @param string $jsonSchema The JSON schema string representing the request and response definitions. - * @param string $operationId The operation ID corresponding to the method in the API instance. - * - * @return array An associative array containing: - * - `requestParams` (array): Prepared request parameters for the specified method. - * - `expectedResponse` (mixed): The expected response extracted from the JSON schema. - * - * @throws \InvalidArgumentException If the provided JSON schema is invalid or cannot be decoded. - * @throws ReflectionException If there is an error during the preparation of request parameters - * (e.g., method does not exist in the API instance). - */ - public function extractRequestAndResponse(object $apiInstance, string $jsonSchema, string $operationId): array - { - // Decode HTML entities - $codegenText = html_entity_decode($jsonSchema); - - // Remove unnecessary characters - $codegenText = str_replace(["\r", "\n"], '', $codegenText); - //$codegenText = str_replace(' ', '', $codegenText); - - // Decode JSON - $data = json_decode($codegenText, true); - if (json_last_error() !== JSON_ERROR_NONE) { - throw new \InvalidArgumentException("Invalid JSON schema provided: " . json_last_error_msg()); - } - - // Extract request object - $request = $data['x-amzn-api-sandbox']['static'][0]['request']['parameters'] ?? null; - - // Prepare request parameters - $reflection = self::getReflectionMethod($apiInstance, $operationId); - $params = $reflection->getParameters(); - $requestParams = self::prepareRequestParams($params, $request); - - // Extract expected response - $expectedResponse = $data['x-amzn-api-sandbox']['static'][0]['response'] ?? null; - if ($expectedResponse !== null) { - $returnType = $reflection->getReturnType(); - if (class_exists($returnType)) { - $expectedResponse = - ObjectSerializer::deserialize($expectedResponse, $returnType, []); - } - } - - return [ - 'requestParams' => $requestParams, - 'expectedResponse' => $expectedResponse - ]; - } - - /** - * Builds a request for dynamic sandbox testing. - * - * This method prepares the request parameters needed for the specified API operation in a dynamic sandbox - * environment. It uses reflection to retrieve the parameters of the given operation and prepares them accordingly. - * - * @param object $apiInstance The API instance that contains the operation. - * @param string $operationId The ID of the operation for which the request is being built. - * @return array Returns an array of prepared request parameters for the specified operation. - * @throws ReflectionException - */ - public function buildRequestForDynamicSandBox(object $apiInstance, string $operationId): array - { - // Prepare request parameters - $reflection = self::getReflectionMethod($apiInstance, $operationId); - $params = $reflection->getParameters(); - return self::prepareRequestParams($params, null); - } - - /** - * Array that defines TestCase Name requires specific ateTimeFormat - * @var array - */ - private array $dateTimeFormatSpecificCase = [ - 'FeesApi' => 'D M d H:i:s T Y', - 'ListingsItemsApi' => 'Y-m-d\TH:i:s\Z', - 'OffersApi' => 'Y-m-d\TH:i:s\Z', - 'SellingpartnersApi' => 'Y-m-d\TH:i:s\Z' - ]; - - - /** - * Get required DateTimeFormat for a given testCaseName. - * - * @param string $caseName - * @return string|null - */ - public function getDateTimeFormatForCase(string $caseName): ?string - { - return $this->dateTimeFormatSpecificCase[$caseName] ?? null; - } - - /** - * Array that defines scopes parameter required for grantless operation such as Notification API - * @var array|string[] - */ - private array $scopesRequiredMap = [ - 'testCreateDestination200' => ['sellingpartnerapi::notifications'], - 'testGetDestination200' => ['sellingpartnerapi::notifications'], - 'testDeleteDestination200' => ['sellingpartnerapi::notifications'], - 'testGetDestinations200' => ['sellingpartnerapi::notifications'], - 'testDeleteSubscriptionById200' => ['sellingpartnerapi::notifications'], - 'testGetSubscriptionById200' => ['sellingpartnerapi::notifications'], - ]; - - - /** - * Get required scopes for a given caseName. - * - * @param string $caseName - * @return array - */ - public function getScopesForApi(string $caseName): array - { - return $this->scopesRequiredMap[$caseName] ?? []; - } - - /** - * Array that defines Vendor related API Test class name - * @var array|string[] - */ - private static array $vendorApiTestClassNameList = [ - 'OpenAPI\Client\Test\Api\UpdateInventoryApiTest', - 'OpenAPI\Client\Test\Api\VendorOrdersApiTest', - 'OpenAPI\Client\Test\Api\VendorInvoiceApiTest', - 'OpenAPI\Client\Test\Api\VendorShippingApiTest', - 'OpenAPI\Client\Test\Api\VendorShippingLabelsApiTest', - 'OpenAPI\Client\Test\Api\VendorTransactionApi' - ]; - - /** - * Checks if the given class name is related to a vendor-related API. - * - * This method determines if the specified class name exists in the list - * of predefined vendor-related API test class names. - * - * @param string $className The fully qualified name of the class to check. - * @return bool Returns true if the class name is related to a vendor-related API; otherwise, false. - */ - public static function isVendorRelatedApi(string $className): bool - { - if (in_array($className, TestHelper::$vendorApiTestClassNameList)) { - return true; - } - return false; - } - - /** - * Checks if the test case should be skipped. - * - * @param string $testCaseName - * @param string|null $className - * @return bool - */ - public static function shouldSkipTest(string $testCaseName, ?string $className = null): bool - { - if (!in_array($testCaseName, TestHelper::$testSkipCasesList)) { - return in_array($className, TestHelper::$testSkipCasesList); - } - return true; - } - - /** - * Array that defines test cases which are shippable for now - * @var array|string[] - */ - public static array $testSkipCasesList = [ - // Definition of Test Class which has not been tested - 'DefaultApi', - 'AplusContentApi', // Doesn't support sandbox as of now. - 'AppIntegrationsApi', // No role for my account yet - 'ApplicationsApi', // Doesn't support sandbox as of now. - 'AwdApi', // No role for my account yet - 'CustomerInvoicesApi', - 'InvoicesApi', // No role for my account yet - 'ServiceApi', // No role for my account yet - 'ShipmentInvoiceApi', // No role for my account yet - 'ShippingApi', // No role for my account yet - 'UploadsApi', // Doesn't support sandbox as of now. - // There is critical bug in swagger and API class can not be compiled. need revisit testing. - 'VendorOrdersApi', - 'VendorShipmentsApi', - - // Definition of individual case which is unable to test - // Order API - // Missing required parameter in Request regulatedOrderVerificationStatus and can not be auto filled - //Because there is no difference between 200 case. - 'testUpdateVerificationStatus400', - 'testGetOrderRegulatedInfo200', // Getting 403 due to restricted role required - 'testGetOrderRegulatedInfo400', // Getting 403 due to restricted role required - // NotificationAPI - 'testDeleteSubscriptionById200', // Getting 400 with InvalidInput error - 'testGetSubscriptionById200', // Getting 400 with InvalidInput error - // Feed API - 'testCancelFeed200', // Always 500 will be returned - 'testCreateFeed400', // Request should have mandatory field FeedType - 'testCreateFeedDocument400', // Request should have mandatory field ContentType - // Report API - 'testCancelReport200', // Always 500 will be returned - 'testCancelReportSchedule200', // Always 500 will be returned - 'testCreateReportSchedule400', // Request should have mandatory field MarketplaceIds - // Pricing API - 'testGetCompetitiveSummary200', // Request offerType should be CONSUMER, not Consumer - // FBA Inbound Eligibility API - 'testGetItemEligibilityPreview401', // Always 500 will be returned - 'testGetItemEligibilityPreview503', // Always 500 will be returned - // fulfillmentInbound_2024-03-20 - 'testGenerateShipmentContentUpdatePreviews202', // Sandbox Returns 400 - 'testGenerateTransportationOptions202', // Sandbox Returns 400 - 'testGetInboundPlan200', // Json expected Response of timestamp is in wrong format. Millisecond should be added. - 'testGetShipment200', // Json expected Response of timestamp is in wrong format. Millisecond should be added. - // Json expected Response of timestamp is in wrong format. Millisecond should be added. - 'testScheduleSelfShipAppointment200', - 'testGetShipmentContentUpdatePreview200', // "expiration" at the Json expected Response Json is wrong - 'testListShipmentContentUpdatePreviews200', // "expiration" at the Json expected Response Json is wrong - 'testListInboundPlans200', //Json expected Response of timestamp is in wrong format.Millisecond should be added. - 'testListPrepDetails200', // Sandbox Returns 400 - 'testSetPackingInformation202', // Sandbox Returns 400 - 'testUpdateItemComplianceDetails202', // Sandbox Returns 400 - 'testSetPrepDetails202', // Sandbox Returns 400 - // CatalogItem - 'testGetCatalogItem200', // Response has Invalid value for images.variant such as PT09-PT14, EEGL and EGUS - 'testSearchCatalogItems200', // Response has Invalid value for images.variant such as PT09-PT14, EEGL and EGUS - // ProductFeesAPI - 'testGetMyFeesEstimates200', // Sandbox Returns 400 - 'testGetMyFeesEstimateForASIN400', // Request can not be made because Request is missing mandatory parameters - 'testGetMyFeesEstimateForSKU400', // Request can not be made because Request is missing mandatory parameters - // ListingsItems_2021-08-01 - // Expected response is different from actual response. - // TimeStamp Format is inconsistent within same Expected response Json - 'testGetListingsItem200', - // fbaInventory - 'testAddInventory200', // Create inventory is dependency for this operation - // listingsRestrictions - 'testGetListingsRestrictions400', // Error response can not be handled due to wrong ErrorList definition in Json - // merchantFulfillment - 'testCancelShipment200', // Label.FileContents.FileType is Enum, but “” is returned. - //Response ShippingServiceList.ShippingServiceOptions.LabelFormat should be Enum value or removed - 'testGetEligibleShipmentServices200', - // fulfillmentOutbound - 'testListReturnReasonCodes200', // Due to complexity, skip for now - 'testCancelFulfillmentOrder200', // Due to test execution order, it can not be passed - 'testCreateFulfillmentReturn200', // Due to test execution order, it can not be passed - 'testUpdateFulfillmentOrder200', // Due to test execution order, it can not be passed - 'testGetPackageTrackingDetails200', // Due to test execution order, it can not be passed - 'testSubmitFulfillmentOrderStatusUpdate200', // Due to test execution order, it can not be passed - 'testDeliveryOffers200', // Due to complexity, skip for now - // EasyShip - 'testCreateScheduledPackage400', // Skip due to mandatory filed "slotId" is null in the sample - 'testCreateScheduledPackageBulk200', // packageDimensions.unit must be Cm (Maybe in JP?) - 'testGetScheduledPackage200', // The expected and actual response is not a member of PackageStatus ENUM - 'testListHandoverSlots400', // Due to Json structure is incorrect, need to be fixed - 'testUpdateScheduledPackages400', // Due to Json structure is incorrect, need to be fixed - // DataKiosk - 'testCancelQuery204', // Sandbox Returns 400 - 'testCancelQuery404', // Sandbox Returns 400 - 'testGetQuery200', // Sandbox Returns 400 - 'testGetQuery404', // Sandbox Returns 400 - // Messaging API - // SandBox request timestamp format (2004-12-13T21:39:45.618-08:00) doesn't match with PHP. - // It will require dedicated customization to make 3 digit millisecond and doesn't match with auto Generation - 'testCreateWarranty201', - // Replenishment Api sellingPartners - 'testGetSellingPartnerMetrics400', // Request timestamp millisecond is 2 digits and requires string mutation - // Replenishment Api offers - 'testListOfferMetrics400', // Request eligibility is not member of Enum - // Sellers - 'testGetAccount200', // Need some application which can be succeeded. Access is denied with my account. - 'testGetAccount400', // Need some application which can be succeeded. Access is denied with my account. - // SolicitationsApi - // Need some application which can be succeeded. Access is denied with my account. - 'testGetSolicitationActionsForOrder200', - // SupplySources - 'testGetSupplySource200', // SandBox expected response.ThroughputUnit should be “Order” not “ORDER”. - 'testUpdateSupplySource400', // Expected sandbox response is missing mandatory field "ThroughputUnit". - 'testGetSupplySources400', // Expecting 400 to be returned, but 200 returned - // Tokens API - 'testCreateRestrictedDataToken400', // SandBox request.method is not member of Enum - // vendorDirectFulfillmentPaymentsV1 - 'testSubmitInvoice202', // Expected transactionId response is different from actual. - 'testSubmitInvoice400', // SandBox dateTime Format is invalid for programing language - // vendorDirectFulfillmentShipping_2021-12-28 - // shipmentStatusUpdates is not required in swagger, but SandBox returns error - 'testSubmitShipmentStatusUpdates202', - // shipmentConfirmations is not required in swagger, but SandBox returns error - 'testSubmitShipmentConfirmations202', - 'testGetPackingSlips200', // only allows purchaseOrderNumber as ‘’, but this is not practical. - 'testGetShippingLabel200', // only allows purchaseOrderNumber as ‘’, but this is not practical. - // shippingLabelRequests is not required in swagger, but SandBox returns error - 'testSubmitShippingLabelRequest202', - 'testCreateShippingLabels200', // Access is denied as 403 - 'testGetShippingLabels200', // Getting 400 error and not sure how to call successfully - // vendorDirectFulfillmentTransactions_2021-12-28 - 'testGetTransactionStatus200', // Not sure which value to pass as a key - // FBA Inbound V0 - 'testConfirmPreorder200', // It will be deprecated - 'testConfirmPreorder400', // It will be deprecated - 'testCreateInboundShipmentPlan400', // It will be deprecated - 'testGetTransportDetails200', // It will be deprecated - // FulfillmentInboundApiTest.testGenerateSelfShipAppointmentSlots201 - // OrdersApiTest.testConfirmShipment204 - // ReportsApiTest.testCreateReport202 - // ReportsApiTest.testCreateReportSchedule201 - 'testGenerateSelfShipAppointmentSlots201', - 'testConfirmShipment204', - 'testCreateReport202', - 'testCreateReportSchedule201', - 'testListOffers400', - 'testGetPackingSlip200' - ]; -} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api_test.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api_test.mustache deleted file mode 100644 index 2efe8c05..00000000 --- a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api_test.mustache +++ /dev/null @@ -1,220 +0,0 @@ -partial_header}} -/** - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Please update the test case below to test the endpoint. - */ - -namespace {{invokerPackage}}\Test\Api; - -use {{invokerPackage}}\Configuration; -use {{invokerPackage}}\ApiException; -use PHPUnit\Framework\TestCase; -use {{invokerPackage}}\Api\{{classname}}; -use OpenAPI\Client\Test\TestHelper; -use SpApi\AuthAndAuth\LWAAuthorizationCredentials; -use {{invokerPackage}}\ObjectSerializer; -use Dotenv\Dotenv; - -$dotenv = Dotenv::createImmutable('../../../sdk'); -$dotenv->load(); - -/** - * {{classname}}Test Class Doc Comment - * - * @category Class - * @package {{invokerPackage}} - * @author OpenAPI Generator team - * @link https://openapi-generator.tech - */ -{{#operations}} -class {{classname}}Test extends TestCase -{ - private {{classname}} $apiInstance; - private TestHelper $testHelper; - - public function setUp(): void - { - $this->testHelper = new TestHelper(); - $credentialsConfig = [ - "clientId" => $_ENV['SP_API_CLIENT_ID'], - "clientSecret" => $_ENV['SP_API_CLIENT_SECRET'], - "refreshToken" => $_ENV['SP_API_REFRESH_TOKEN'], - "endpoint" => $_ENV['SP_API_ENDPOINT'] ?: "https://api.amazon.com/auth/o2/token" - ]; - $scopes = $this->testHelper->getScopesForApi($this->getName()); - if (!empty($scopes)) { - $credentialsConfig['scopes'] = $scopes; - } - $lwaAuthorizationCredentials = new LWAAuthorizationCredentials($credentialsConfig); - $config = new Configuration([], $lwaAuthorizationCredentials); - $config->setHost($_ENV['SP_API_ENDPOINT_HOST'] ?: 'https://sandbox.sellingpartnerapi-fe.amazon.com'); - $this->apiInstance = new {{classname}}($config, null, null); - } - - /** - * Handles the response based on the expected HTTP status code. - * - * @param mixed $response The API response. - * @param int $statusCode The actual HTTP status code. - * @param int $expectedStatusCode The expected HTTP status code. - * @throws \ReflectionException - */ - private function handleResponse(mixed $response, int $statusCode, int $expectedStatusCode, $responseParams): void - { - switch ($expectedStatusCode) { - case 200: - case 201: - case 202: - $actual = json_decode($response, true); - $this->assertEquals($responseParams, $actual); - break; - - case 204: - $this->assertTrue(true); - echo "Response is empty as expected for status code 204."; - break; - - case 400: - $this->assertArrayHasKey('errors', $responseParams); - $this->assertEquals($responseParams['errors'], $response->getErrors()); - break; - - default: - $this->fail("Unhandled response code: $expectedStatusCode"); - break; - } - } - - /** - * Handles exceptions thrown during the API call. - * - * @param ApiException $e The exception thrown by the API. - */ - private function handleApiException(ApiException $e, int $expectedCode): void - { - if ($e->getCode() == $expectedCode) { - $this->assertTrue(true); - } else { - $this->fail('Unexpected error code: ' . $e->getCode()); - } - } - - /** - * Asserts the HTTP status code. - * - * @param int $expectedStatusCode - * @param int $actualStatusCode - */ - private function assertHttpStatusCode(int $expectedStatusCode, int $actualStatusCode): void - { - $this->assertEquals( - $expectedStatusCode, - $actualStatusCode, - "Expected HTTP status code $expectedStatusCode but got $actualStatusCode." - ); - } - - {{#operation}} - {{#responses}} - /** - * Test case for {{{operationId}}}_{{code}} - */ - public function test{{operationIdCamelCase}}{{code}}() - { - {{^vendorExtensions.x-amzn-api-sandbox.static}} - {{^vendorExtensions}} - // Skip this test if no static sandbox extension is present - $this->markTestSkipped('Static sandbox is not defined for this operation.'); - {{/vendorExtensions}} - {{#vendorExtensions}} - {{^is2xx}} - // Skip this test - $this->markTestSkipped('Skip test for this operation.'); - {{/is2xx}} - {{#is2xx}} - // Dynamic sandbox case - try { - // Skip test if it is in the skip list - if ($this->testHelper->shouldSkipTest('test{{operationIdCamelCase}}{{code}}', '{{classname}}')) { - $this->assertTrue(true); - return; - } - // Skip entire class - if ($this->testHelper->shouldSkipTest('{{classname}}')) { - $this->assertTrue(true); - return; - } - $result = $this->testHelper->buildRequestForDynamicSandBox( - $this->apiInstance, - '{{operationId}}' - ); - $requestParams = $result; - - // Act: Call API - list($response, $statusCode, $headers) = - $this->apiInstance->{{operationId}}WithHttpInfo(...array_values($requestParams)); - - // Assert the response code - $this->assertHttpStatusCode({{code}}, $statusCode); - } catch (ApiException $e) { - $this->handleApiException($e, {{code}}); - } catch (\ReflectionException $e) { - $this->fail("Reflection exception: " . $e->getMessage()); - } - {{/is2xx}} - {{/vendorExtensions}} - {{/vendorExtensions.x-amzn-api-sandbox.static}} - {{#vendorExtensions.x-amzn-api-sandbox.static.0}} - try { - // Skip test if it is in the skip list - if ($this->testHelper->shouldSkipTest('test{{operationIdCamelCase}}{{code}}', '{{classname}}')) { - $this->assertTrue(true); - return; - } - $jsonSchema = '{{jsonSchema}}'; - $result = $this->testHelper->extractRequestAndResponse( - $this->apiInstance, - $jsonSchema, - '{{operationId}}' - ); - $requestParams = $result['requestParams']; - $expectedResponse = $result['expectedResponse']; - - // Change Time Format if it requires - $specificTimeFormat = $this->testHelper->getDateTimeFormatForCase('{{classname}}'); - if ($specificTimeFormat) { - ObjectSerializer::setDateTimeFormat($specificTimeFormat); - } - - // Act: Call API - list($response, $statusCode, $headers) = - $this->apiInstance->{{operationId}}WithHttpInfo(...array_values($requestParams)); - - // Assert the response code - $this->assertHttpStatusCode({{code}}, $statusCode); - - // Handle different response codes - $this->handleResponse($response, $statusCode, {{code}}, $expectedResponse); - } catch (ApiException $e) { - $this->handleApiException($e, {{code}}); - } catch (\ReflectionException $e) { - $this->fail("Reflection exception: " . $e->getMessage()); - } - {{/vendorExtensions.x-amzn-api-sandbox.static.0}} - } - {{/responses}} - {{/operation}} -} -{{/operations}} diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_test.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_test.mustache deleted file mode 100644 index 5316c94d..00000000 --- a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/model_test.mustache +++ /dev/null @@ -1,98 +0,0 @@ -model = new {{classname}}(); - } - - /** - * Clean up after running each test case - */ - public function tearDown(): void - { - unset($this->model); - } - - /** - * Test "{{classname}}" - */ - public function test{{classname}}() - { - $this->assertInstanceOf({{classname}}::class, $this->model); - } -{{#vars}} - - /** - * Test attribute "{{name}}" - */ - public function testProperty{{nameInPascalCase}}() - { - {{#isEnum}} - $enumInstance = new {{classname}}(); - $allowedValues = $enumInstance->{{getter}}AllowableValues(); - $testValue = reset($allowedValues); - {{/isEnum}} - {{^isEnum}} - {{#isEnumRef}} - $enumInstance = new {{dataType}}(); - $allowedValues = $enumInstance->getAllowableEnumValues(); - $testValue = reset($allowedValues); - {{/isEnumRef}} - {{#isString}} - $testValue = 'test'; - {{/isString}} - {{#isInteger}} - $testValue = 123; - {{/isInteger}} - {{#isNumber}} - $testValue = 1; - {{/isNumber}} - {{#isBoolean}} - $testValue = true; - {{/isBoolean}} - {{#isArray}} - $testValue = []; - {{/isArray}} - {{^isEnumRef}}{{^isString}}{{^isInteger}}{{^isNumber}}{{^isBoolean}}{{^isArray}} - $testValue = new {{{dataType}}}(); - {{/isArray}}{{/isBoolean}}{{/isNumber}}{{/isInteger}}{{/isString}}{{/isEnumRef}} - {{/isEnum}} - $this->model->set{{nameInPascalCase}}($testValue); - $this->assertEquals($testValue, $this->model->get{{nameInPascalCase}}()); - } -{{/vars}} -} -{{/model}} -{{/models}} From 55147ebd305a7001e5c02a554516001d2ee2e38d Mon Sep 17 00:00:00 2001 From: nakagleo Date: Thu, 30 Jan 2025 13:45:23 -0300 Subject: [PATCH 3/3] Updates on PHP sdk --- clients/sellingpartner-api-aa-php/README.md | 38 +++++++------------ .../openapi-generator/templates/api.mustache | 2 +- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/clients/sellingpartner-api-aa-php/README.md b/clients/sellingpartner-api-aa-php/README.md index cf977234..57ada0b8 100644 --- a/clients/sellingpartner-api-aa-php/README.md +++ b/clients/sellingpartner-api-aa-php/README.md @@ -8,36 +8,25 @@ Obtains and signs a request with an access token from LWA (Login with Amazon) fo *Example* ``` -$request = new \GuzzleHttp\Psr7\Request( - "method", "uri", ... -); - -// Seller APIs - $lwaAuthorizationCredentials = new LWAAuthorizationCredentials([ - "clientId" => "...", - "clientSecret" => "...", - "refreshToken" => "...", - "endpoint" => "..." +"clientId" => '.....', +"clientSecret" => '.....', +"refreshToken" => '.....', +"endpoint" => 'https://api.amazon.com/auth/o2/token' ]); -/* Sellerless APIs -The Selling Partner API scopes can be retrieved from the ScopeConstants class and passed as -argument(s) to either the "scopes" => "..." or setScopes(...) method during -lwaAuthorizationCredentials object instantiation. -*/ +// Initialize LWAAuthorizationSigner instance +$lwaAuthorizationSigner = new LWAAuthorizationSigner($lwaAuthorizationCredentials); +$config = new Configuration([], $lwaAuthorizationCredentials); -use SpApi\AuthAndAuth\ScopeConstants; +// Setting SP-API endpoint region. Change it according to the desired region +$config->setHost('https://sellingpartnerapi-na.amazon.com'); -$lwaAuthorizationCredentials = new LWAAuthorizationCredentials([ - "clientId" => "...", - "clientSecret" => "...", - "scopes" => "...", - "endpoint" => "..." -]); +// Create a new HTTP client +$client = new GuzzleHttp\Client(); -$signedRequest = (new LWAAuthorizationSigner($lwaAuthorizationCredentials)) - ->sign($request); +// Create an instance of the Orders Api client +$api = new OrdersApi($config, null, $client); ``` ## LWAAccessTokenCache @@ -55,6 +44,7 @@ $rateLimitOption = new RateLimitConfigurationOnRequests([ ]); ``` + ## Resources This package features Mustache templates designed for use with [openapi generator](https://openapi-generator.tech/). When you build Selling Partner API OpenAPI models with these templates, they help generate a rich SDK with functionality to invoke Selling Partner APIs built in. The templates are located in *resources/openapi-generator*. diff --git a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api.mustache b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api.mustache index 5c254a58..77a92999 100644 --- a/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api.mustache +++ b/clients/sellingpartner-api-aa-php/resources/openapi-generator/templates/api.mustache @@ -235,7 +235,7 @@ use {{invokerPackage}}\ObjectSerializer; $response = $this->client->send($request, $options); } catch (RequestException $e) { throw new ApiException( - "[{$e->getCode()}] {$e->getMessage()}", + "[{$e->getCode()}] {$e->getResponse()->getBody()}", (int) $e->getCode(), $e->getResponse() ? $e->getResponse()->getHeaders() : null, $e->getResponse() ? (string) $e->getResponse()->getBody() : null