diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7060b6b..befe91e 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: operating-system: [ ubuntu-22.04 ] - php-versions: ['7.4', '8.3'] + php-versions: ['7.4', '8.4'] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index ede7da4..f00e6c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # PHP specific files # ########################## /vendor/* -/test.php # OS generated files # ###################### diff --git a/README.md b/README.md index e1344ea..4eb0dc3 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,14 @@ $sdk = \CarApiSdk\CarApi::build([ ]); ``` -You have now created an instance of the SDK. +You have now created an instance of the SDK. For Powersports use the following: + +```php +$sdk = \CarApiSdk\Powersports::build([ + 'token' => getenv('CARAPI_TOKEN'), + 'secret' => getenv('CARAPI_SECRET'), +]); +``` ### Other Options diff --git a/composer.json b/composer.json index 4166999..25558ec 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ }, "require-dev": { "ext-zlib": "*", + "ext-curl": "*", "josegonzalez/dotenv": "^4.0", "phpunit/phpunit": "^9.0", "php-http/mock-client": "^1.6", diff --git a/src/BaseApi.php b/src/BaseApi.php new file mode 100644 index 0000000..c95b1ae --- /dev/null +++ b/src/BaseApi.php @@ -0,0 +1,249 @@ +config = $config; + $this->client = $client ?? new Psr18Client(); + $this->host = ($config->host ?? 'https://carapi.app') . '/api'; + $this->streamFactory = Psr17FactoryDiscovery::findStreamFactory(); + $this->uriFactory = Psr17FactoryDiscovery::findUriFactory(); + } + + /** + * Returns a JWT. + * + * @return string + * @throws CarApiException + */ + public function authenticate(): string + { + try { + $json = json_encode(AuthDto::build($this->config), JSON_THROW_ON_ERROR); + if ($json === false) { + throw new \JsonException('JSON Payload is false'); + } + } catch (\JsonException $e) { + throw new CarApiException('Unable to build JSON payload', 500, $e); + } + + $stream = $this->streamFactory->createStream($json); + + $request = $this->client->createRequest('POST', sprintf('%s/auth/login', $this->host)) + ->withProtocolVersion($this->config->httpVersion) + ->withHeader('accept', 'text/plain') + ->withHeader('content-type', 'application/json') + ->withHeader('content-length', (string) $stream->getSize()) + ->withBody($stream); + + $response = $this->sendRequest($request); + $body = (string) $response->getBody(); + if ($response->getStatusCode() !== 200) { + throw new CarApiException( + sprintf( + 'HTTP %s - CarAPI authentication failed: %s', + $response->getStatusCode(), + $body + ) + ); + } + + $encoding = array_map(fn (string $str) => strtolower($str), $response->getHeader('Content-Encoding')); + if (in_array('gzip', $encoding) + && in_array('gzip', $this->config->encoding) + && \extension_loaded('zlib') + ) { + $body = gzdecode(base64_decode($body)); + if ($body === false) { + throw new CarApiException('Unable to decompress response. Maybe try without gzip.'); + } + } + + $pieces = explode('.', $body); + if (count($pieces) !== 3) { + throw new CarApiException('Invalid JWT'); + } + + return $this->jwt = $body; + } + + /** + * Returns a boolean indicating if the JWT has expired. If a null response is returned it means no JWT is set. + * + * @param int $buffer A buffer in seconds. This will check if the JWT is expired or will expire within $buffer + * seconds. + * + * @return bool|null + * @throws CarApiException + */ + public function isJwtExpired(int $buffer = 60): ?bool + { + if (empty($this->jwt)) { + return null; + } + + $pieces = explode('.', $this->jwt); + if (count($pieces) !== 3) { + throw new CarApiException('JWT is invalid'); + } + + $payload = base64_decode($pieces[1]); + try { + $data = json_decode($payload, false, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new CarApiException('Error decoding JWT', $e->getCode(), $e); + } + + return (new \DateTime('now', new \DateTimeZone('America/New_York')))->getTimestamp() > $data->exp + $buffer; + } + + /** + * Loads a JWT. + * + * @param string $jwt The JWT to be loaded + * + * @return $this + */ + public function loadJwt(string $jwt): self + { + $this->jwt = $jwt; + + return $this; + } + + /** + * Get the JWT + * + * @return string|null + */ + public function getJwt(): ?string + { + if (empty($this->jwt)) { + return null; + } + + return $this->jwt; + } + + /** + * HTTP GET and decode the response. + * + * @param string $url The endpoint + * @param array $options Options to be passed to the endpoint + * @param bool|null $associative Whether decoding should be associative or not + * + * @return mixed + * @throws CarApiException + */ + protected function getDecoded(string $url, array $options = [], ?bool $associative = null) + { + $response = $this->get($url, $options); + $body = (string) $response->getBody(); + + $encoding = array_map(fn (string $str) => strtolower($str), $response->getHeader('Content-Encoding')); + if (in_array('gzip', $encoding) + && in_array('gzip', $this->config->encoding) + && \extension_loaded('zlib') + ) { + $body = gzdecode(base64_decode($body)); + if ($body === false) { + throw new CarApiException('Unable to decompress response. Maybe try without gzip.'); + } + } + + try { + $decoded = json_decode($body, $associative, 512, JSON_THROW_ON_ERROR); + if ($response->getStatusCode() !== 200) { + $decoded = (object) $decoded; + $exception = $decoded->exception ?? 'Unknown Error'; + $message = $decoded->message ?? 'Unknown Message'; + $url = $decoded->url ?? 'Unknown URL'; + throw new CarApiException( + "$exception: $message while requesting $url", + $response->getStatusCode() + ); + } + + return $decoded; + } catch (\JsonException $e) { + throw new CarApiException('Error decoding response', $e->getCode(), $e); + } + } + + /** + * HTTP GET request + * + * @param string $url The endpoint being requested + * @param array $options Options to be passed to the endpoint + * + * @return ResponseInterface + * @throws CarApiException + */ + protected function get(string $url, array $options): ResponseInterface + { + $query = array_map( + function ($param) { + if ($param instanceof \JsonSerializable) { + return json_encode($param); + } + return $param; + }, $options['query'] ?? [] + ); + + $uri = $this->uriFactory->createUri($this->host . $url)->withQuery(http_build_query($query)); + + $request = $this->client->createRequest('GET', $uri) + ->withHeader('accept', 'application/json'); + + if (!empty($this->jwt)) { + $request = $request->withHeader('Authorization', sprintf('Bearer %s', $this->jwt)); + } + + return $this->sendRequest($request); + } + + /** + * Sends the request + * + * @param RequestInterface $request RequestInterface instance + * + * @return ResponseInterface + * @throws CarApiException + */ + protected function sendRequest(RequestInterface $request): ResponseInterface + { + if (in_array('gzip', $this->config->encoding) && \extension_loaded('zlib')) { + $request = $request->withHeader('accept-encoding', 'gzip'); + } + + try { + return $this->client->sendRequest($request->withProtocolVersion($this->config->httpVersion)); + } catch (ClientExceptionInterface $e) { + throw new CarApiException($e->getMessage(), $e->getCode(), $e); + } + } +} \ No newline at end of file diff --git a/src/CarApi.php b/src/CarApi.php index 7029c48..17b8ba7 100644 --- a/src/CarApi.php +++ b/src/CarApi.php @@ -3,38 +3,10 @@ namespace CarApiSdk; -use Http\Discovery\Psr17FactoryDiscovery; -use Http\Discovery\Psr18Client; -use Psr\Http\Client\ClientExceptionInterface; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamFactoryInterface; -use Psr\Http\Message\UriFactoryInterface; -class CarApi +class CarApi extends BaseApi { - private string $host; - private CarApiConfig $config; - private Psr18Client $client; - private StreamFactoryInterface $streamFactory; - private UriFactoryInterface $uriFactory; - private string $jwt; - - /** - * Construct - * - * @param CarApiConfig $config An instance of CarApiConfig - * @param Psr18Client|null $client If left null an instance will be created automatically - */ - public function __construct(CarApiConfig $config, Psr18Client $client = null) - { - $this->config = $config; - $this->client = $client ?? new Psr18Client(); - $this->host = ($config->host ?? 'https://carapi.app') . '/api'; - $this->streamFactory = Psr17FactoryDiscovery::findStreamFactory(); - $this->uriFactory = Psr17FactoryDiscovery::findUriFactory(); - } - /** * Builds the SDK. Look at CarApiConfig for all options possible. * @@ -47,107 +19,6 @@ public static function build(array $options): self return new self(CarApiConfig::build($options)); } - /** - * Returns a JWT. - * - * @return string - * @throws CarApiException - */ - public function authenticate(): string - { - try { - $json = json_encode(AuthDto::build($this->config), JSON_THROW_ON_ERROR); - if ($json === false) { - throw new \JsonException('JSON Payload is false'); - } - } catch (\JsonException $e) { - throw new CarApiException('Unable to build JSON payload', 500, $e); - } - - $stream = $this->streamFactory->createStream($json); - - $request = $this->client->createRequest('POST', sprintf('%s/auth/login', $this->host)) - ->withProtocolVersion($this->config->httpVersion) - ->withHeader('accept', 'text/plain') - ->withHeader('content-type', 'application/json') - ->withHeader('content-length', (string) $stream->getSize()) - ->withBody($stream); - - $response = $this->sendRequest($request); - $body = (string) $response->getBody(); - if ($response->getStatusCode() !== 200) { - throw new CarApiException( - sprintf( - 'HTTP %s - CarAPI authentication failed: %s', - $response->getStatusCode(), - $body - ) - ); - } - - $encoding = array_map(fn (string $str) => strtolower($str), $response->getHeader('Content-Encoding')); - if (in_array('gzip', $encoding) - && in_array('gzip', $this->config->encoding) - && \extension_loaded('zlib') - ) { - $body = gzdecode(base64_decode($body)); - if ($body === false) { - throw new CarApiException('Unable to decompress response. Maybe try without gzip.'); - } - } - - $pieces = explode('.', $body); - if (count($pieces) !== 3) { - throw new CarApiException('Invalid JWT'); - } - - return $this->jwt = $body; - } - - /** - * Returns a boolean indicating if the JWT has expired. If a null response is returned it means no JWT is set. - * - * @param int $buffer A buffer in seconds. This will check if the JWT is expired or will expire within $buffer - * seconds. - * - * @return bool|null - * @throws CarApiException - */ - public function isJwtExpired(int $buffer = 60): ?bool - { - if (empty($this->jwt)) { - return null; - } - - $pieces = explode('.', $this->jwt); - if (count($pieces) !== 3) { - throw new CarApiException('JWT is invalid'); - } - - $payload = base64_decode($pieces[1]); - try { - $data = json_decode($payload, false, 512, JSON_THROW_ON_ERROR); - } catch (\JsonException $e) { - throw new CarApiException('Error decoding JWT', $e->getCode(), $e); - } - - return (new \DateTime('now', new \DateTimeZone('America/New_York')))->getTimestamp() > $data->exp + $buffer; - } - - /** - * Loads a JWT. - * - * @param string $jwt The JWT to be loaded - * - * @return $this - */ - public function loadJwt(string $jwt): self - { - $this->jwt = $jwt; - - return $this; - } - /** * Return vehicle years. * @@ -241,11 +112,11 @@ public function licensePlate(string $countryCode, string $lookup, ?string $regio { return $this->getDecoded( '/license-plate', [ - 'query' => [ - 'country_code' => $countryCode, - 'lookup' => $lookup, - 'region' => $region, - ] + 'query' => [ + 'country_code' => $countryCode, + 'lookup' => $lookup, + 'region' => $region, + ] ] ); } @@ -406,116 +277,4 @@ public function csvDataFeedLastUpdated(): \StdClass { return $this->getDecoded('/data-feeds/last-updated'); } - - /** - * Get the JWT - * - * @return string|null - */ - public function getJwt(): ?string - { - if (empty($this->jwt)) { - return null; - } - - return $this->jwt; - } - - /** - * HTTP GET and decode the response. - * - * @param string $url The endpoint - * @param array $options Options to be passed to the endpoint - * @param bool|null $associative Whether decoding should be associative or not - * - * @return mixed - * @throws CarApiException - */ - private function getDecoded(string $url, array $options = [], ?bool $associative = null) - { - $response = $this->get($url, $options); - $body = (string) $response->getBody(); - - $encoding = array_map(fn (string $str) => strtolower($str), $response->getHeader('Content-Encoding')); - if (in_array('gzip', $encoding) - && in_array('gzip', $this->config->encoding) - && \extension_loaded('zlib') - ) { - $body = gzdecode(base64_decode($body)); - if ($body === false) { - throw new CarApiException('Unable to decompress response. Maybe try without gzip.'); - } - } - - try { - $decoded = json_decode($body, $associative, 512, JSON_THROW_ON_ERROR); - if ($response->getStatusCode() !== 200) { - $decoded = (object) $decoded; - $exception = $decoded->exception ?? 'Unknown Error'; - $message = $decoded->message ?? 'Unknown Message'; - $url = $decoded->url ?? 'Unknown URL'; - throw new CarApiException( - "$exception: $message while requesting $url", - $response->getStatusCode() - ); - } - - return $decoded; - } catch (\JsonException $e) { - throw new CarApiException('Error decoding response', $e->getCode(), $e); - } - } - - /** - * HTTP GET request - * - * @param string $url The endpoint being requested - * @param array $options Options to be passed to the endpoint - * - * @return ResponseInterface - * @throws CarApiException - */ - private function get(string $url, array $options): ResponseInterface - { - $query = array_map( - function ($param) { - if ($param instanceof \JsonSerializable) { - return json_encode($param); - } - return $param; - }, $options['query'] ?? [] - ); - - $uri = $this->uriFactory->createUri($this->host . $url)->withQuery(http_build_query($query)); - - $request = $this->client->createRequest('GET', $uri) - ->withHeader('accept', 'application/json'); - - if (!empty($this->jwt)) { - $request = $request->withHeader('Authorization', sprintf('Bearer %s', $this->jwt)); - } - - return $this->sendRequest($request); - } - - /** - * Sends the request - * - * @param RequestInterface $request RequestInterface instance - * - * @return ResponseInterface - * @throws CarApiException - */ - private function sendRequest(RequestInterface $request): ResponseInterface - { - if (in_array('gzip', $this->config->encoding) && \extension_loaded('zlib')) { - $request = $request->withHeader('accept-encoding', 'gzip'); - } - - try { - return $this->client->sendRequest($request->withProtocolVersion($this->config->httpVersion)); - } catch (ClientExceptionInterface $e) { - throw new CarApiException($e->getMessage(), $e->getCode(), $e); - } - } } diff --git a/src/Powersports.php b/src/Powersports.php new file mode 100644 index 0000000..42b5584 --- /dev/null +++ b/src/Powersports.php @@ -0,0 +1,58 @@ +getDecoded('/years/powersports', $options, true); + } + + /** + * Return powersports makes. + * + * @param array $options An array of options to pass into the request. + * + * @return \stdClass + * @throws CarApiException + */ + public function makes(array $options = []): \stdClass + { + return $this->getDecoded('/makes/powersports', $options); + } + + /** + * Return powersports models + * + * @param array $options An array of options to pass into the request. + * + * @return \stdClass + * @throws CarApiException + */ + public function models(array $options = []): \stdClass + { + return $this->getDecoded('/models/powersports', $options); + } +} diff --git a/test.php b/test.php new file mode 100644 index 0000000..cf520fe --- /dev/null +++ b/test.php @@ -0,0 +1,114 @@ +parse() + ->toEnv() + ->toArray(); + +if (!isset($env['TOKEN'], $env['SECRET'], $env['HOST'])) { + throw new LogicException('An .env file is required and must contain a TOKEN, SECRET and HOST.'); +} + +function println(string $string) { + echo "\n\n$string\n\n"; +} + +$sdk = CarApi::build([ + 'token' => $env['TOKEN'], + 'secret' => $env['SECRET'], + 'host' => $env['HOST'], + 'httpVersion' => '1.1', + 'encoding' => ['gzip'], +]); + +println('JWT:' . $sdk->authenticate()); + +println('Years:'); +print_r($sdk->years(['query' => ['make' => 'Tesla']])); + +println('Makes:'); +print_r($sdk->makes(['query' => ['limit' => 1, 'page' => 0]])); + +println('Models:'); +print_r($sdk->models(['query' => ['make' => 'Tesla', 'limit' => 1]])); + +println('Trims:'); +$json = new JsonSearch(); +$json->addItem(new JsonSearchItem('make', 'like', 'Tesla')); +print_r($sdk->trims(['query' => ['json' => $json, 'limit' => 1]])); + +println('Trims:'); +print_r($sdk->trimItem(1)); + +println('Bodies:'); +print_r($sdk->bodies(['query' => ['make' => 'Tesla', 'limit' => 1]])); + +println('Engines:'); +print_r($sdk->engines(['query' => ['make' => 'Tesla', 'limit' => 1]])); + +println('Mileages:'); +print_r($sdk->mileages(['query' => ['make' => 'Tesla', 'limit' => 1]])); + +println('VIN:'); +print_r($sdk->vin('1GTG6CEN0L1139305')); + +println('Interior Colors:'); +print_r($sdk->interiorColors(['query' => ['make' => 'Tesla', 'limit' => 1]])); + +println('Exterior Colors:'); +print_r($sdk->exteriorColors(['query' => ['make' => 'Tesla', 'limit' => 1]])); + +println('Vehicle Attributes:'); +print_r($sdk->vehicleAttributes('bodies.type')); + +println('Account Requests:'); +print_r($sdk->accountRequests()); + +/*println('Account Requests Today:'); +print_r($sdk->accountRequestsToday());*/ + +println('License Plate:'); +print_r($sdk->licensePlate('US', 'LNP8460#TEST', 'NY')); + +println('OBD Codes:'); +print_r($sdk->obdCodes(['query' => ['limit' => 1]])); + +println('Single OBD Code:'); +print_r($sdk->obdCodeItem('B1200')); + +println('Done with Vehicles!'); + +$sdk = Powersports::build([ + 'token' => $env['TOKEN'], + 'secret' => $env['SECRET'], + 'host' => 'http://localhost:8080', + 'httpVersion' => '1.1', + 'encoding' => ['gzip'], +]); + +println('JWT:' . $sdk->authenticate()); + +println('Years:'); +print_r($sdk->years(['query' => ['make' => 'Honda', 'type' => 'street_motorcycle']])); + +println('Makes:'); +print_r($sdk->makes(['query' => ['limit' => 1, 'page' => 0, 'type' => 'street_motorcycle']])); + +println('Models:'); +print_r($sdk->models(['query' => ['make' => 'Honda', 'limit' => 1, 'type' => 'street_motorcycle']])); + +println('Done with Powersports!'); \ No newline at end of file diff --git a/tests/BaseApiTest.php b/tests/BaseApiTest.php new file mode 100644 index 0000000..66ae4e6 --- /dev/null +++ b/tests/BaseApiTest.php @@ -0,0 +1,129 @@ +expectException(CarApiException::class); + CarApiConfig::build($options); + } + + public static function dataProviderForBuildOptions(): array + { + return [ + [[]], + [['token' => '123']], + [['secret' => '123']], + ]; + } + + public function test_authenticate(): void + { + $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); + $client = $this->createMockClient(200, '1.2.3'); + $sdk = new BaseApi($config, $client); + $jwt = $sdk->authenticate(); + $this->assertNotEmpty($jwt); + } + + public function test_authenticate_with_gzip(): void + { + $config = CarApiConfig::build(['token' => '1', 'secret' => '1', 'encoding' => ['gzip']]); + $body = base64_encode(gzencode('1.2.3')); + $client = $this->createMockClient(200, $body, ['Content-Encoding' => 'gzip']); + $sdk = new BaseApi($config, $client); + $jwt = $sdk->authenticate(); + $this->assertNotEmpty($jwt); + } + + public function test_authenticate_fails(): void + { + $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); + $client = $this->createMockClient(401, 'auth failed message'); + $sdk = new BaseApi($config, $client); + + $this->expectException(CarApiException::class); + $this->expectExceptionMessage('auth failed message'); + $sdk->authenticate(); + } + + public function test_authenticate_returns_bad_jwt(): void + { + $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); + $client = $this->createMockClient(200, '1.2'); + $sdk = new BaseApi($config, $client); + + $this->expectException(CarApiException::class); + $this->expectExceptionMessage('Invalid JWT'); + $sdk->authenticate(); + } + + /** + * @dataProvider dataProviderForJwt + */ + public function test_loaded_jwt_not_expired(?string $payload, ?bool $result): void + { + $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); + $sdk = new BaseApi($config); + + if ($payload) { + $jwt = sprintf( + '%s.%s.%s', + base64_encode('{"typ": "JWT", "alg": "HS256"}'), + $payload, + '123' + ); + $this->assertEquals($result, $sdk->loadJwt($jwt)->isJwtExpired()); + } else { + $this->assertNull($sdk->isJwtExpired()); + } + } + + public static function dataProviderForJwt(): array + { + $timestamp = (new \DateTime('now', new \DateTimeZone('America/New_York')))->getTimestamp(); + + return [ + [base64_encode(sprintf('{"exp": %s}', $timestamp + 86400)), false], + [base64_encode(sprintf('{"exp": %s}', $timestamp - 86400)), true], + [null, null], + ]; + } + + /** + * @dataProvider dataProviderForBadJwt + */ + public function test_loaded_jwt_is_malformed(string $jwt, string $error): void + { + $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); + $sdk = new BaseApi($config); + $this->expectException(CarApiException::class); + $this->expectExceptionMessage($error); + $sdk->loadJwt($jwt)->isJwtExpired(); + } + + public static function dataProviderForBadJwt(): array + { + return [ + ['bad jwt', 'JWT is invalid'], + ['1..3', 'Error decoding JWT'], + ]; + } +} \ No newline at end of file diff --git a/tests/CarApiTest.php b/tests/CarApiTest.php index 03f1aba..8c5a060 100644 --- a/tests/CarApiTest.php +++ b/tests/CarApiTest.php @@ -1,77 +1,18 @@ expectException(CarApiException::class); - CarApiConfig::build($options); - } - - public static function dataProviderForBuildOptions(): array - { - return [ - [[]], - [['token' => '123']], - [['secret' => '123']], - ]; - } - - public function test_authenticate(): void - { - $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); - $client = $this->createMockClient(200, '1.2.3'); - $sdk = new CarApi($config, $client); - $jwt = $sdk->authenticate(); - $this->assertNotEmpty($jwt); - } - - public function test_authenticate_with_gzip(): void - { - $config = CarApiConfig::build(['token' => '1', 'secret' => '1', 'encoding' => ['gzip']]); - $body = base64_encode(gzencode('1.2.3')); - $client = $this->createMockClient(200, $body, ['Content-Encoding' => 'gzip']); - $sdk = new CarApi($config, $client); - $jwt = $sdk->authenticate(); - $this->assertNotEmpty($jwt); - } - - public function test_authenticate_fails(): void - { - $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); - $client = $this->createMockClient(401, 'auth failed message'); - $sdk = new CarApi($config, $client); - - $this->expectException(CarApiException::class); - $this->expectExceptionMessage('auth failed message'); - $sdk->authenticate(); - } - - public function test_authenticate_returns_bad_jwt(): void - { - $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); - $client = $this->createMockClient(200, '1.2'); - $sdk = new CarApi($config, $client); - - $this->expectException(CarApiException::class); - $this->expectExceptionMessage('Invalid JWT'); - $sdk->authenticate(); - } + use TestHelperTrait; /** * @dataProvider dataProviderForMethods @@ -207,50 +148,6 @@ public function test_query_params(): void $this->assertNotEmpty($arr); } - /** - * @dataProvider dataProviderForJwt - */ - public function test_loaded_jwt_not_expired(?string $payload, ?bool $result): void - { - $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); - $sdk = new CarApi($config); - - if ($payload) { - $jwt = sprintf( - '%s.%s.%s', - base64_encode('{"typ": "JWT", "alg": "HS256"}'), - $payload, - '123' - ); - $this->assertEquals($result, $sdk->loadJwt($jwt)->isJwtExpired()); - } else { - $this->assertNull($sdk->isJwtExpired()); - } - } - - public static function dataProviderForJwt(): array - { - $timestamp = (new \DateTime('now', new \DateTimeZone('America/New_York')))->getTimestamp(); - - return [ - [base64_encode(sprintf('{"exp": %s}', $timestamp + 86400)), false], - [base64_encode(sprintf('{"exp": %s}', $timestamp - 86400)), true], - [null, null], - ]; - } - - /** - * @dataProvider dataProviderForBadJwt - */ - public function test_loaded_jwt_is_malformed(string $jwt, string $error): void - { - $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); - $sdk = new CarApi($config); - $this->expectException(CarApiException::class); - $this->expectExceptionMessage($error); - $sdk->loadJwt($jwt)->isJwtExpired(); - } - public function test_gzip_encoding(): void { $config = CarApiConfig::build(['token' => '1', 'secret' => '1', 'encoding' => ['gzip']]); @@ -261,35 +158,4 @@ public function test_gzip_encoding(): void $arr = $sdk->years(); $this->assertNotEmpty($arr); } - - public static function dataProviderForBadJwt(): array - { - return [ - ['bad jwt', 'JWT is invalid'], - ['1..3', 'Error decoding JWT'], - ]; - } - - /** - * @param int $statusCode - * @param string $responseBody - * @return MockObject&Psr18Client - * @throws \PHPUnit\Framework\MockObject\Exception - */ - private function createMockClient(int $statusCode, string $responseBody, array $headers = []): MockObject - { - $responseMock = $this->createPartialMock(Response::class, [ - 'getStatusCode', - 'getBody', - 'getHeader', - ]); - $stream = Psr17FactoryDiscovery::findStreamFactory()->createStream($responseBody); - $responseMock->method('getStatusCode')->willReturn($statusCode); - $responseMock->method('getBody')->willReturn($stream); - $responseMock->method('getHeader')->willReturn($headers); - $clientMock = $this->createPartialMock(Psr18Client::class, ['sendRequest']); - $clientMock->method('sendRequest')->willReturn($responseMock); - - return $clientMock; - } } \ No newline at end of file diff --git a/tests/PowersportsTest.php b/tests/PowersportsTest.php new file mode 100644 index 0000000..bc49e28 --- /dev/null +++ b/tests/PowersportsTest.php @@ -0,0 +1,102 @@ + '1', 'secret' => '1']); + $client = $this->createMockClient(200, '{"data": []}'); + $sdk = new Powersports($config, $client); + $obj = $sdk->{$method}(); + $this->assertObjectHasProperty('data', $obj); + } + + public static function dataProviderForMethods(): array + { + return [ + ['makes'], + ['models'], + ]; + } + + public function test_years(): void + { + $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); + $client = $this->createMockClient(200, '["data"]'); + $sdk = new Powersports($config, $client); + $arr = $sdk->years(); + $this->assertNotEmpty($arr); + } + + public function test_exception_response(): void + { + $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); + $client = $this->createMockClient( + 401, + '{ + "exception": "ExceptionName", + "code": 500, + "url": "/url/path", + "message": "Internal Error" + }'); + $sdk = new Powersports($config, $client); + + $this->expectException(CarApiException::class); + $this->expectExceptionMessage('ExceptionName: Internal Error while requesting /url/path'); + $sdk->years(); + } + + public function test_malformed_json_response(): void + { + $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); + $client = $this->createMockClient(200, 'bad json'); + $sdk = new Powersports($config, $client); + + $this->expectException(CarApiException::class); + $this->expectExceptionMessage('Error decoding response'); + $sdk->years(); + } + + public function test_query_params(): void + { + $config = CarApiConfig::build(['token' => '1', 'secret' => '1']); + $client = $this->createMockClient(200, '{"data": []}'); + $sdk = new Powersports($config, $client); + + $json = (new JsonSearch()) + ->addItem(new JsonSearchItem('make', 'in', ['Tesla'])); + + $arr = $sdk->models(['query' => ['json' => $json, 'year' => 2020]]); + $this->assertNotEmpty($arr); + } + + public function test_gzip_encoding(): void + { + $config = CarApiConfig::build(['token' => '1', 'secret' => '1', 'encoding' => ['gzip']]); + $body = base64_encode(gzencode('["data"]')); + $clientMock = $this->createMockClient(200, $body, ['Content-Encoding' => 'gzip']); + + $sdk = new Powersports($config, $clientMock); + $arr = $sdk->years(); + $this->assertNotEmpty($arr); + } +} \ No newline at end of file diff --git a/tests/TestHelperTrait.php b/tests/TestHelperTrait.php new file mode 100644 index 0000000..4cdc401 --- /dev/null +++ b/tests/TestHelperTrait.php @@ -0,0 +1,35 @@ +createPartialMock(Response::class, [ + 'getStatusCode', + 'getBody', + 'getHeader', + ]); + $stream = Psr17FactoryDiscovery::findStreamFactory()->createStream($responseBody); + $responseMock->method('getStatusCode')->willReturn($statusCode); + $responseMock->method('getBody')->willReturn($stream); + $responseMock->method('getHeader')->willReturn($headers); + $clientMock = $this->createPartialMock(Psr18Client::class, ['sendRequest']); + $clientMock->method('sendRequest')->willReturn($responseMock); + + return $clientMock; + } +} \ No newline at end of file