Skip to content

Commit

Permalink
Merge pull request #58 from plug-and-pay/feature/INTEG-238
Browse files Browse the repository at this point in the history
INTEG-238: create refresh, access token and redirect url
  • Loading branch information
JoeyDeHaas authored Sep 6, 2024
2 parents 5f23e5e + e9239ff commit 6b69a17
Show file tree
Hide file tree
Showing 12 changed files with 496 additions and 17 deletions.
17 changes: 14 additions & 3 deletions Tests/Feature/ClientMock.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,24 @@ public function delete(string $path): Response
return $this->standardResponse();
}

private function standardResponse(): Response
public function getAccessToken(string $code, string $codeVerifier, string $redirectUri, int $clientId): Response
{
$exception = ExceptionFactory::create($this->status, json_encode($this->responseBody, JSON_THROW_ON_ERROR));
return $this->standardResponse([
'code' => $code,
'codeVerifier' => $codeVerifier,
'redirectUri' => $redirectUri,
'clientId' => $clientId,
]);
}

private function standardResponse(array $additionalBody = []): Response
{
$responseBody = array_merge($this->responseBody, $additionalBody);
$exception = ExceptionFactory::create($this->status, json_encode($responseBody, JSON_THROW_ON_ERROR));
if ($exception) {
throw $exception;
}

return new Response($this->status, $this->responseBody);
return new Response($this->status, $responseBody);
}
}
27 changes: 27 additions & 0 deletions Tests/Feature/Token/GetAccessTokenTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace PlugAndPay\Sdk\Tests\Feature\Token;

use PHPUnit\Framework\TestCase;
use PlugAndPay\Sdk\Tests\Feature\ClientMock;

class GetAccessTokenTest extends TestCase
{
/** @test */
public function it_should_return_access_token(): void
{
$client = new ClientMock(200, ['access_token' => 'token']);
$response = $client->getAccessToken('code', 'codeVerifier', 'redirectUri', 123);

$this->assertEquals(200, $response->status());
$this->assertEquals([
'access_token' => 'token',
'code' => 'code',
'codeVerifier' => 'codeVerifier',
'redirectUri' => 'redirectUri',
'clientId' => 123,
], $response->body());
}
}
92 changes: 92 additions & 0 deletions Tests/Unit/ClientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

namespace PlugAndPay\Sdk\Tests\Unit;

use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\Response as GuzzleResponse;
use PHPUnit\Framework\TestCase;
use PlugAndPay\Sdk\Service\Client;
use PlugAndPay\Sdk\Service\TokenService;

class ClientTest extends TestCase
{
/** @test */
public function it_should_return_random_string(): void
{
$length = 10;

$randomString = Client::getRandomString($length);

$this->assertIsString($randomString);
$this->assertEquals($length, strlen($randomString));
}

/** @test */
public function it_should_return_authorization_url(): void
{
$clientId = 123;
$state = 'testState';
$codeVerifier = 'testVerifier';
$redirectUrl = 'https://example.com/callback';

$client = new Client();
$url = $client->generateAuthorizationUrl($clientId, $state, $codeVerifier, $redirectUrl);

$this->assertStringContainsString('client_id=123', $url);
$this->assertStringContainsString('state=testState', $url);
$this->assertStringContainsString('redirect_uri=https%3A%2F%2Fexample.com%2Fcallback', $url);
}

/** @test */
public function it_should_return_credentials(): void
{
$code = 'testCode';
$codeVerifier = 'testVerifier';
$redirectUrl = 'https://example.com/callback';
$clientId = 123;

$mockResponse = new GuzzleResponse(200, [], json_encode(['access_token' => 'testAccessToken', 'refresh_token' => 'testRefreshToken'], JSON_THROW_ON_ERROR));
$mockGuzzleClient = $this->createMock(GuzzleClient::class);
$mockGuzzleClient->method('request')->willReturn($mockResponse);

$client = new Client(null, null, null, null, $mockGuzzleClient);
$response = $client->getCredentials($code, $codeVerifier, $redirectUrl, $clientId);

$this->assertEquals(200, $response->status());
$this->assertEquals('testAccessToken', $response->body()['access_token']);
}

/** @test */
public function it_should_refresh_access_token_if_needed(): void
{
$mockResponse = new GuzzleResponse(200, [], json_encode(['access_token' => 'newAccessToken'], JSON_THROW_ON_ERROR));
$mockGuzzleClient = $this->createMock(GuzzleClient::class);
$mockGuzzleClient->method('request')->willReturn($mockResponse);

$mockTokenService = $this->createMock(TokenService::class);
$mockTokenService->method('isValid')->willReturn(false);

$client = new Client('expiredAccessToken', 'expiredRefreshToken', null, 123, $mockGuzzleClient, $mockTokenService);

$client->refreshAccessTokenIfNeeded();

$this->assertEquals('newAccessToken', $client->getAccessToken());
}

/** @test */
public function it_should_refresh_access_token(): void
{
$refreshToken = 'testRefreshToken';
$clientId = 123;

$mockResponse = new GuzzleResponse(200, [], json_encode(['access_token' => 'newAccessToken']));
$mockGuzzleClient = $this->createMock(GuzzleClient::class);
$mockGuzzleClient->method('request')->willReturn($mockResponse);

$client = new Client(null, null, null, null, $mockGuzzleClient);
$response = $client->refreshAccessToken($refreshToken, $clientId);

$this->assertEquals(200, $response->status());
$this->assertEquals('newAccessToken', $response->body()['access_token']);
}
}
23 changes: 23 additions & 0 deletions Tests/Unit/StringUtilsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace PlugAndPay\Sdk\Tests\Unit;

use PHPUnit\Framework\TestCase;
use PlugAndPay\Sdk\Support\Str;

class StringUtilsTest extends TestCase
{
/** @test */
public function it_should_generate_correct_string_length(): void
{
$this->assertEquals(40, strlen(Str::random(40)));
}

/** @test */
public function it_should_generate_default_string_length(): void
{
$this->assertEquals(16, strlen(Str::random()));
}
}
90 changes: 90 additions & 0 deletions Tests/Unit/TokenServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace PlugAndPay\Sdk\Tests\Unit;

use PHPUnit\Framework\TestCase;
use PlugAndPay\Sdk\Exception\InvalidTokenException;
use PlugAndPay\Sdk\Service\TokenService;

class TokenServiceTest extends TestCase
{
/** @test */
public function it_should_return_true_for_valid_token(): void
{
$payload = json_encode(['exp' => time() + 3600], JSON_THROW_ON_ERROR); // Token expires in 1 hour
$jwt = $this->createJwt($payload);

$isTokenValid = (new TokenService)->isValid($jwt);

$this->assertTrue($isTokenValid);
}

/** @test */
public function it_should_return_false_for_invalid_token(): void
{
$payload = json_encode(['exp' => time() - 3600], JSON_THROW_ON_ERROR); // Token expired 1 hour ago
$jwt = $this->createJwt($payload);

$isTokenValid = (new TokenService)->isValid($jwt);

$this->assertFalse($isTokenValid);
}

/** @test */
public function it_should_throw_exception_when_jwt_structure_is_invalid(): void
{
$this->expectException(InvalidTokenException::class);
$this->expectExceptionMessage('Invalid JWT structure.');

(new TokenService)->isValid('invalid.jwt');
}

/** @test */
public function it_should_throw_exception_when_payload_is_invalid(): void
{
$this->expectException(InvalidTokenException::class);
$this->expectExceptionMessage('Invalid payload JSON.');

$jwt = $this->createJwt('invalid_json');
(new TokenService)->isValid($jwt);
}

/** @test */
public function it_should_throw_exception_when_expiration_time_is_not_set(): void
{
$this->expectException(InvalidTokenException::class);
$this->expectExceptionMessage('Expiration time not set in token.');

$payload = json_encode(['data' => 'no_exp'], JSON_THROW_ON_ERROR);
$jwt = $this->createJwt($payload);

(new TokenService)->isValid($jwt);
}

private function createJwt(string $payload): string
{
$header = json_encode(['alg' => 'HS256', 'typ' => 'JWT'], JSON_THROW_ON_ERROR);
$base64UrlHeader = $this->base64UrlEncode($header);
$base64UrlPayload = $this->base64UrlEncode($payload);
$signature = 'signature'; // Mock signature for testing

return $base64UrlHeader . '.' . $base64UrlPayload . '.' . $signature;
}

/** @test */
public function it_should_return_false_for_token_with_less_than_30_seconds_ttl(): void
{
$payload = json_encode(['exp' => time() + 20], JSON_THROW_ON_ERROR); // Token expires in 20 seconds

$isTokenValid = (new TokenService)->isValid($this->createJwt($payload));

$this->assertFalse($isTokenValid);
}

private function base64UrlEncode(string $input): string
{
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($input));
}
}
1 change: 1 addition & 0 deletions src/Entity/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Response extends AbstractEntity
public const HTTP_OK = 200;
public const HTTP_CREATED = 201;
public const HTTP_NO_CONTENT = 204;
public const HTTP_BAD_REQUEST = 400;
public const HTTP_UNAUTHORIZED = 401;
public const HTTP_NOT_FOUND = 404;
public const HTTP_UNPROCESSABLE_ENTITY = 422;
Expand Down
4 changes: 4 additions & 0 deletions src/Exception/ExceptionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public static function create(int $status, string $body = ''): ?Exception
return new NotFoundException();
case Response::HTTP_UNAUTHORIZED:
return new UnauthenticatedException();
case Response::HTTP_BAD_REQUEST:
$body = json_decode($body, true, 512, JSON_THROW_ON_ERROR);

return new \RuntimeException("Error: $status; Body: " . $body['message']);
default:
return null;
}
Expand Down
15 changes: 15 additions & 0 deletions src/Exception/InvalidTokenException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace PlugAndPay\Sdk\Exception;

use Exception;

class InvalidTokenException extends Exception
{
public function __construct($message = 'Invalid token')
{
parent::__construct($message);
}
}
Loading

0 comments on commit 6b69a17

Please sign in to comment.