diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8ef9f42d..ac6b83a5 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -8,8 +8,8 @@ jobs: # always run on push events # only run on pull_request_target event when pull request pulls from fork repository if: > - github.event_name == 'push' || - github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository + github.event_name == 'push' || + github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository strategy: fail-fast: false matrix: @@ -31,18 +31,16 @@ jobs: - run: composer test - php7-4: - name: Unit Tests php7.4 (php ${{ matrix.php-version }}) + php8-2: + name: Unit Tests php8.2 (php ${{ matrix.php-version }}) runs-on: ubuntu-latest - # always run on push events - # only run on pull_request_target event when pull request pulls from fork repository if: > - github.event_name == 'push' || + github.event_name == 'push' || github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository strategy: fail-fast: false matrix: - php-version: [ 7.4 ] + php-version: [ 8.2 ] steps: - uses: actions/checkout@v2 @@ -51,24 +49,24 @@ jobs: with: php-version: ${{ matrix.php-version }} + - run: composer remove --dev --no-update --no-interaction friendsofphp/php-cs-fixer + - run: composer self-update - run: composer install --no-interaction --prefer-source --dev - run: composer test - php8-0: - name: Unit Tests php8.0 (php ${{ matrix.php-version }}) + php8-3: + name: Unit Tests php8.3 (php ${{ matrix.php-version }}) runs-on: ubuntu-latest - # always run on push events - # only run on pull_request_target event when pull request pulls from fork repository if: > - github.event_name == 'push' || + github.event_name == 'push' || github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository strategy: fail-fast: false matrix: - php-version: [ 8.0 ] + php-version: [ 8.3 ] steps: - uses: actions/checkout@v2 @@ -77,6 +75,34 @@ jobs: with: php-version: ${{ matrix.php-version }} + - run: composer remove --dev --no-update --no-interaction friendsofphp/php-cs-fixer + + - run: composer self-update + + - run: composer install --no-interaction --prefer-source --dev + + - run: composer test + + php8-4: + name: Unit Tests php8.4 (php ${{ matrix.php-version }}) + runs-on: ubuntu-latest + if: > + github.event_name == 'push' || + github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository + strategy: + fail-fast: false + matrix: + php-version: [ 8.4 ] + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@2.9.0 + with: + php-version: ${{ matrix.php-version }} + + - run: composer remove --dev --no-update --no-interaction friendsofphp/php-cs-fixer + - run: composer self-update - run: composer install --no-interaction --prefer-source --dev @@ -84,13 +110,13 @@ jobs: - run: composer test protobuf: - name: Unit Tests With Protobuf C Extension 3.13 (php ${{ matrix.php-version }}) + name: Unit Tests With Protobuf C Extension (php ${{ matrix.php-version }}) runs-on: ubuntu-latest # always run on push events # only run on pull_request_target event when pull request pulls from fork repository if: > - github.event_name == 'push' || - github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository + github.event_name == 'push' || + github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository strategy: fail-fast: false matrix: @@ -109,4 +135,3 @@ jobs: - run: composer install --no-interaction --prefer-source --dev - run: composer test - diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache new file mode 100644 index 00000000..c0dde319 --- /dev/null +++ b/.php-cs-fixer.cache @@ -0,0 +1 @@ +{"php":"8.1.27","version":"3.22.0:v3.22.0#92b019f6c8d79aa26349d0db7671d37440dc0ff3","indent":" ","lineEnding":"\n","rules":{"array_syntax":{"syntax":"short"},"no_unused_imports":true,"ordered_imports":{"imports_order":["const","class","function"]},"php_unit_fqcn_annotation":true,"phpdoc_return_self_reference":true,"phpdoc_scalar":true},"hashes":{"\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder2419\/examples\/digitalidentity\/app\/Http\/Controllers\/IdentityController.php":"369515522c3efd6cd55a8363d4e97c05","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder4308\/src\/Identity\/Policy\/Policy.php":"e1bca74eaafe5271dd1a38769fe1c3b2","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder3692\/src\/Identity\/Policy\/PolicyBuilder.php":"88302b88aba33563661d4b989b5dc429","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder2054\/tests\/Identity\/Policy\/PolicyBuilderTest.php":"a262a261102744a1acf6d5d0b421dc44","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder5221\/src\/Identity\/ReceiptBuilder.php":"2e6ef33d3401f7cbd36145ad66b3b2ef","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder289\/examples\/digitalidentity\/app\/Http\/Controllers\/ReceiptController.php":"e79ec7e1511895c954f77c713d435ad2","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder2348\/tests\/Identity\/ReceiptTest.php":"b602e6828020fef411df597e17fa7c88","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder4139\/examples\/digitalidentity\/routes\/web.php":"dcdc77843f3e59dd61467a324edf0c77","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder5396\/src\/Identity\/Receipt.php":"4744c8887009fd9ffbf084f99021eb1c","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder5193\/src\/Identity\/WrappedReceipt.php":"3a77a22be093a1da75438ea2bb9fcb20","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder4416\/examples\/digitalidentity\/app\/Http\/Controllers\/AdvancedIdentityController.php":"6b5c23f2ce8da246bc41d136d87ecdb6","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder5544\/examples\/digitalidentity\/app\/Http\/Controllers\/AdvancedIdentityController.php":"6b5c23f2ce8da246bc41d136d87ecdb6","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder4533\/src\/Identity\/Policy\/Policy.php":"bea4b7ebb268fca1ad719f933ec82cbd","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder3559\/tests\/Identity\/Policy\/PolicyBuilderTest.php":"f6d7380ae2db4eca426bb39ccfb3a900","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder2922\/examples\/digitalidentity\/routes\/web.php":"fdf260e4dfd18c8ba12078943564875a","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder3947\/src\/Constants.php":"4bb1127c9665c5d0496b90ea3211951d","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder4440\/src\/Constants.php":"99a3224f6e3fcae067362798bbab64f0","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder4745\/src\/Constants.php":"afc40e02bdc3a87ff7a874826447c604","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder1311\/src\/Constants.php":"77a8a39eac3e973495b7719ebd41509e","\/private\/var\/folders\/b6\/tqq9d7y54ll62fjfysz50ry80000gn\/T\/PHP CS Fixertemp_folder1635\/examples\/digitalidentity\/app\/Http\/Controllers\/ReceiptController.php":"10f70ffe111a0030b29762494cc5de7d"}} \ No newline at end of file diff --git a/README.md b/README.md index 6f35b579..4cfbbe17 100755 --- a/README.md +++ b/README.md @@ -27,9 +27,12 @@ Please feel free to reach out ## Requirements -* PHP ^7.4 || ^8.0 || ^8.1 +* PHP ^8.1 * CURL PHP extension (must support TLSv1.2) +> **Breaking change:** this SDK release supports PHP 8.1 and above only. +> Support for PHP 7.4 and PHP 8.0 has been removed. +> If you are still running on PHP 7.4/8.0, please remain on an earlier SDK version until you can upgrade your runtime. ### Recommended (optional) - [Protobuf C extension](https://github.com/protocolbuffers/protobuf/tree/master/php) (PHP package will be used by default) @@ -42,13 +45,13 @@ Add the Yoti SDK dependency: ```json "require": { - "yoti/yoti-php-sdk" : "^4.3.0" + "yoti/yoti-php-sdk" : "^4.5.0" } ``` Or run this Composer command ```console -$ composer require yoti/yoti-php-sdk "^4.3.0" +$ composer require yoti/yoti-php-sdk "^4.5.0" ``` ## Setup diff --git a/composer.json b/composer.json index 95ef8a52..91b4d313 100755 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "yoti/yoti-php-sdk", "description": "Yoti SDK for quickly integrating your PHP backend with Yoti", - "version": "4.3.0", + "version": "4.5.0", "keywords": [ "yoti", "sdk" @@ -9,10 +9,10 @@ "homepage": "https://yoti.com", "license": "MIT", "require": { - "php": "^7.4 || ^8.0 || ^8.1", + "php": "^8.1", "ext-json": "*", - "google/protobuf": "^3.10", - "phpseclib/phpseclib": "^3.0", + "google/protobuf": "^4.33.6", + "phpseclib/phpseclib": "^3.0.50", "guzzlehttp/guzzle": "^7.0", "psr/http-client": "^1.0", "psr/http-message": "^2.0", diff --git a/examples/digitalidentity/README.md b/examples/digitalidentity/README.md index 3673b72a..9bf2d1b3 100644 --- a/examples/digitalidentity/README.md +++ b/examples/digitalidentity/README.md @@ -24,4 +24,4 @@ This example requires [Docker](https://docs.docker.com/) ## Digital Identity(Advanced) Share Example * Visit [/generate-advanced-identity-share](https://localhost:4002/generate-advanced-identity-share) * ## Digital Identity DBS Example -* Visit [/generate-dbs-share](https://localhost:4002/generate-dbs-share) \ No newline at end of file +* Visit [/generate-dbs-share](https://localhost:4002/generate-dbs-share) diff --git a/examples/digitalidentity/resources/views/dbs.blade.php b/examples/digitalidentity/resources/views/dbs.blade.php index 1359cc7c..9f325177 100644 --- a/examples/digitalidentity/resources/views/dbs.blade.php +++ b/examples/digitalidentity/resources/views/dbs.blade.php @@ -85,5 +85,6 @@ function onErrorListener(...data) { await onReadyToStart() } + diff --git a/examples/digitalidentity/routes/web.php b/examples/digitalidentity/routes/web.php index 3fb377e7..f0e02806 100644 --- a/examples/digitalidentity/routes/web.php +++ b/examples/digitalidentity/routes/web.php @@ -19,4 +19,4 @@ Route::get('/generate-advanced-identity-share', 'AdvancedIdentityController@show'); Route::get('/generate-advanced-identity-session', 'AdvancedIdentityController@generateSession'); Route::get('/generate-dbs-share', 'DbsController@show'); -Route::get('/generate-dbs-session', 'DbsController@generateSession'); \ No newline at end of file +Route::get('/generate-dbs-session', 'DbsController@generateSession'); diff --git a/examples/doc-scan/app/Console/Kernel.php b/examples/doc-scan/app/Console/Kernel.php index 69914e99..31f4b24d 100644 --- a/examples/doc-scan/app/Console/Kernel.php +++ b/examples/doc-scan/app/Console/Kernel.php @@ -34,7 +34,7 @@ protected function schedule(Schedule $schedule) */ protected function commands() { - $this->load(__DIR__.'/Commands'); + $this->load(__DIR__ . '/Commands'); require base_path('routes/console.php'); } diff --git a/examples/doc-scan/app/Http/Controllers/HomeController.php b/examples/doc-scan/app/Http/Controllers/HomeController.php index 299fa6b1..d52fdd8e 100644 --- a/examples/doc-scan/app/Http/Controllers/HomeController.php +++ b/examples/doc-scan/app/Http/Controllers/HomeController.php @@ -103,11 +103,12 @@ public function show(Request $request, DocScanClient $client) ->withMaxRetries(3) ->build() ) + /* ->withRequestedCheck( (new RequestedWatchlistAdvancedCaCheckBuilder()) ->withConfig($customConfig) ->build() - ) + )*/ ->withRequestedCheck( (new RequestedFaceMatchCheckBuilder()) ->withManualCheckFallback() @@ -150,6 +151,12 @@ public function show(Request $request, DocScanClient $client) ->withErrorUrl(config('app.url') . '/error') ->withPrivacyPolicyUrl(config('app.url') . '/privacy-policy') ->withBiometricConsentFlow('EARLY') + ->withBrandId('brand_id') + // Suppress specific screens to shorten the flow + ->withSuppressedScreens(['intro_screen', 'document_capture_instruction']) + // Or add screens individually: + // ->withSuppressedScreen('intro_screen') + // ->withSuppressedScreen('document_capture_instruction') ->build() ) ->withRequiredDocument( @@ -167,14 +174,14 @@ public function show(Request $request, DocScanClient $client) ->withRequiredDocument( (new RequiredSupplementaryDocumentBuilder()) ->withObjective( - (new ProofOfAddressObjectiveBuilder) + (new ProofOfAddressObjectiveBuilder()) ->build() ) ->build() ) ->build(); - + $session = $client->createSession($sessionSpec); $request->session()->put('YOTI_SESSION_ID', $session->getSessionId()); diff --git a/examples/doc-scan/config/cache.php b/examples/doc-scan/config/cache.php index 4f41fdf9..a8eaf93b 100644 --- a/examples/doc-scan/config/cache.php +++ b/examples/doc-scan/config/cache.php @@ -99,6 +99,6 @@ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'), + 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache'), ]; diff --git a/examples/doc-scan/config/database.php b/examples/doc-scan/config/database.php index b42d9b30..3bfc47a5 100644 --- a/examples/doc-scan/config/database.php +++ b/examples/doc-scan/config/database.php @@ -123,7 +123,7 @@ 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'), ], 'default' => [ diff --git a/examples/doc-scan/config/filesystems.php b/examples/doc-scan/config/filesystems.php index cd9f0962..bd18d920 100644 --- a/examples/doc-scan/config/filesystems.php +++ b/examples/doc-scan/config/filesystems.php @@ -51,7 +51,7 @@ 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), - 'url' => env('APP_URL').'/storage', + 'url' => env('APP_URL') . '/storage', 'visibility' => 'public', ], diff --git a/examples/doc-scan/config/session.php b/examples/doc-scan/config/session.php index d0ccd5a8..60aec7d2 100644 --- a/examples/doc-scan/config/session.php +++ b/examples/doc-scan/config/session.php @@ -126,7 +126,7 @@ 'cookie' => env( 'SESSION_COOKIE', - Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + Str::slug(env('APP_NAME', 'laravel'), '_') . '_session' ), /* diff --git a/examples/doc-scan/config/yoti.php b/examples/doc-scan/config/yoti.php index ad3f6af5..b4719eae 100644 --- a/examples/doc-scan/config/yoti.php +++ b/examples/doc-scan/config/yoti.php @@ -5,7 +5,7 @@ return [ 'client.sdk.id' => env('YOTI_SDK_ID'), 'doc.scan.iframe.url' => (env('YOTI_DOC_SCAN_API_URL') ?: Constants::DOC_SCAN_API_URL) . '/web/index.html', - 'pem.file.path' => (function($filePath) { + 'pem.file.path' => (function ($filePath) { return strpos($filePath, '/') === 0 ? $filePath : base_path($filePath); })(env('YOTI_KEY_FILE_PATH')), ]; diff --git a/examples/doc-scan/public/index.php b/examples/doc-scan/public/index.php index 4584cbcd..f9ea6927 100644 --- a/examples/doc-scan/public/index.php +++ b/examples/doc-scan/public/index.php @@ -21,7 +21,7 @@ | */ -require __DIR__.'/../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; /* |-------------------------------------------------------------------------- @@ -35,7 +35,7 @@ | */ -$app = require_once __DIR__.'/../bootstrap/app.php'; +$app = require_once __DIR__ . '/../bootstrap/app.php'; /* |-------------------------------------------------------------------------- diff --git a/examples/doc-scan/server.php b/examples/doc-scan/server.php index 5fb6379e..7f109d96 100644 --- a/examples/doc-scan/server.php +++ b/examples/doc-scan/server.php @@ -14,8 +14,8 @@ // This file allows us to emulate Apache's "mod_rewrite" functionality from the // built-in PHP web server. This provides a convenient way to test a Laravel // application without having installed a "real" web server software here. -if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { +if ($uri !== '/' && file_exists(__DIR__ . '/public' . $uri)) { return false; } -require_once __DIR__.'/public/index.php'; +require_once __DIR__ . '/public/index.php'; diff --git a/src/Aml/Profile.php b/src/Aml/Profile.php index b0ee2a10..d03219c3 100644 --- a/src/Aml/Profile.php +++ b/src/Aml/Profile.php @@ -50,7 +50,7 @@ class Profile implements \JsonSerializable * @param \Yoti\Aml\Address $amlAddress * @param null|string $ssn */ - public function __construct($givenNames, $familyName, Address $amlAddress, string $ssn = null) + public function __construct(string $givenNames, string $familyName, Address $amlAddress, ?string $ssn = null) { $this->givenNames = $givenNames; $this->familyName = $familyName; diff --git a/src/Auth/AuthenticationTokenGenerator.php b/src/Auth/AuthenticationTokenGenerator.php new file mode 100644 index 00000000..3b267889 --- /dev/null +++ b/src/Auth/AuthenticationTokenGenerator.php @@ -0,0 +1,227 @@ +withSdkId($sdkId) + * ->withPemFile($pemFile) + * ->build(); + * + * $response = $generator->generate(['scope1', 'scope2']); + * $token = $response->getAccessToken(); + * + * Mirrors the Java SDK's com.yoti.auth.AuthenticationTokenGenerator. + */ +class AuthenticationTokenGenerator +{ + /** + * @var string + */ + private $sdkId; + + /** + * @var PemFile + */ + private $pemFile; + + /** + * @var callable + */ + private $jwtIdSupplier; + + /** + * @var string + */ + private $authApiUrl; + + /** + * @var ClientInterface + */ + private $httpClient; + + /** + * @param string $sdkId + * @param PemFile $pemFile + * @param callable $jwtIdSupplier + * @param string $authApiUrl + * @param ClientInterface|null $httpClient + */ + public function __construct( + string $sdkId, + PemFile $pemFile, + callable $jwtIdSupplier, + string $authApiUrl, + ?ClientInterface $httpClient = null + ) { + $this->sdkId = $sdkId; + $this->pemFile = $pemFile; + $this->jwtIdSupplier = $jwtIdSupplier; + $this->authApiUrl = $authApiUrl; + $this->httpClient = $httpClient ?? new \GuzzleHttp\Client(); + } + + /** + * Creates a new Builder instance. + * + * @return Builder + */ + public static function builder(): Builder + { + return new Builder(); + } + + /** + * Generate an authentication token for the supplied scopes. + * + * @param array $scopes + * + * @return CreateAuthenticationTokenResponse + * + * @throws AuthException + * @throws \InvalidArgumentException + */ + public function generate(array $scopes): CreateAuthenticationTokenResponse + { + if (count($scopes) === 0) { + throw new \InvalidArgumentException('scopes must not be empty'); + } + + $jwt = $this->createSignedJwt(); + + $formParams = [ + 'grant_type' => 'client_credentials', + 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + 'scope' => implode(' ', $scopes), + 'client_assertion' => $jwt, + ]; + + $responseBody = $this->performFormRequest($formParams); + + $responseData = json_decode($responseBody, true); + if (!is_array($responseData)) { + throw new AuthException('Failed to decode authentication token response'); + } + + return new CreateAuthenticationTokenResponse($responseData); + } + + /** + * Create a PS384-signed JWT for the client assertion. + * + * Uses phpseclib3 for RSASSA-PSS (PS384) signing, since + * firebase/php-jwt does not support PSS algorithms. + * + * @return string + * + * @throws AuthException + */ + private function createSignedJwt(): string + { + $sdkIdProperty = sprintf('sdk:%s', $this->sdkId); + $now = time(); + $jwtId = ($this->jwtIdSupplier)(); + + $header = [ + 'alg' => 'PS384', + 'typ' => 'JWT', + ]; + + $claims = [ + 'iss' => $sdkIdProperty, + 'sub' => $sdkIdProperty, + 'jti' => $jwtId, + 'aud' => $this->authApiUrl, + 'exp' => $now + 300, // 5 minutes + 'iat' => $now, + ]; + + $headerEncoded = $this->base64UrlEncode((string) json_encode($header)); + $claimsEncoded = $this->base64UrlEncode((string) json_encode($claims)); + $signingInput = $headerEncoded . '.' . $claimsEncoded; + + try { + /** @var \phpseclib3\Crypt\RSA\PrivateKey $rsaKey */ + $rsaKey = \phpseclib3\Crypt\PublicKeyLoader::load((string) $this->pemFile); + $rsaKey = $rsaKey + ->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PSS) + ->withHash('sha384') + ->withMGFHash('sha384'); + } catch (\Exception $e) { + throw new AuthException('Failed to load private key from PEM file: ' . $e->getMessage(), 0, $e); + } + + $signature = $rsaKey->sign($signingInput); + + return $signingInput . '.' . $this->base64UrlEncode($signature); + } + + /** + * Base64url-encode a string (RFC 7515). + * + * @param string $data + * + * @return string + */ + private function base64UrlEncode(string $data): string + { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } + + /** + * Perform an application/x-www-form-urlencoded POST request. + * + * @param array $formParams + * + * @return string + * + * @throws AuthException + */ + private function performFormRequest(array $formParams): string + { + $postData = http_build_query($formParams); + + $request = new \GuzzleHttp\Psr7\Request( + 'POST', + $this->authApiUrl, + [ + 'Content-Type' => 'application/x-www-form-urlencoded', + 'Content-Length' => (string) strlen($postData), + ], + $postData + ); + + try { + $response = $this->httpClient->sendRequest($request); + } catch (\Psr\Http\Client\ClientExceptionInterface $e) { + throw new AuthException('Auth token request failed: ' . $e->getMessage(), 0, $e); + } + + $httpCode = $response->getStatusCode(); + $responseBody = (string) $response->getBody(); + + if ($httpCode >= 400) { + throw new AuthException( + sprintf( + 'Auth token request failed with HTTP %d: %s', + $httpCode, + $responseBody + ) + ); + } + + return $responseBody; + } +} diff --git a/src/Auth/Builder.php b/src/Auth/Builder.php new file mode 100644 index 00000000..9438c58d --- /dev/null +++ b/src/Auth/Builder.php @@ -0,0 +1,190 @@ +sdkId = $sdkId; + return $this; + } + + /** + * Sets the PEM file used for signing the JWT. + * + * @param PemFile $pemFile + * + * @return self + */ + public function withPemFile(PemFile $pemFile): self + { + $this->pemFile = $pemFile; + return $this; + } + + /** + * Sets the PEM file from a file path. + * + * @param string $filePath + * + * @return self + */ + public function withPemFilePath(string $filePath): self + { + return $this->withPemFile(PemFile::fromFilePath($filePath)); + } + + /** + * Sets the PEM file from a string. + * + * @param string $content + * + * @return self + */ + public function withPemString(string $content): self + { + return $this->withPemFile(PemFile::fromString($content)); + } + + /** + * Sets a callable that generates unique JWT IDs. + * Defaults to generating UUID v4 if not provided. + * + * @param callable $jwtIdSupplier A callable that returns a string + * + * @return self + */ + public function withJwtIdSupplier(callable $jwtIdSupplier): self + { + $this->jwtIdSupplier = $jwtIdSupplier; + return $this; + } + + /** + * Sets a custom auth API URL (primarily for testing). + * + * @param string $authApiUrl + * + * @return self + */ + public function withAuthApiUrl(string $authApiUrl): self + { + $this->authApiUrl = $authApiUrl; + return $this; + } + + /** + * Sets a custom PSR-18 HTTP client (primarily for testing). + * + * @param ClientInterface $httpClient + * + * @return self + */ + public function withHttpClient(ClientInterface $httpClient): self + { + $this->httpClient = $httpClient; + return $this; + } + + /** + * Builds the AuthenticationTokenGenerator. + * + * @return AuthenticationTokenGenerator + * + * @throws \InvalidArgumentException + */ + public function build(): AuthenticationTokenGenerator + { + if ($this->sdkId === null || $this->sdkId === '') { + throw new \InvalidArgumentException("'sdkId' must not be empty or null"); + } + + if ($this->pemFile === null) { + throw new \InvalidArgumentException("'pemFile' must not be null"); + } + + $jwtIdSupplier = $this->jwtIdSupplier ?? static function (): string { + return self::generateUuidV4(); + }; + + // Resolve auth URL: custom > environment variable > default + $authApiUrl = $this->authApiUrl + ?? Env::get(Properties::ENV_YOTI_AUTH_URL) + ?? Properties::DEFAULT_YOTI_AUTH_URL; + + return new AuthenticationTokenGenerator( + $this->sdkId, + $this->pemFile, + $jwtIdSupplier, + $authApiUrl, + $this->httpClient + ); + } + + /** + * Generate a UUID v4. + * + * @return string + */ + private static function generateUuidV4(): string + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff) + ); + } +} diff --git a/src/Auth/CreateAuthenticationTokenResponse.php b/src/Auth/CreateAuthenticationTokenResponse.php new file mode 100644 index 00000000..b9e6adb8 --- /dev/null +++ b/src/Auth/CreateAuthenticationTokenResponse.php @@ -0,0 +1,88 @@ + $responseData + */ + public function __construct(array $responseData) + { + $this->accessToken = $responseData['access_token'] ?? ''; + $this->tokenType = $responseData['token_type'] ?? ''; + $this->expiresIn = isset($responseData['expires_in']) ? (int)$responseData['expires_in'] : null; + $this->scope = $responseData['scope'] ?? null; + } + + /** + * Returns the Yoti Authentication token used to perform requests to other Yoti services. + * + * @return string + */ + public function getAccessToken(): string + { + return $this->accessToken; + } + + /** + * Returns the type of the newly generated authentication token. + * + * @return string + */ + public function getTokenType(): string + { + return $this->tokenType; + } + + /** + * Returns the amount of time (in seconds) in which the newly generated + * Authentication Token will expire. + * + * @return int|null + */ + public function getExpiresIn(): ?int + { + return $this->expiresIn; + } + + /** + * A whitespace delimited string of scopes that the Authentication token has. + * + * @return string|null + */ + public function getScope(): ?string + { + return $this->scope; + } +} diff --git a/src/Auth/Exception/AuthException.php b/src/Auth/Exception/AuthException.php new file mode 100644 index 00000000..3b6b8ec8 --- /dev/null +++ b/src/Auth/Exception/AuthException.php @@ -0,0 +1,12 @@ +id = $sdkId; } + /** + * Returns a new Builder instance for fluent construction. + * + * @return DigitalIdentityClientBuilder + */ + public static function builder(): DigitalIdentityClientBuilder + { + return new DigitalIdentityClientBuilder(); + } + + /** + * Internal factory used by DigitalIdentityClientBuilder to create an instance + * with an already-configured service. + * + * @internal + * @param DigitalIdentityService $service + * @return self + */ + public static function fromService(DigitalIdentityService $service): self + { + $instance = new \ReflectionClass(self::class); + /** @var self $client */ + $client = $instance->newInstanceWithoutConstructor(); + $client->digitalIdentityService = $service; + $client->id = ''; + return $client; + } + /** * Create a sharing session to initiate a sharing process based on a policy * diff --git a/src/DigitalIdentityClientBuilder.php b/src/DigitalIdentityClientBuilder.php new file mode 100644 index 00000000..0c067dea --- /dev/null +++ b/src/DigitalIdentityClientBuilder.php @@ -0,0 +1,195 @@ +withClientSdkId('your-sdk-id') + * ->withPemFilePath('/path/to/key.pem') + * ->build(); + * + * // Authentication token mode: + * $client = DigitalIdentityClient::builder() + * ->withAuthenticationToken('your-bearer-token') + * ->build(); + * ``` + */ +class DigitalIdentityClientBuilder +{ + /** + * @var string|null + */ + private $authenticationToken; + + /** + * @var string|null + */ + private $sdkId; + + /** + * @var PemFile|null + */ + private $pemFile; + + /** + * @var array + */ + private $options = []; + + /** + * Set the authentication token for Bearer token auth mode. + * Mutually exclusive with sdkId/PEM configuration. + * + * @param string $authenticationToken + * @return $this + */ + public function withAuthenticationToken(string $authenticationToken): self + { + $this->authenticationToken = $authenticationToken; + return $this; + } + + /** + * Set the SDK client ID for signed request auth mode. + * + * @param string $sdkId + * @return $this + */ + public function withClientSdkId(string $sdkId): self + { + $this->sdkId = $sdkId; + return $this; + } + + /** + * Set the PEM file for signed request auth mode. + * + * @param PemFile $pemFile + * @return $this + */ + public function withPemFile(PemFile $pemFile): self + { + $this->pemFile = $pemFile; + return $this; + } + + /** + * Set the PEM from a file path for signed request auth mode. + * + * @param string $pemFilePath + * @return $this + */ + public function withPemFilePath(string $pemFilePath): self + { + $this->pemFile = PemFile::resolveFromString($pemFilePath); + return $this; + } + + /** + * Set the PEM from a string for signed request auth mode. + * + * @param string $pemString + * @return $this + */ + public function withPemString(string $pemString): self + { + $this->pemFile = PemFile::resolveFromString($pemString); + return $this; + } + + /** + * Set SDK configuration options. + * + * @param array $options + * @return $this + */ + public function withOptions(array $options): self + { + $this->options = $options; + return $this; + } + + /** + * Build the DigitalIdentityClient instance. + * + * @return DigitalIdentityClient + * @throws \InvalidArgumentException if configuration is invalid + */ + public function build(): DigitalIdentityClient + { + // Set API URL from environment variable. + $this->options[Config::API_URL] = $this->options[Config::API_URL] + ?? Env::get(Constants::ENV_DIGITAL_IDENTITY_API_URL); + + $config = new Config($this->options); + + if ($this->authenticationToken !== null) { + $this->validateAuthToken(); + /** @var string $authToken */ + $authToken = $this->authenticationToken; + $authStrategy = new BearerTokenStrategy($authToken); + $service = DigitalIdentityService::withAuthStrategy($authStrategy, $config); + return DigitalIdentityClient::fromService($service); + } + + $this->validateForSignedRequest(); + /** @var string $sdkId */ + $sdkId = $this->sdkId; + /** @var PemFile $pemFile */ + $pemFile = $this->pemFile; + $service = new DigitalIdentityService($sdkId, $pemFile, $config); + return DigitalIdentityClient::fromService($service); + } + + /** + * Validate that sdkId and PEM are provided for signed request mode. + * + * @throws \InvalidArgumentException + */ + private function validateForSignedRequest(): void + { + if ($this->sdkId === null || $this->sdkId === '' || $this->pemFile === null) { + throw new \InvalidArgumentException( + 'An sdkId and PEM file must be provided when not using an authentication token' + ); + } + } + + /** + * Validate that sdkId and PEM are NOT provided when using auth token mode. + * + * @throws \InvalidArgumentException + */ + private function validateAuthToken(): void + { + Validation::notEmptyString($this->authenticationToken, 'Authentication token'); + + if ($this->sdkId !== null || $this->pemFile !== null) { + throw new \InvalidArgumentException( + 'Must not supply sdkId or PEM file when using an authentication token' + ); + } + } +} diff --git a/src/DocScan/Constants.php b/src/DocScan/Constants.php index 8551c6ba..7e5cf894 100644 --- a/src/DocScan/Constants.php +++ b/src/DocScan/Constants.php @@ -56,4 +56,6 @@ class Constants public const RECLASSIFICATION = "RECLASSIFICATION"; public const GENERIC = "GENERIC"; + + public const VERIFY_SHARE_CODE_TASK = 'VERIFY_SHARE_CODE_TASK'; } diff --git a/src/DocScan/DocScanClient.php b/src/DocScan/DocScanClient.php index d2ca279f..d6bac470 100644 --- a/src/DocScan/DocScanClient.php +++ b/src/DocScan/DocScanClient.php @@ -66,6 +66,16 @@ public function __construct( $this->docScanService = new Service($sdkId, $pemFile, $config); } + /** + * Returns a new Builder instance for fluent construction. + * + * @return DocScanClientBuilder + */ + public static function builder(): DocScanClientBuilder + { + return new DocScanClientBuilder(); + } + /** * Creates a session within the Yoti Doc Scan session * using the supplied specification. @@ -244,4 +254,21 @@ public function triggerIbvEmailNotification(string $sessionId): void { $this->docScanService->triggerIbvEmailNotification($sessionId); } + + /** + * Internal factory used by DocScanClientBuilder to create an instance + * with an already-configured Service. + * + * @internal + * @param Service $service + * @return self + */ + public static function fromService(Service $service): self + { + $instance = new \ReflectionClass(self::class); + /** @var self $client */ + $client = $instance->newInstanceWithoutConstructor(); + $client->docScanService = $service; + return $client; + } } diff --git a/src/DocScan/DocScanClientBuilder.php b/src/DocScan/DocScanClientBuilder.php new file mode 100644 index 00000000..708bb0d5 --- /dev/null +++ b/src/DocScan/DocScanClientBuilder.php @@ -0,0 +1,195 @@ +withClientSdkId('your-sdk-id') + * ->withPemFilePath('/path/to/key.pem') + * ->build(); + * + * // Authentication token mode: + * $client = DocScanClient::builder() + * ->withAuthenticationToken('your-bearer-token') + * ->build(); + * ``` + */ +class DocScanClientBuilder +{ + /** + * @var string|null + */ + private $authenticationToken; + + /** + * @var string|null + */ + private $sdkId; + + /** + * @var PemFile|null + */ + private $pemFile; + + /** + * @var array + */ + private $options = []; + + /** + * Set the authentication token for Bearer token auth mode. + * Mutually exclusive with sdkId/PEM configuration. + * + * @param string $authenticationToken + * @return $this + */ + public function withAuthenticationToken(string $authenticationToken): self + { + $this->authenticationToken = $authenticationToken; + return $this; + } + + /** + * Set the SDK client ID for signed request auth mode. + * + * @param string $sdkId + * @return $this + */ + public function withClientSdkId(string $sdkId): self + { + $this->sdkId = $sdkId; + return $this; + } + + /** + * Set the PEM file for signed request auth mode. + * + * @param PemFile $pemFile + * @return $this + */ + public function withPemFile(PemFile $pemFile): self + { + $this->pemFile = $pemFile; + return $this; + } + + /** + * Set the PEM from a file path for signed request auth mode. + * + * @param string $pemFilePath + * @return $this + */ + public function withPemFilePath(string $pemFilePath): self + { + $this->pemFile = PemFile::resolveFromString($pemFilePath); + return $this; + } + + /** + * Set the PEM from a string for signed request auth mode. + * + * @param string $pemString + * @return $this + */ + public function withPemString(string $pemString): self + { + $this->pemFile = PemFile::resolveFromString($pemString); + return $this; + } + + /** + * Set SDK configuration options. + * + * @param array $options + * @return $this + */ + public function withOptions(array $options): self + { + $this->options = $options; + return $this; + } + + /** + * Build the DocScanClient instance. + * + * @return DocScanClient + * @throws \InvalidArgumentException if configuration is invalid + */ + public function build(): DocScanClient + { + // Set API URL from environment variable. + $this->options[Config::API_URL] = $this->options[Config::API_URL] + ?? Env::get(Constants::ENV_DOC_SCAN_API_URL); + + $config = new Config($this->options); + + if ($this->authenticationToken !== null) { + $this->validateAuthToken(); + /** @var string $authToken */ + $authToken = $this->authenticationToken; + $authStrategy = new BearerTokenStrategy($authToken); + $service = Service::withAuthStrategy($authStrategy, $config); + return DocScanClient::fromService($service); + } + + $this->validateForSignedRequest(); + /** @var string $sdkId */ + $sdkId = $this->sdkId; + /** @var PemFile $pemFile */ + $pemFile = $this->pemFile; + $service = new Service($sdkId, $pemFile, $config); + return DocScanClient::fromService($service); + } + + /** + * Validate that sdkId and PEM are provided for signed request mode. + * + * @throws \InvalidArgumentException + */ + private function validateForSignedRequest(): void + { + if ($this->sdkId === null || $this->sdkId === '' || $this->pemFile === null) { + throw new \InvalidArgumentException( + 'An sdkId and PEM file must be provided when not using an authentication token' + ); + } + } + + /** + * Validate that sdkId and PEM are NOT provided when using auth token mode. + * + * @throws \InvalidArgumentException + */ + private function validateAuthToken(): void + { + Validation::notEmptyString($this->authenticationToken, 'Authentication token'); + + if ($this->sdkId !== null || $this->pemFile !== null) { + throw new \InvalidArgumentException( + 'Must not supply sdkId or PEM file when using an authentication token' + ); + } + } +} diff --git a/src/DocScan/Service.php b/src/DocScan/Service.php index 9e57feb6..f46961e2 100644 --- a/src/DocScan/Service.php +++ b/src/DocScan/Service.php @@ -18,6 +18,7 @@ use Yoti\DocScan\Session\Retrieve\Instructions\ContactProfileResponse; use Yoti\DocScan\Session\Retrieve\Instructions\InstructionsResponse; use Yoti\DocScan\Support\SupportedDocumentsResponse; +use Yoti\Http\AuthStrategy\AuthStrategyInterface; use Yoti\Http\Payload; use Yoti\Http\Request; use Yoti\Http\RequestBuilder; @@ -37,10 +38,15 @@ class Service private $sdkId; /** - * @var PemFile + * @var PemFile|null */ private $pemFile; + /** + * @var AuthStrategyInterface|null + */ + private $authStrategy; + /** * @var Config */ @@ -64,6 +70,54 @@ public function __construct(string $sdkId, PemFile $pemFile, Config $config) $this->apiUrl = $config->getApiUrl() ?? Constants::DOC_SCAN_API_URL; } + /** + * Create a Service instance using an authentication strategy. + * + * When using BearerTokenStrategy (central auth), no sdkId or PEM + * is required since the Bearer token handles authorization. + * + * @param AuthStrategyInterface $authStrategy + * @param Config $config + * + * @return self + */ + public static function withAuthStrategy(AuthStrategyInterface $authStrategy, Config $config): self + { + $instance = new \ReflectionClass(self::class); + $service = $instance->newInstanceWithoutConstructor(); + $service->authStrategy = $authStrategy; + $service->config = $config; + $service->apiUrl = $config->getApiUrl() ?? Constants::DOC_SCAN_API_URL; + $service->sdkId = ''; + return $service; + } + + /** + * Apply authentication to a RequestBuilder. + * + * If an explicit auth strategy was set, uses it. + * Otherwise falls back to the legacy PemFile + sdkId approach. + * + * @param RequestBuilder $builder + * @param bool $includeSdkId Whether to include sdkId as query param (legacy mode only) + * + * @return RequestBuilder + */ + private function applyAuth(RequestBuilder $builder, bool $includeSdkId = true): RequestBuilder + { + if ($this->authStrategy !== null) { + return $builder->withAuthStrategy($this->authStrategy); + } + + if ($this->pemFile !== null) { + $builder->withPemFile($this->pemFile); + } + if ($includeSdkId && $this->sdkId !== null && $this->sdkId !== '') { + $builder->withQueryParam('sdkId', $this->sdkId); + } + return $builder; + } + /** * Creates a Yoti Doc Scan session using the supplied * specification. @@ -76,14 +130,14 @@ public function __construct(string $sdkId, PemFile $pemFile, Config $config) */ public function createSession(SessionSpecification $sessionSpec): CreateSessionResult { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) ->withEndpoint('/sessions') - ->withQueryParam('sdkId', $this->sdkId) ->withPayload(Payload::fromJsonData($sessionSpec)) ->withHeader('Content-Type', 'application/json') - ->withPemFile($this->pemFile) - ->withPost() + ->withPost(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -103,12 +157,12 @@ public function createSession(SessionSpecification $sessionSpec): CreateSessionR */ public function retrieveSession(string $sessionId): GetSessionResult { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) ->withEndpoint(sprintf('/sessions/%s', $sessionId)) - ->withQueryParam('sdkId', $this->sdkId) - ->withPemFile($this->pemFile) - ->withGet() + ->withGet(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -127,12 +181,12 @@ public function retrieveSession(string $sessionId): GetSessionResult */ public function deleteSession(string $sessionId): void { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) ->withEndpoint(sprintf('/sessions/%s', $sessionId)) - ->withQueryParam('sdkId', $this->sdkId) - ->withPemFile($this->pemFile) - ->withMethod(Request::METHOD_DELETE) + ->withMethod(Request::METHOD_DELETE); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -150,12 +204,12 @@ public function deleteSession(string $sessionId): void */ public function getMediaContent(string $sessionId, string $mediaId): ?Media { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) ->withEndpoint(sprintf('/sessions/%s/media/%s/content', $sessionId, $mediaId)) - ->withQueryParam('sdkId', $this->sdkId) - ->withPemFile($this->pemFile) - ->withGet() + ->withGet(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -181,12 +235,12 @@ public function getMediaContent(string $sessionId, string $mediaId): ?Media */ public function deleteMediaContent(string $sessionId, string $mediaId): void { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) ->withEndpoint(sprintf('/sessions/%s/media/%s/content', $sessionId, $mediaId)) - ->withQueryParam('sdkId', $this->sdkId) - ->withPemFile($this->pemFile) - ->withMethod(Request::METHOD_DELETE) + ->withMethod(Request::METHOD_DELETE); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -203,14 +257,14 @@ public function getSupportedDocuments(bool $isStrictlyLatin): SupportedDocuments $requestBuilder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) ->withEndpoint('/supported-documents') - ->withPemFile($this->pemFile) ->withGet(); if ($isStrictlyLatin) { $requestBuilder->withQueryParam('includeNonLatin', '1'); } - $response = $requestBuilder + // getSupportedDocuments does not require sdkId in legacy mode + $response = $this->applyAuth($requestBuilder, false) ->build() ->execute(); @@ -231,13 +285,13 @@ public function createFaceCaptureResource( string $sessionId, CreateFaceCaptureResourcePayload $createFaceCaptureResourcePayload ): CreateFaceCaptureResourceResponse { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) - ->withQueryParam('sdkId', $this->sdkId) ->withEndpoint("sessions/$sessionId/resources/face-capture") - ->withPemFile($this->pemFile) ->withPayload(Payload::fromJsonData($createFaceCaptureResourcePayload)) - ->withPost() + ->withPost(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -259,7 +313,7 @@ public function uploadFaceCaptureImage( string $resourceId, UploadFaceCaptureImagePayload $faceCaptureImagePayload ): void { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withMultipartBoundary(Config::YOTI_MULTIPART_BOUNDARY) ->withMultipartBinaryBody( "binary-content", @@ -267,11 +321,11 @@ public function uploadFaceCaptureImage( $faceCaptureImagePayload->getImageContentType(), 'face-capture-image' ) - ->withPemFile($this->pemFile) ->withBaseUrl($this->apiUrl) - ->withQueryParam('sdkId', $this->sdkId) ->withEndpoint("/sessions/$sessionId/resources/face-capture/$resourceId/image") - ->withPut() + ->withPut(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -285,12 +339,12 @@ public function uploadFaceCaptureImage( */ public function fetchSessionConfiguration(string $sessionId): SessionConfigurationResponse { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) ->withEndpoint(sprintf('/sessions/%s/configuration', $sessionId)) - ->withQueryParam('sdkId', $this->sdkId) - ->withPemFile($this->pemFile) - ->withGet() + ->withGet(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -308,12 +362,13 @@ public function fetchSessionConfiguration(string $sessionId): SessionConfigurati */ public function putIbvInstructions(string $sessionId, Instructions $instructions): void { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) - ->withPemFile($this->pemFile) ->withEndpoint(sprintf('/sessions/%s/instructions', $sessionId)) ->withPut() - ->withPayload(Payload::fromJsonData($instructions)) + ->withPayload(Payload::fromJsonData($instructions)); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -327,11 +382,12 @@ public function putIbvInstructions(string $sessionId, Instructions $instructions */ public function getIbvInstructions(string $sessionId): InstructionsResponse { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) - ->withPemFile($this->pemFile) ->withEndpoint(sprintf('/sessions/%s/instructions', $sessionId)) - ->withGet() + ->withGet(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -349,11 +405,12 @@ public function getIbvInstructions(string $sessionId): InstructionsResponse */ public function getIbvInstructionsPdf(string $sessionId): Media { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) - ->withPemFile($this->pemFile) ->withEndpoint(sprintf('/sessions/%s/instructions/pdf', $sessionId)) - ->withGet() + ->withGet(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -372,11 +429,12 @@ public function getIbvInstructionsPdf(string $sessionId): Media */ public function fetchInstructionsContactProfile(string $sessionId): ContactProfileResponse { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) - ->withPemFile($this->pemFile) ->withEndpoint(sprintf('/sessions/%s/instructions/contact-profile', $sessionId)) - ->withGet() + ->withGet(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -393,11 +451,12 @@ public function fetchInstructionsContactProfile(string $sessionId): ContactProfi */ public function triggerIbvEmailNotification(string $sessionId): void { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->apiUrl) - ->withPemFile($this->pemFile) ->withEndpoint(sprintf('/sessions/%s/instructions/email', $sessionId)) - ->withPost() + ->withPost(); + + $response = $this->applyAuth($builder) ->build() ->execute(); diff --git a/src/DocScan/Session/Create/ApplicantProfile.php b/src/DocScan/Session/Create/ApplicantProfile.php new file mode 100644 index 00000000..0eb26dc0 --- /dev/null +++ b/src/DocScan/Session/Create/ApplicantProfile.php @@ -0,0 +1,95 @@ +fullName = $fullName; + $this->dateOfBirth = $dateOfBirth; + $this->namePrefix = $namePrefix; + $this->structuredPostalAddress = $structuredPostalAddress; + } + + /** + * @return stdClass + */ + public function jsonSerialize(): stdClass + { + return (object) Json::withoutNullValues([ + 'full_name' => $this->fullName, + 'date_of_birth' => $this->dateOfBirth, + 'name_prefix' => $this->namePrefix, + 'structured_postal_address' => $this->structuredPostalAddress, + ]); + } + + /** + * @return string|null + */ + public function getFullName(): ?string + { + return $this->fullName; + } + + /** + * @return string|null + */ + public function getDateOfBirth(): ?string + { + return $this->dateOfBirth; + } + + /** + * @return string|null + */ + public function getNamePrefix(): ?string + { + return $this->namePrefix; + } + + /** + * @return StructuredPostalAddress|null + */ + public function getStructuredPostalAddress(): ?StructuredPostalAddress + { + return $this->structuredPostalAddress; + } +} diff --git a/src/DocScan/Session/Create/ApplicantProfileBuilder.php b/src/DocScan/Session/Create/ApplicantProfileBuilder.php new file mode 100644 index 00000000..e0c85d3d --- /dev/null +++ b/src/DocScan/Session/Create/ApplicantProfileBuilder.php @@ -0,0 +1,81 @@ +fullName = $fullName; + return $this; + } + + /** + * @param string $dateOfBirth + * @return $this + */ + public function withDateOfBirth(string $dateOfBirth): self + { + $this->dateOfBirth = $dateOfBirth; + return $this; + } + + /** + * @param string $namePrefix + * @return $this + */ + public function withNamePrefix(string $namePrefix): self + { + $this->namePrefix = $namePrefix; + return $this; + } + + /** + * @param StructuredPostalAddress $structuredPostalAddress + * @return $this + */ + public function withStructuredPostalAddress(StructuredPostalAddress $structuredPostalAddress): self + { + $this->structuredPostalAddress = $structuredPostalAddress; + return $this; + } + + /** + * @return ApplicantProfile + */ + public function build(): ApplicantProfile + { + return new ApplicantProfile( + $this->fullName, + $this->dateOfBirth, + $this->namePrefix, + $this->structuredPostalAddress + ); + } +} diff --git a/src/DocScan/Session/Create/Check/RequestedLivenessConfig.php b/src/DocScan/Session/Create/Check/RequestedLivenessConfig.php index 050655db..07db4fea 100644 --- a/src/DocScan/Session/Create/Check/RequestedLivenessConfig.php +++ b/src/DocScan/Session/Create/Check/RequestedLivenessConfig.php @@ -23,7 +23,7 @@ class RequestedLivenessConfig implements RequestedCheckConfigInterface */ private $manualCheck; - public function __construct(string $livenessType, int $maxRetries, string $manualCheck = null) + public function __construct(string $livenessType, int $maxRetries, ?string $manualCheck = null) { $this->livenessType = $livenessType; $this->maxRetries = $maxRetries; diff --git a/src/DocScan/Session/Create/ImportTokenBuilder.php b/src/DocScan/Session/Create/ImportTokenBuilder.php index 84340213..b40dd9bf 100644 --- a/src/DocScan/Session/Create/ImportTokenBuilder.php +++ b/src/DocScan/Session/Create/ImportTokenBuilder.php @@ -10,7 +10,7 @@ class ImportTokenBuilder private int $ttl; - public function withTtl(int $ttl = null): ImportTokenBuilder + public function withTtl(?int $ttl = null): ImportTokenBuilder { $this->ttl = $ttl ?? self::DEFAULT_TTL; diff --git a/src/DocScan/Session/Create/ResourceCreationContainer.php b/src/DocScan/Session/Create/ResourceCreationContainer.php new file mode 100644 index 00000000..ebbbd776 --- /dev/null +++ b/src/DocScan/Session/Create/ResourceCreationContainer.php @@ -0,0 +1,43 @@ +applicantProfile = $applicantProfile; + } + + /** + * @return stdClass + */ + public function jsonSerialize(): stdClass + { + return (object) Json::withoutNullValues([ + 'applicant_profile' => $this->applicantProfile, + ]); + } + + /** + * @return ApplicantProfile|null + */ + public function getApplicantProfile(): ?ApplicantProfile + { + return $this->applicantProfile; + } +} diff --git a/src/DocScan/Session/Create/ResourceCreationContainerBuilder.php b/src/DocScan/Session/Create/ResourceCreationContainerBuilder.php new file mode 100644 index 00000000..9ea9e394 --- /dev/null +++ b/src/DocScan/Session/Create/ResourceCreationContainerBuilder.php @@ -0,0 +1,33 @@ +applicantProfile = $applicantProfile; + return $this; + } + + /** + * @return ResourceCreationContainer + */ + public function build(): ResourceCreationContainer + { + return new ResourceCreationContainer( + $this->applicantProfile + ); + } +} diff --git a/src/DocScan/Session/Create/SdkConfig.php b/src/DocScan/Session/Create/SdkConfig.php index a3c8086d..60b43fb7 100644 --- a/src/DocScan/Session/Create/SdkConfig.php +++ b/src/DocScan/Session/Create/SdkConfig.php @@ -68,6 +68,26 @@ class SdkConfig implements \JsonSerializable */ private $biometricConsentFlow; + /** + * @var string|null + */ + private $darkMode; + + /** + * @var string|null + */ + private $primaryColourDarkMode; + + /** + * @var string|null + */ + private $brandId; + + /** + * @var array|null + */ + private $suppressedScreens; + /** * @param string|null $allowedCaptureMethods * @param string|null $primaryColour @@ -81,6 +101,10 @@ class SdkConfig implements \JsonSerializable * @param bool|null $allowHandoff * @param array|null $idDocumentTextDataExtractionRetriesConfig * @param string|null $biometricConsentFlow + * @param string|null $darkMode + * @param string|null $primaryColourDarkMode + * @param string|null $brandId + * @param array|null $suppressedScreens */ public function __construct( ?string $allowedCaptureMethods, @@ -94,7 +118,11 @@ public function __construct( ?string $privacyPolicyUrl = null, ?bool $allowHandoff = null, ?array $idDocumentTextDataExtractionRetriesConfig = null, - ?string $biometricConsentFlow = null + ?string $biometricConsentFlow = null, + ?string $darkMode = null, + ?string $primaryColourDarkMode = null, + ?string $brandId = null, + ?array $suppressedScreens = null ) { $this->allowedCaptureMethods = $allowedCaptureMethods; $this->primaryColour = $primaryColour; @@ -110,6 +138,10 @@ public function __construct( $this->attemptsConfiguration = new AttemptsConfiguration($idDocumentTextDataExtractionRetriesConfig); } $this->biometricConsentFlow = $biometricConsentFlow; + $this->darkMode = $darkMode; + $this->primaryColourDarkMode = $primaryColourDarkMode; + $this->brandId = $brandId; + $this->suppressedScreens = $suppressedScreens; } /** @@ -129,7 +161,11 @@ public function jsonSerialize(): \stdClass 'privacy_policy_url' => $this->getPrivacyPolicyUrl(), 'allow_handoff' => $this->getAllowHandoff(), 'attempts_configuration' => $this->getAttemptsConfiguration(), - 'biometric_consent_flow' => $this->getBiometricConsentFlow() + 'biometric_consent_flow' => $this->getBiometricConsentFlow(), + 'dark_mode' => $this->getDarkMode(), + 'primary_colour_dark_mode' => $this->getPrimaryColourDarkMode(), + 'brand_id' => $this->getBrandId(), + 'suppressed_screens' => $this->getSuppressedScreens() ]); } @@ -228,4 +264,36 @@ public function getBiometricConsentFlow(): ?string { return $this->biometricConsentFlow; } + + /** + * @return string|null + */ + public function getDarkMode(): ?string + { + return $this->darkMode; + } + + /** + * @return string|null + */ + public function getPrimaryColourDarkMode(): ?string + { + return $this->primaryColourDarkMode; + } + + /** + * @return string|null + */ + public function getBrandId(): ?string + { + return $this->brandId; + } + + /** + * @return array|null + */ + public function getSuppressedScreens(): ?array + { + return $this->suppressedScreens; + } } diff --git a/src/DocScan/Session/Create/SdkConfigBuilder.php b/src/DocScan/Session/Create/SdkConfigBuilder.php index acf30fcc..178fb241 100644 --- a/src/DocScan/Session/Create/SdkConfigBuilder.php +++ b/src/DocScan/Session/Create/SdkConfigBuilder.php @@ -71,6 +71,26 @@ class SdkConfigBuilder */ private $biometricConsentFlow; + /** + * @var string|null + */ + private $darkMode; + + /** + * @var string|null + */ + private $primaryColourDarkMode; + + /** + * @var string|null + */ + private $brandId; + + /** + * @var array|null + */ + private $suppressedScreens; + public function withAllowsCamera(): self { return $this->withAllowedCaptureMethod(self::CAMERA); @@ -146,6 +166,7 @@ public function withBiometricConsentFlow(string $biometricConsentFlow): self $this->biometricConsentFlow = $biometricConsentFlow; return $this; } + /** * Allows configuring the number of attempts permitted for text extraction on an ID document * @@ -199,6 +220,68 @@ public function withIdDocumentTextExtractionGenericAttempts(int $genericRetries) return $this; } + public function withDarkMode(string $darkMode): self + { + $this->darkMode = $darkMode; + return $this; + } + + public function withDarkModeOn(): self + { + $this->darkMode = "ON"; + return $this; + } + + public function withDarkModeOff(): self + { + $this->darkMode = "OFF"; + return $this; + } + + public function withDarkModeAuto(): self + { + $this->darkMode = "AUTO"; + return $this; + } + + public function withPrimaryColourDarkMode(string $primaryColourDarkMode): self + { + $this->primaryColourDarkMode = $primaryColourDarkMode; + return $this; + } + + public function withBrandId(string $brandId): self + { + $this->brandId = $brandId; + return $this; + } + + /** + * Sets the suppressed screens array for configuration + * + * @param array $suppressedScreens Array of screen identifiers to suppress + * @return $this + */ + public function withSuppressedScreens(array $suppressedScreens): self + { + $this->suppressedScreens = $suppressedScreens; + return $this; + } + + /** + * Adds a single screen to the suppressed screens list + * + * @param string $screenIdentifier The screen identifier to suppress + * @return $this + */ + public function withSuppressedScreen(string $screenIdentifier): self + { + if ($this->suppressedScreens === null) { + $this->suppressedScreens = []; + } + $this->suppressedScreens[] = $screenIdentifier; + return $this; + } public function build(): SdkConfig { @@ -214,7 +297,11 @@ public function build(): SdkConfig $this->privacyPolicyUrl, $this->allowHandoff, $this->idDocumentTextDataExtractionRetriesConfig, - $this->biometricConsentFlow + $this->biometricConsentFlow, + $this->darkMode, + $this->primaryColourDarkMode, + $this->brandId, + $this->suppressedScreens ); } } diff --git a/src/DocScan/Session/Create/SessionSpecification.php b/src/DocScan/Session/Create/SessionSpecification.php index 46ab1f4a..060245d7 100644 --- a/src/DocScan/Session/Create/SessionSpecification.php +++ b/src/DocScan/Session/Create/SessionSpecification.php @@ -78,6 +78,11 @@ class SessionSpecification implements JsonSerializable */ private $identityProfileRequirements; + /** + * @var object|null + */ + private $advancedIdentityProfileRequirements; + private ?bool $createIdentityProfilePreview; /** @@ -85,6 +90,11 @@ class SessionSpecification implements JsonSerializable */ private $importToken; + /** + * @var ResourceCreationContainer|null + */ + private $resources; + /** * @param int|null $clientSessionTokenTtl * @param string|null $sessionDeadline @@ -99,8 +109,10 @@ class SessionSpecification implements JsonSerializable * @param IbvOptions|null $ibvOptions * @param object|null $subject * @param object|null $identityProfileRequirements + * @param object|null $advancedIdentityProfileRequirements * @param bool|null $createIdentityProfilePreview * @param ImportToken|null $importToken + * @param ResourceCreationContainer|null $resources */ public function __construct( ?int $clientSessionTokenTtl, @@ -116,8 +128,10 @@ public function __construct( ?IbvOptions $ibvOptions = null, ?object $subject = null, ?object $identityProfileRequirements = null, + ?object $advancedIdentityProfileRequirements = null, ?bool $createIdentityProfilePreview = null, - ?ImportToken $importToken = null + ?ImportToken $importToken = null, + ?ResourceCreationContainer $resources = null ) { $this->clientSessionTokenTtl = $clientSessionTokenTtl; $this->sessionDeadline = $sessionDeadline; @@ -132,8 +146,10 @@ public function __construct( $this->ibvOptions = $ibvOptions; $this->subject = $subject; $this->identityProfileRequirements = $identityProfileRequirements; + $this->advancedIdentityProfileRequirements = $advancedIdentityProfileRequirements; $this->createIdentityProfilePreview = $createIdentityProfilePreview; $this->importToken = $importToken; + $this->resources = $resources; } /** @@ -155,8 +171,10 @@ public function jsonSerialize(): stdClass 'ibv_options' => $this->getIbvOptions(), 'subject' => $this->getSubject(), 'identity_profile_requirements' => $this->getIdentityProfileRequirements(), + 'advanced_identity_profile_requirements' => $this->getAdvancedIdentityProfileRequirements(), 'create_identity_profile_preview' => $this->getCreateIdentityProfilePreview(), 'import_token' => $this->getImportToken(), + 'resources' => $this->getResources(), ]); } @@ -269,6 +287,14 @@ public function getIdentityProfileRequirements(): ?object return $this->identityProfileRequirements; } + /** + * @return object|null + */ + public function getAdvancedIdentityProfileRequirements(): ?object + { + return $this->advancedIdentityProfileRequirements; + } + public function getCreateIdentityProfilePreview(): ?bool { return $this->createIdentityProfilePreview; @@ -278,4 +304,12 @@ public function getImportToken(): ?ImportToken { return $this->importToken; } + + /** + * @return ResourceCreationContainer|null + */ + public function getResources(): ?ResourceCreationContainer + { + return $this->resources; + } } diff --git a/src/DocScan/Session/Create/SessionSpecificationBuilder.php b/src/DocScan/Session/Create/SessionSpecificationBuilder.php index bb54de9e..2348a16e 100644 --- a/src/DocScan/Session/Create/SessionSpecificationBuilder.php +++ b/src/DocScan/Session/Create/SessionSpecificationBuilder.php @@ -78,11 +78,21 @@ class SessionSpecificationBuilder */ private $identityProfileRequirements; + /** + * @var object|null + */ + private $advancedIdentityProfileRequirements; + /** * @var ImportToken|null */ private $importToken; + /** + * @var ResourceCreationContainer|null + */ + private $resources; + /** * @var bool */ @@ -254,6 +264,19 @@ public function withIdentityProfileRequirements($identityProfileRequirements): s return $this; } + /** + * Sets the Advanced Identity Profile Requirements for the session + * + * @param object $advancedIdentityProfileRequirements + * + * @return $this + */ + public function withAdvancedIdentityProfileRequirements($advancedIdentityProfileRequirements): self + { + $this->advancedIdentityProfileRequirements = $advancedIdentityProfileRequirements; + return $this; + } + /** * @return $this */ @@ -274,6 +297,19 @@ public function withImportToken($importToken): self return $this; } + /** + * Sets the resources for the session + * + * @param ResourceCreationContainer $resources + * + * @return $this + */ + public function withResources(ResourceCreationContainer $resources): self + { + $this->resources = $resources; + return $this; + } + /** * @return SessionSpecification */ @@ -293,8 +329,10 @@ public function build(): SessionSpecification $this->ibvOptions, $this->subject, $this->identityProfileRequirements, + $this->advancedIdentityProfileRequirements, $this->createIdentityProfilePreview, $this->importToken, + $this->resources, ); } } diff --git a/src/DocScan/Session/Create/StructuredPostalAddress.php b/src/DocScan/Session/Create/StructuredPostalAddress.php new file mode 100644 index 00000000..bb915e4f --- /dev/null +++ b/src/DocScan/Session/Create/StructuredPostalAddress.php @@ -0,0 +1,163 @@ +addressFormat = $addressFormat; + $this->buildingNumber = $buildingNumber; + $this->addressLine1 = $addressLine1; + $this->townCity = $townCity; + $this->postalCode = $postalCode; + $this->countryIso = $countryIso; + $this->country = $country; + $this->formattedAddress = $formattedAddress; + } + + /** + * @return stdClass + */ + public function jsonSerialize(): stdClass + { + return (object) Json::withoutNullValues([ + 'address_format' => $this->addressFormat, + 'building_number' => $this->buildingNumber, + 'address_line1' => $this->addressLine1, + 'town_city' => $this->townCity, + 'postal_code' => $this->postalCode, + 'country_iso' => $this->countryIso, + 'country' => $this->country, + 'formatted_address' => $this->formattedAddress, + ]); + } + + /** + * @return int|null + */ + public function getAddressFormat(): ?int + { + return $this->addressFormat; + } + + /** + * @return string|null + */ + public function getBuildingNumber(): ?string + { + return $this->buildingNumber; + } + + /** + * @return string|null + */ + public function getAddressLine1(): ?string + { + return $this->addressLine1; + } + + /** + * @return string|null + */ + public function getTownCity(): ?string + { + return $this->townCity; + } + + /** + * @return string|null + */ + public function getPostalCode(): ?string + { + return $this->postalCode; + } + + /** + * @return string|null + */ + public function getCountryIso(): ?string + { + return $this->countryIso; + } + + /** + * @return string|null + */ + public function getCountry(): ?string + { + return $this->country; + } + + /** + * @return string|null + */ + public function getFormattedAddress(): ?string + { + return $this->formattedAddress; + } +} diff --git a/src/DocScan/Session/Create/StructuredPostalAddressBuilder.php b/src/DocScan/Session/Create/StructuredPostalAddressBuilder.php new file mode 100644 index 00000000..3750c25f --- /dev/null +++ b/src/DocScan/Session/Create/StructuredPostalAddressBuilder.php @@ -0,0 +1,145 @@ +addressFormat = $addressFormat; + return $this; + } + + /** + * @param string $buildingNumber + * @return $this + */ + public function withBuildingNumber(string $buildingNumber): self + { + $this->buildingNumber = $buildingNumber; + return $this; + } + + /** + * @param string $addressLine1 + * @return $this + */ + public function withAddressLine1(string $addressLine1): self + { + $this->addressLine1 = $addressLine1; + return $this; + } + + /** + * @param string $townCity + * @return $this + */ + public function withTownCity(string $townCity): self + { + $this->townCity = $townCity; + return $this; + } + + /** + * @param string $postalCode + * @return $this + */ + public function withPostalCode(string $postalCode): self + { + $this->postalCode = $postalCode; + return $this; + } + + /** + * @param string $countryIso + * @return $this + */ + public function withCountryIso(string $countryIso): self + { + $this->countryIso = $countryIso; + return $this; + } + + /** + * @param string $country + * @return $this + */ + public function withCountry(string $country): self + { + $this->country = $country; + return $this; + } + + /** + * @param string $formattedAddress + * @return $this + */ + public function withFormattedAddress(string $formattedAddress): self + { + $this->formattedAddress = $formattedAddress; + return $this; + } + + /** + * @return StructuredPostalAddress + */ + public function build(): StructuredPostalAddress + { + return new StructuredPostalAddress( + $this->addressFormat, + $this->buildingNumber, + $this->addressLine1, + $this->townCity, + $this->postalCode, + $this->countryIso, + $this->country, + $this->formattedAddress + ); + } +} diff --git a/src/DocScan/Session/Retrieve/AdvancedIdentityProfilePreviewResponse.php b/src/DocScan/Session/Retrieve/AdvancedIdentityProfilePreviewResponse.php new file mode 100644 index 00000000..ec1f2ce3 --- /dev/null +++ b/src/DocScan/Session/Retrieve/AdvancedIdentityProfilePreviewResponse.php @@ -0,0 +1,27 @@ + $sessionData + * @throws \Yoti\Exception\DateTimeException + */ + public function __construct(array $sessionData) + { + if (isset($sessionData['media'])) { + $this->media = new MediaResponse($sessionData['media']); + } + } + + /** + * @return MediaResponse|null + */ + public function getMedia(): ?MediaResponse + { + return $this->media; + } +} diff --git a/src/DocScan/Session/Retrieve/AdvancedIdentityProfileResponse.php b/src/DocScan/Session/Retrieve/AdvancedIdentityProfileResponse.php new file mode 100644 index 00000000..5e70b5f3 --- /dev/null +++ b/src/DocScan/Session/Retrieve/AdvancedIdentityProfileResponse.php @@ -0,0 +1,77 @@ + $sessionData + */ + public function __construct(array $sessionData) + { + $this->subjectId = $sessionData['subject_id'] ?? ''; + $this->result = $sessionData['result']; + + if (isset($sessionData['failure_reason'])) { + $this->failureReason = new FailureReasonResponse($sessionData['failure_reason']); + } + + if (isset($sessionData['identity_profile_report'])) { + $this->identityProfileReport = (object)$sessionData['identity_profile_report']; + } + } + + /** + * @return string + */ + public function getSubjectId(): string + { + return $this->subjectId; + } + + /** + * @return string + */ + public function getResult(): string + { + return $this->result; + } + + /** + * @return FailureReasonResponse|null + */ + public function getFailureReason(): ?FailureReasonResponse + { + return $this->failureReason; + } + + /** + * @return object|null + */ + public function getIdentityProfileReport(): ?object + { + return $this->identityProfileReport; + } +} diff --git a/src/DocScan/Session/Retrieve/ApplicantProfileResourceResponse.php b/src/DocScan/Session/Retrieve/ApplicantProfileResourceResponse.php new file mode 100644 index 00000000..35dc108b --- /dev/null +++ b/src/DocScan/Session/Retrieve/ApplicantProfileResourceResponse.php @@ -0,0 +1,70 @@ + $applicantProfile + * @throws DateTimeException + */ + public function __construct(array $applicantProfile) + { + parent::__construct($applicantProfile); + + if (isset($applicantProfile['media'])) { + $this->media = new MediaResponse($applicantProfile['media']); + } + + $this->createdAt = isset($applicantProfile['created_at']) + ? DateTime::stringToDateTime($applicantProfile['created_at']) : null; + + $this->lastUpdated = isset($applicantProfile['last_updated']) + ? DateTime::stringToDateTime($applicantProfile['last_updated']) : null; + } + + /** + * @return MediaResponse|null + */ + public function getMedia(): ?MediaResponse + { + return $this->media; + } + + /** + * @return \DateTime|null + */ + public function getCreatedAt(): ?\DateTime + { + return $this->createdAt; + } + + /** + * @return \DateTime|null + */ + public function getLastUpdated(): ?\DateTime + { + return $this->lastUpdated; + } +} diff --git a/src/DocScan/Session/Retrieve/BreakdownResponse.php b/src/DocScan/Session/Retrieve/BreakdownResponse.php index 8e9ba9c3..410e635c 100644 --- a/src/DocScan/Session/Retrieve/BreakdownResponse.php +++ b/src/DocScan/Session/Retrieve/BreakdownResponse.php @@ -16,6 +16,11 @@ class BreakdownResponse */ private $result; + /** + * @var string|null + */ + private $process; + /** * @var DetailsResponse[] */ @@ -29,6 +34,7 @@ public function __construct(array $breakdown) { $this->subCheck = $breakdown['sub_check'] ?? null; $this->result = $breakdown['result'] ?? null; + $this->process = $breakdown['process'] ?? null; if (isset($breakdown['details'])) { foreach ($breakdown['details'] as $detail) { @@ -53,6 +59,14 @@ public function getResult(): ?string return $this->result; } + /** + * @return string|null + */ + public function getProcess(): ?string + { + return $this->process; + } + /** * @return DetailsResponse[] */ diff --git a/src/DocScan/Session/Retrieve/Configuration/SessionConfigurationResponse.php b/src/DocScan/Session/Retrieve/Configuration/SessionConfigurationResponse.php index 3f935a80..2b3f16d0 100644 --- a/src/DocScan/Session/Retrieve/Configuration/SessionConfigurationResponse.php +++ b/src/DocScan/Session/Retrieve/Configuration/SessionConfigurationResponse.php @@ -26,6 +26,11 @@ class SessionConfigurationResponse */ private $capture; + /** + * @var array|null + */ + private $sdkConfig; + /** * @param array $sessionData */ @@ -35,6 +40,7 @@ public function __construct(array $sessionData) $this->sessionId = $sessionData['session_id'] ?? null; $this->requestedChecks = $sessionData['requested_checks'] ?? null; $this->capture = isset($sessionData['capture']) ? new CaptureResponse($sessionData['capture']) : null; + $this->sdkConfig = $sessionData['sdk_config'] ?? null; } /** @@ -79,4 +85,27 @@ public function getCapture(): ?CaptureResponse { return $this->capture; } + + /** + * Returns the SDK configuration for the session + * + * @return array|null + */ + public function getSdkConfig(): ?array + { + return $this->sdkConfig; + } + + /** + * Returns the suppressed screens configuration if present in the SDK config + * + * @return array|null + */ + public function getSuppressedScreens(): ?array + { + if ($this->sdkConfig === null) { + return null; + } + return $this->sdkConfig['suppressed_screens'] ?? null; + } } diff --git a/src/DocScan/Session/Retrieve/CustomAccountWatchlistCaSearchConfigResponse.php b/src/DocScan/Session/Retrieve/CustomAccountWatchlistCaSearchConfigResponse.php index efa7819d..6b95fd6c 100644 --- a/src/DocScan/Session/Retrieve/CustomAccountWatchlistCaSearchConfigResponse.php +++ b/src/DocScan/Session/Retrieve/CustomAccountWatchlistCaSearchConfigResponse.php @@ -35,7 +35,9 @@ public function __construct(array $searchConfig) $this->apiKey = $searchConfig['api_key']; $this->monitoring = $searchConfig['monitoring']; $this->clientRef = $searchConfig['client_ref']; - $this->tags = array_key_exists('tags', $searchConfig) ? json_decode($searchConfig['tags'], true) : []; + $this->tags = array_key_exists('tags', $searchConfig) && is_string($searchConfig['tags']) + ? json_decode($searchConfig['tags'], true) + : (array_key_exists('tags', $searchConfig) && is_array($searchConfig['tags']) ? $searchConfig['tags'] : []); } /** diff --git a/src/DocScan/Session/Retrieve/GetSessionResult.php b/src/DocScan/Session/Retrieve/GetSessionResult.php index 857e3263..16e3f788 100644 --- a/src/DocScan/Session/Retrieve/GetSessionResult.php +++ b/src/DocScan/Session/Retrieve/GetSessionResult.php @@ -53,6 +53,10 @@ class GetSessionResult private ?IdentityProfilePreviewResponse $identityProfilePreview; + private ?AdvancedIdentityProfileResponse $advancedIdentityProfile = null; + + private ?AdvancedIdentityProfilePreviewResponse $advancedIdentityProfilePreview = null; + private ?ImportTokenResponse $importToken; /** @@ -93,6 +97,18 @@ public function __construct(array $sessionData) ); } + if (isset($sessionData['advanced_identity_profile'])) { + $this->advancedIdentityProfile = new AdvancedIdentityProfileResponse( + $sessionData['advanced_identity_profile'] + ); + } + + if (isset($sessionData['advanced_identity_profile_preview'])) { + $this->advancedIdentityProfilePreview = new AdvancedIdentityProfilePreviewResponse( + $sessionData['advanced_identity_profile_preview'] + ); + } + if (isset($sessionData['import_token'])) { $this->importToken = new ImportTokenResponse($sessionData['import_token']); } @@ -325,6 +341,20 @@ public function getIdentityProfilePreview(): ?IdentityProfilePreviewResponse return $this->identityProfilePreview; } + public function getAdvancedIdentityProfile(): ?AdvancedIdentityProfileResponse + { + if (isset($this->advancedIdentityProfile)) { + return $this->advancedIdentityProfile; + } else { + return null; + } + } + + public function getAdvancedIdentityProfilePreview(): ?AdvancedIdentityProfilePreviewResponse + { + return $this->advancedIdentityProfilePreview; + } + public function getImportToken(): ?ImportTokenResponse { return $this->importToken; diff --git a/src/DocScan/Session/Retrieve/PageResponse.php b/src/DocScan/Session/Retrieve/PageResponse.php index 163b9ecf..eb3e6c45 100644 --- a/src/DocScan/Session/Retrieve/PageResponse.php +++ b/src/DocScan/Session/Retrieve/PageResponse.php @@ -21,6 +21,11 @@ class PageResponse */ private $frames = []; + /** + * @var string[] + */ + private $extractionImageIds = []; + /** * PageInfo constructor. * @param array $page @@ -38,6 +43,8 @@ public function __construct(array $page) $this->frames[] = new FrameResponse($frame); } } + + $this->extractionImageIds = $page['extraction_image_ids'] ?? []; } /** @@ -63,4 +70,12 @@ public function getFrames(): array { return $this->frames; } + + /** + * @return string[] + */ + public function getExtractionImageIds(): array + { + return $this->extractionImageIds; + } } diff --git a/src/DocScan/Session/Retrieve/ResourceContainer.php b/src/DocScan/Session/Retrieve/ResourceContainer.php index 99b730d5..dd0d07c3 100644 --- a/src/DocScan/Session/Retrieve/ResourceContainer.php +++ b/src/DocScan/Session/Retrieve/ResourceContainer.php @@ -26,6 +26,16 @@ class ResourceContainer */ private $faceCapture = []; + /** + * @var ShareCodeResourceResponse[] + */ + private $shareCodes = []; + + /** + * @var ApplicantProfileResourceResponse[] + */ + private $applicantProfiles = []; + /** * ResourceContainer constructor. * @param array $resources @@ -47,6 +57,14 @@ public function __construct(array $resources) if (isset($resources['face_capture'])) { $this->faceCapture = $this->parseFaceCapture($resources['face_capture']); } + + if (isset($resources['share_codes'])) { + $this->shareCodes = $this->parseShareCodes($resources['share_codes']); + } + + if (isset($resources['applicant_profiles'])) { + $this->applicantProfiles = $this->parseApplicantProfiles($resources['applicant_profiles']); + } } /** @@ -161,6 +179,48 @@ public function getFaceCapture(): array return $this->faceCapture; } + /** + * @return ShareCodeResourceResponse[] + */ + public function getShareCodes(): array + { + return $this->shareCodes; + } + + /** + * @return ApplicantProfileResourceResponse[] + */ + public function getApplicantProfiles(): array + { + return $this->applicantProfiles; + } + + /** + * @param array> $shareCodes + * @return ShareCodeResourceResponse[] + */ + private function parseShareCodes(array $shareCodes): array + { + $parsedShareCodes = []; + foreach ($shareCodes as $shareCode) { + $parsedShareCodes[] = new ShareCodeResourceResponse($shareCode); + } + return $parsedShareCodes; + } + + /** + * @param array> $applicantProfiles + * @return ApplicantProfileResourceResponse[] + */ + private function parseApplicantProfiles(array $applicantProfiles): array + { + $parsedApplicantProfiles = []; + foreach ($applicantProfiles as $applicantProfile) { + $parsedApplicantProfiles[] = new ApplicantProfileResourceResponse($applicantProfile); + } + return $parsedApplicantProfiles; + } + /** * @param string $class * @return mixed[] diff --git a/src/DocScan/Session/Retrieve/ResourceResponse.php b/src/DocScan/Session/Retrieve/ResourceResponse.php index aa7de1f3..973e0918 100644 --- a/src/DocScan/Session/Retrieve/ResourceResponse.php +++ b/src/DocScan/Session/Retrieve/ResourceResponse.php @@ -104,6 +104,8 @@ private function createTaskFromArray(array $task): TaskResponse return new TextExtractionTaskResponse($task); case Constants::SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION: return new SupplementaryDocTextExtractionTaskResponse($task); + case Constants::VERIFY_SHARE_CODE_TASK: + return new VerifyShareCodeTaskResponse($task); default: return new TaskResponse($task); } diff --git a/src/DocScan/Session/Retrieve/ShareCodeMediaResponse.php b/src/DocScan/Session/Retrieve/ShareCodeMediaResponse.php new file mode 100644 index 00000000..9ef99fc3 --- /dev/null +++ b/src/DocScan/Session/Retrieve/ShareCodeMediaResponse.php @@ -0,0 +1,34 @@ + $data + * @throws DateTimeException + */ + public function __construct(array $data) + { + $this->media = isset($data['media']) + ? new MediaResponse($data['media']) + : null; + } + + /** + * @return MediaResponse|null + */ + public function getMedia(): ?MediaResponse + { + return $this->media; + } +} diff --git a/src/DocScan/Session/Retrieve/ShareCodeResourceResponse.php b/src/DocScan/Session/Retrieve/ShareCodeResourceResponse.php new file mode 100644 index 00000000..d7a80ad9 --- /dev/null +++ b/src/DocScan/Session/Retrieve/ShareCodeResourceResponse.php @@ -0,0 +1,130 @@ + $shareCode + * + * @throws DateTimeException + */ + public function __construct(array $shareCode) + { + parent::__construct($shareCode); + + $this->createdAt = isset($shareCode['created_at']) ? + DateTime::stringToDateTime($shareCode['created_at']) : null; + $this->lastUpdated = isset($shareCode['last_updated']) ? + DateTime::stringToDateTime($shareCode['last_updated']) : null; + + $this->lookupProfile = isset($shareCode['lookup_profile']) + ? new ShareCodeMediaResponse($shareCode['lookup_profile']) + : null; + + $this->returnedProfile = isset($shareCode['returned_profile']) + ? new ShareCodeMediaResponse($shareCode['returned_profile']) + : null; + + $this->idPhoto = isset($shareCode['id_photo']) + ? new ShareCodeMediaResponse($shareCode['id_photo']) + : null; + + $this->file = isset($shareCode['file']) + ? new ShareCodeMediaResponse($shareCode['file']) + : null; + } + + /** + * @return \DateTime|null + */ + public function getCreatedAt(): ?\DateTime + { + return $this->createdAt; + } + + /** + * @return \DateTime|null + */ + public function getLastUpdated(): ?\DateTime + { + return $this->lastUpdated; + } + + /** + * @return ShareCodeMediaResponse|null + */ + public function getLookupProfile(): ?ShareCodeMediaResponse + { + return $this->lookupProfile; + } + + /** + * @return ShareCodeMediaResponse|null + */ + public function getReturnedProfile(): ?ShareCodeMediaResponse + { + return $this->returnedProfile; + } + + /** + * @return ShareCodeMediaResponse|null + */ + public function getIdPhoto(): ?ShareCodeMediaResponse + { + return $this->idPhoto; + } + + /** + * @return ShareCodeMediaResponse|null + */ + public function getFile(): ?ShareCodeMediaResponse + { + return $this->file; + } + + /** + * @return VerifyShareCodeTaskResponse[] + */ + public function getVerifyShareCodeTasks(): array + { + return $this->filterTasksByType(VerifyShareCodeTaskResponse::class); + } +} diff --git a/src/DocScan/Session/Retrieve/TaskRecommendationReasonResponse.php b/src/DocScan/Session/Retrieve/TaskRecommendationReasonResponse.php new file mode 100644 index 00000000..c365b7cc --- /dev/null +++ b/src/DocScan/Session/Retrieve/TaskRecommendationReasonResponse.php @@ -0,0 +1,44 @@ + $reason + */ + public function __construct(array $reason) + { + $this->value = $reason['value'] ?? null; + $this->detail = $reason['detail'] ?? null; + } + + /** + * @return string|null + */ + public function getValue(): ?string + { + return $this->value; + } + + /** + * @return string|null + */ + public function getDetail(): ?string + { + return $this->detail; + } +} diff --git a/src/DocScan/Session/Retrieve/TaskRecommendationResponse.php b/src/DocScan/Session/Retrieve/TaskRecommendationResponse.php new file mode 100644 index 00000000..9e4ffa72 --- /dev/null +++ b/src/DocScan/Session/Retrieve/TaskRecommendationResponse.php @@ -0,0 +1,47 @@ + $recommendation + */ + public function __construct(array $recommendation) + { + $this->value = $recommendation['value'] ?? null; + + if (isset($recommendation['reason'])) { + $this->reason = new TaskRecommendationReasonResponse($recommendation['reason']); + } + } + + /** + * @return string|null + */ + public function getValue(): ?string + { + return $this->value; + } + + /** + * @return TaskRecommendationReasonResponse|null + */ + public function getReason(): ?TaskRecommendationReasonResponse + { + return $this->reason; + } +} diff --git a/src/DocScan/Session/Retrieve/TaskResponse.php b/src/DocScan/Session/Retrieve/TaskResponse.php index e824cc51..324d8266 100644 --- a/src/DocScan/Session/Retrieve/TaskResponse.php +++ b/src/DocScan/Session/Retrieve/TaskResponse.php @@ -5,6 +5,7 @@ namespace Yoti\DocScan\Session\Retrieve; use Yoti\DocScan\Constants; +use Yoti\Exception\DateTimeException; use Yoti\Util\DateTime; class TaskResponse @@ -44,9 +45,15 @@ class TaskResponse */ private $generatedMedia = []; + /** + * @var TaskRecommendationResponse|null + */ + private $recommendation; + /** * TaskResponse constructor. * @param array $task + * @throws DateTimeException */ public function __construct(array $task) { @@ -67,6 +74,10 @@ public function __construct(array $task) if (isset($task['generated_media'])) { $this->generatedMedia = $this->parseGeneratedMedia($task['generated_media']); } + + if (isset($task['recommendation'])) { + $this->recommendation = new TaskRecommendationResponse($task['recommendation']); + } } /** @@ -161,6 +172,14 @@ public function getGeneratedMedia(): array return $this->generatedMedia; } + /** + * @return TaskRecommendationResponse|null + */ + public function getRecommendation(): ?TaskRecommendationResponse + { + return $this->recommendation; + } + /** * @return GeneratedCheckResponse[] */ diff --git a/src/DocScan/Session/Retrieve/VerifyShareCodeTaskResponse.php b/src/DocScan/Session/Retrieve/VerifyShareCodeTaskResponse.php new file mode 100644 index 00000000..52c629a6 --- /dev/null +++ b/src/DocScan/Session/Retrieve/VerifyShareCodeTaskResponse.php @@ -0,0 +1,9 @@ +formatMessage($message, $response), 0, $previous); diff --git a/src/Http/AuthStrategy/AuthStrategyInterface.php b/src/Http/AuthStrategy/AuthStrategyInterface.php new file mode 100644 index 00000000..5fd1b173 --- /dev/null +++ b/src/Http/AuthStrategy/AuthStrategyInterface.php @@ -0,0 +1,34 @@ + Headers to include in the request + */ + public function createAuthHeaders(string $httpMethod, string $endpoint, ?Payload $payload = null): array; + + /** + * Create query parameters required by this auth strategy. + * + * @return array Query parameters to include in the request + */ + public function createQueryParams(): array; +} diff --git a/src/Http/AuthStrategy/BearerTokenStrategy.php b/src/Http/AuthStrategy/BearerTokenStrategy.php new file mode 100644 index 00000000..2e651b82 --- /dev/null +++ b/src/Http/AuthStrategy/BearerTokenStrategy.php @@ -0,0 +1,51 @@ +authenticationToken = $authenticationToken; + } + + /** + * {@inheritdoc} + */ + public function createAuthHeaders(string $httpMethod, string $endpoint, ?Payload $payload = null): array + { + return [ + 'Authorization' => 'Bearer ' . $this->authenticationToken, + ]; + } + + /** + * {@inheritdoc} + */ + public function createQueryParams(): array + { + return []; + } +} diff --git a/src/Http/AuthStrategy/NoAuthStrategy.php b/src/Http/AuthStrategy/NoAuthStrategy.php new file mode 100644 index 00000000..181ce786 --- /dev/null +++ b/src/Http/AuthStrategy/NoAuthStrategy.php @@ -0,0 +1,33 @@ +pemFile = $pemFile; + $this->sdkId = $sdkId; + } + + /** + * {@inheritdoc} + */ + public function createAuthHeaders(string $httpMethod, string $endpoint, ?Payload $payload = null): array + { + $digest = RequestSigner::sign( + $this->pemFile, + $endpoint, + $httpMethod, + $payload + ); + + return [ + 'X-Yoti-Auth-Digest' => $digest, + ]; + } + + /** + * {@inheritdoc} + */ + public function createQueryParams(): array + { + $params = [ + 'nonce' => self::generateNonce(), + 'timestamp' => (string)(round(microtime(true) * 1000)), + ]; + + if ($this->sdkId !== null) { + $params['sdkId'] = $this->sdkId; + } + + return $params; + } + + /** + * Generate a UUID v4 nonce. + * + * @return string + */ + private static function generateNonce(): string + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff) + ); + } +} diff --git a/src/Http/Exception/NetworkException.php b/src/Http/Exception/NetworkException.php index 0879d9f9..814871f7 100644 --- a/src/Http/Exception/NetworkException.php +++ b/src/Http/Exception/NetworkException.php @@ -19,7 +19,7 @@ class NetworkException extends ClientException implements NetworkExceptionInterf public function __construct( string $message, RequestInterface $request, - \Throwable $previous = null + ?\Throwable $previous = null ) { $this->setRequest($request); parent::__construct($message, 0, $previous); diff --git a/src/Http/Exception/RequestException.php b/src/Http/Exception/RequestException.php index 2ce34d5d..6c3a946c 100644 --- a/src/Http/Exception/RequestException.php +++ b/src/Http/Exception/RequestException.php @@ -19,7 +19,7 @@ class RequestException extends ClientException implements RequestExceptionInterf public function __construct( string $message, RequestInterface $request, - \Throwable $previous = null + ?\Throwable $previous = null ) { $this->setRequest($request); parent::__construct($message, 0, $previous); diff --git a/src/Http/RequestBuilder.php b/src/Http/RequestBuilder.php index 8afff19e..e1922228 100644 --- a/src/Http/RequestBuilder.php +++ b/src/Http/RequestBuilder.php @@ -8,6 +8,8 @@ use GuzzleHttp\Psr7\Utils; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\StreamInterface; +use Yoti\Http\AuthStrategy\AuthStrategyInterface; +use Yoti\Http\AuthStrategy\SignedRequestStrategy; use Yoti\Util\Config; use Yoti\Util\PemFile; @@ -32,6 +34,11 @@ class RequestBuilder */ private $pemFile; + /** + * @var AuthStrategyInterface|null + */ + private $authStrategy; + /** * @var array */ @@ -73,9 +80,9 @@ class RequestBuilder private $multipartEntity; /** - * @param \Yoti\Util\Config $config + * @param \Yoti\Util\Config|null $config */ - public function __construct(Config $config = null) + public function __construct(?Config $config = null) { $this->config = $config ?? new Config(); } @@ -135,6 +142,23 @@ public function withPemString(string $content): self return $this->withPemFile(PemFile::fromString($content)); } + /** + * Set the authentication strategy for this request. + * + * When set, the auth strategy will be used instead of the default + * signed request behavior. If neither authStrategy nor pemFile is set, + * build() will throw an exception. + * + * @param AuthStrategyInterface $authStrategy + * + * @return \Yoti\Http\RequestBuilder + */ + public function withAuthStrategy(AuthStrategyInterface $authStrategy): self + { + $this->authStrategy = $authStrategy; + return $this; + } + /** * @param string $method * @@ -313,32 +337,6 @@ private function validateMethod(): void } } - /** - * @return string - */ - private static function generateNonce(): string - { - return sprintf( - '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', - // 32 bits for "time_low" - mt_rand(0, 0xffff), - mt_rand(0, 0xffff), - // 16 bits for "time_mid" - mt_rand(0, 0xffff), - // 16 bits for "time_hi_and_version", - // four most significant bits holds version number 4 - mt_rand(0, 0x0fff) | 0x4000, - // 16 bits, 8 bits for "clk_seq_hi_res", - // 8 bits for "clk_seq_low", - // two most significant bits holds zero and one for variant DCE1.1 - mt_rand(0, 0x3fff) | 0x8000, - // 48 bits for "node" - mt_rand(0, 0xffff), - mt_rand(0, 0xffff), - mt_rand(0, 0xffff) - ); - } - /** * @return \Yoti\Http\Request * @@ -350,31 +348,38 @@ public function build(): Request throw new \InvalidArgumentException('Base URL must be provided to ' . __CLASS__); } - if (!isset($this->pemFile)) { - throw new \InvalidArgumentException('Pem file must be provided to ' . __CLASS__); - } - $this->validateMethod(); - // Add nonce and timestamp to the URL. - $this - ->withQueryParam('nonce', self::generateNonce()) - ->withQueryParam('timestamp', (string)(round(microtime(true) * 1000))); + // Resolve the auth strategy: + // 1. Explicit authStrategy takes priority + // 2. PemFile present: use legacy SignedRequestStrategy (backward compatible) + // 3. Neither: throw + $authStrategy = $this->resolveAuthStrategy(); - $endpointWithParams = $this->endpoint . '?' . http_build_query($this->queryParams); + // Merge strategy query params with manually set query params. + // Manual params go first to preserve backward-compatible URL ordering. + $strategyQueryParams = $authStrategy->createQueryParams(); + $allQueryParams = array_merge($this->queryParams, $strategyQueryParams); + + $queryString = http_build_query($allQueryParams); + $endpointWithParams = $queryString !== '' ? $this->endpoint . '?' . $queryString : $this->endpoint; $payload = isset($this->multipartEntity) ? Payload::fromStream($this->multipartEntity->createStream()) : $this->payload; - $this->withHeader(self::YOTI_DIGEST_HEADER_KEY, RequestSigner::sign( - $this->pemFile, - $endpointWithParams, + // Get auth headers from strategy. + $authHeaders = $authStrategy->createAuthHeaders( $this->method, + $endpointWithParams, $payload - )); + ); - $url = $this->baseUrl . $endpointWithParams; + // Merge auth headers into manual headers. + foreach ($authHeaders as $name => $value) { + $this->withHeader($name, $value); + } + $url = $this->baseUrl . $endpointWithParams; $message = new RequestMessage( $this->method, @@ -386,6 +391,28 @@ public function build(): Request return new Request($message, $this->client ?? $this->config->getHttpClient()); } + /** + * Resolve the authentication strategy to use. + * + * @return AuthStrategyInterface + * + * @throws \InvalidArgumentException + */ + private function resolveAuthStrategy(): AuthStrategyInterface + { + if (isset($this->authStrategy)) { + return $this->authStrategy; + } + + if (isset($this->pemFile)) { + return new SignedRequestStrategy($this->pemFile); + } + + throw new \InvalidArgumentException( + 'Either an AuthStrategy or a PEM file must be provided to ' . __CLASS__ + ); + } + /** * @return StreamInterface|null */ diff --git a/src/Http/RequestSigner.php b/src/Http/RequestSigner.php index 99bdff79..abf09fe0 100644 --- a/src/Http/RequestSigner.php +++ b/src/Http/RequestSigner.php @@ -26,7 +26,7 @@ public static function sign( PemFile $pemFile, string $endpoint, string $httpMethod, - Payload $payload = null + ?Payload $payload = null ): string { $messageToSign = "{$httpMethod}&$endpoint"; if ($payload instanceof Payload) { diff --git a/src/Identity/Content/ApplicationContent.php b/src/Identity/Content/ApplicationContent.php index 8487fc4a..7c0d267f 100644 --- a/src/Identity/Content/ApplicationContent.php +++ b/src/Identity/Content/ApplicationContent.php @@ -10,7 +10,7 @@ class ApplicationContent private ?ApplicationProfile $profile; private ?ExtraData $extraData; - public function __construct(ApplicationProfile $profile = null, ExtraData $extraData = null) + public function __construct(?ApplicationProfile $profile = null, ?ExtraData $extraData = null) { $this->profile = $profile; $this->extraData = $extraData; diff --git a/src/Identity/Content/Content.php b/src/Identity/Content/Content.php index 9cf61c01..cef3ef37 100644 --- a/src/Identity/Content/Content.php +++ b/src/Identity/Content/Content.php @@ -9,7 +9,7 @@ class Content private ?string $profile; private ?string $extraData; - public function __construct(string $profile = null, string $extraData = null) + public function __construct(?string $profile = null, ?string $extraData = null) { $this->profile = $profile; $this->extraData = $extraData; diff --git a/src/Identity/Content/UserContent.php b/src/Identity/Content/UserContent.php index a32c2dfd..a69e24fd 100644 --- a/src/Identity/Content/UserContent.php +++ b/src/Identity/Content/UserContent.php @@ -10,7 +10,7 @@ class UserContent private ?UserProfile $profile; private ?ExtraData $extraData; - public function __construct(UserProfile $profile = null, ExtraData $extraData = null) + public function __construct(?UserProfile $profile = null, ?ExtraData $extraData = null) { $this->profile = $profile; $this->extraData = $extraData; diff --git a/src/Identity/DigitalIdentityService.php b/src/Identity/DigitalIdentityService.php index 344b18c7..0b68ffe6 100644 --- a/src/Identity/DigitalIdentityService.php +++ b/src/Identity/DigitalIdentityService.php @@ -4,6 +4,7 @@ use Yoti\Constants; use Yoti\Exception\DigitalIdentityException; +use Yoti\Http\AuthStrategy\AuthStrategyInterface; use Yoti\Http\Payload; use Yoti\Http\RequestBuilder; use Yoti\Util\Config; @@ -19,11 +20,25 @@ class DigitalIdentityService private const IDENTITY_SESSION_RECEIPT_RETRIEVAL = '/v2/receipts/%s'; private const IDENTITY_SESSION_RECEIPT_KEY_RETRIEVAL = '/v2/wrapped-item-keys/%s'; - private string $sdkId; + /** + * @var string + */ + private $sdkId; + + /** + * @var PemFile|null + */ + private $pemFile; - private PemFile $pemFile; + /** + * @var AuthStrategyInterface|null + */ + private $authStrategy; - private Config $config; + /** + * @var Config + */ + private $config; public function __construct(string $sdkId, PemFile $pemFile, Config $config) { @@ -32,15 +47,62 @@ public function __construct(string $sdkId, PemFile $pemFile, Config $config) $this->config = $config; } + /** + * Create a DigitalIdentityService instance using an authentication strategy. + * + * When using BearerTokenStrategy (central auth), no sdkId or PEM + * is required since the Bearer token handles authorization. + * + * @param AuthStrategyInterface $authStrategy + * @param Config $config + * + * @return self + */ + public static function withAuthStrategy(AuthStrategyInterface $authStrategy, Config $config): self + { + $instance = new \ReflectionClass(self::class); + $service = $instance->newInstanceWithoutConstructor(); + $service->authStrategy = $authStrategy; + $service->config = $config; + $service->sdkId = ''; + return $service; + } + + /** + * Apply authentication to a RequestBuilder. + * + * If an explicit auth strategy was set, uses it directly. + * Otherwise falls back to the legacy PemFile + X-Yoti-Auth-Id header approach. + * + * @param RequestBuilder $builder + * @param bool $includeAuthId Whether to include X-Yoti-Auth-Id header (legacy mode only) + * + * @return RequestBuilder + */ + private function applyAuth(RequestBuilder $builder, bool $includeAuthId = true): RequestBuilder + { + if ($this->authStrategy !== null) { + return $builder->withAuthStrategy($this->authStrategy); + } + + if ($this->pemFile !== null) { + $builder->withPemFile($this->pemFile); + } + if ($includeAuthId && $this->sdkId !== null && $this->sdkId !== '') { + $builder->withHeader('X-Yoti-Auth-Id', $this->sdkId); + } + return $builder; + } + public function createShareSession(ShareSessionRequest $shareSessionRequest): ShareSessionCreated { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->config->getApiUrl() ?? Constants::DIGITAL_IDENTITY_API_URL) ->withEndpoint(self::IDENTITY_SESSION_CREATION) - ->withHeader('X-Yoti-Auth-Id', $this->sdkId) ->withPost() - ->withPayload(Payload::fromJsonData($shareSessionRequest)) - ->withPemFile($this->pemFile) + ->withPayload(Payload::fromJsonData($shareSessionRequest)); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -54,12 +116,12 @@ public function createShareSession(ShareSessionRequest $shareSessionRequest): Sh public function createShareQrCode(string $sessionId): ShareSessionCreatedQrCode { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->config->getApiUrl() ?? Constants::DIGITAL_IDENTITY_API_URL) ->withEndpoint(sprintf(self::IDENTITY_SESSION_QR_CODE_CREATION, $sessionId)) - ->withHeader('X-Yoti-Auth-Id', $this->sdkId) - ->withPost() - ->withPemFile($this->pemFile) + ->withPost(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -73,12 +135,12 @@ public function createShareQrCode(string $sessionId): ShareSessionCreatedQrCode public function fetchShareQrCode(string $qrCodeId): ShareSessionFetchedQrCode { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->config->getApiUrl() ?? Constants::DIGITAL_IDENTITY_API_URL) ->withEndpoint(sprintf(self::IDENTITY_SESSION_QR_CODE_RETRIEVAL, $qrCodeId)) - ->withHeader('X-Yoti-Auth-Id', $this->sdkId) - ->withGet() - ->withPemFile($this->pemFile) + ->withGet(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -92,12 +154,12 @@ public function fetchShareQrCode(string $qrCodeId): ShareSessionFetchedQrCode public function fetchShareSession(string $sessionId): ShareSessionFetched { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->config->getApiUrl() ?? Constants::DIGITAL_IDENTITY_API_URL) ->withEndpoint(sprintf(self::IDENTITY_SESSION_RETRIEVAL, $sessionId)) - ->withHeader('X-Yoti-Auth-Id', $this->sdkId) - ->withGet() - ->withPemFile($this->pemFile) + ->withGet(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -118,6 +180,13 @@ public function fetchShareReceipt(string $receiptId): Receipt $wrappedReceipt = $this->doFetchShareReceipt($receiptId); if (null === $wrappedReceipt->getError()) { + if ($this->pemFile === null) { + throw new DigitalIdentityException( + 'Cannot decrypt receipt without a PEM file. ' + . 'Receipt decryption is not supported in token-auth mode.' + ); + } + $receiptKey = $this->fetchShareReceiptKey($wrappedReceipt); return $receiptParser->createSuccess($wrappedReceipt, $receiptKey, $this->pemFile); @@ -129,12 +198,12 @@ public function fetchShareReceipt(string $receiptId): Receipt private function doFetchShareReceipt(string $receiptId): WrappedReceipt { $receiptIdUrl = strtr($receiptId, '+/', '-_'); - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->config->getApiUrl() ?? Constants::DIGITAL_IDENTITY_API_URL) ->withEndpoint(sprintf(self::IDENTITY_SESSION_RECEIPT_RETRIEVAL, $receiptIdUrl)) - ->withHeader('X-Yoti-Auth-Id', $this->sdkId) - ->withGet() - ->withPemFile($this->pemFile) + ->withGet(); + + $response = $this->applyAuth($builder) ->build() ->execute(); @@ -148,15 +217,15 @@ private function doFetchShareReceipt(string $receiptId): WrappedReceipt private function fetchShareReceiptKey(WrappedReceipt $wrappedReceipt): ReceiptItemKey { - $response = (new RequestBuilder($this->config)) + $builder = (new RequestBuilder($this->config)) ->withBaseUrl($this->config->getApiUrl() ?? Constants::DIGITAL_IDENTITY_API_URL) ->withEndpoint(sprintf( self::IDENTITY_SESSION_RECEIPT_KEY_RETRIEVAL, $wrappedReceipt->getWrappedItemKeyId() )) - ->withHeader('X-Yoti-Auth-Id', $this->sdkId) - ->withGet() - ->withPemFile($this->pemFile) + ->withGet(); + + $response = $this->applyAuth($builder) ->build() ->execute(); diff --git a/src/Identity/Policy/PolicyBuilder.php b/src/Identity/Policy/PolicyBuilder.php index a3b8f479..8ad89aaa 100644 --- a/src/Identity/Policy/PolicyBuilder.php +++ b/src/Identity/Policy/PolicyBuilder.php @@ -51,8 +51,8 @@ public function withWantedAttribute(WantedAttribute $wantedAttribute): self */ public function withWantedAttributeByName( string $name, - array $constraints = null, - bool $acceptSelfAsserted = null + ?array $constraints = null, + ?bool $acceptSelfAsserted = null ): self { $wantedAttributeBuilder = (new WantedAttributeBuilder()) ->withName($name); @@ -71,7 +71,7 @@ public function withWantedAttributeByName( /** * @param Constraint[]|null $constraints */ - public function withFamilyName(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withFamilyName(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_FAMILY_NAME, @@ -83,7 +83,7 @@ public function withFamilyName(array $constraints = null, bool $acceptSelfAssert /** * @param Constraint[]|null $constraints */ - public function withGivenNames(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withGivenNames(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_GIVEN_NAMES, @@ -95,7 +95,7 @@ public function withGivenNames(array $constraints = null, bool $acceptSelfAssert /** * @param Constraint[]|null $constraints */ - public function withFullName(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withFullName(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_FULL_NAME, @@ -107,7 +107,7 @@ public function withFullName(array $constraints = null, bool $acceptSelfAsserted /** * @param Constraint[]|null $constraints */ - public function withDateOfBirth(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withDateOfBirth(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_DATE_OF_BIRTH, @@ -119,7 +119,7 @@ public function withDateOfBirth(array $constraints = null, bool $acceptSelfAsser /** * @param Constraint[]|null $constraints */ - public function withAgeOver(int $age, array $constraints = null, bool $acceptSelfAsserted = null): self + public function withAgeOver(int $age, ?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withAgeDerivedAttribute( UserProfile::AGE_OVER . $age, @@ -131,7 +131,7 @@ public function withAgeOver(int $age, array $constraints = null, bool $acceptSel /** * @param Constraint[]|null $constraints */ - public function withAgeUnder(int $age, array $constraints = null, bool $acceptSelfAsserted = null): self + public function withAgeUnder(int $age, ?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withAgeDerivedAttribute( UserProfile::AGE_UNDER . $age, @@ -145,8 +145,8 @@ public function withAgeUnder(int $age, array $constraints = null, bool $acceptSe */ public function withAgeDerivedAttribute( string $derivation, - array $constraints = null, - bool $acceptSelfAsserted = null + ?array $constraints = null, + ?bool $acceptSelfAsserted = null ): self { $wantedAttributeBuilder = (new WantedAttributeBuilder()) ->withName(UserProfile::ATTR_DATE_OF_BIRTH) @@ -166,7 +166,7 @@ public function withAgeDerivedAttribute( /** * @param Constraint[]|null $constraints */ - public function withGender(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withGender(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_GENDER, @@ -178,7 +178,7 @@ public function withGender(array $constraints = null, bool $acceptSelfAsserted = /** * @param Constraint[]|null $constraints */ - public function withPostalAddress(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withPostalAddress(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_POSTAL_ADDRESS, @@ -190,7 +190,7 @@ public function withPostalAddress(array $constraints = null, bool $acceptSelfAss /** * @param Constraint[]|null $constraints */ - public function withStructuredPostalAddress(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withStructuredPostalAddress(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_STRUCTURED_POSTAL_ADDRESS, @@ -202,7 +202,7 @@ public function withStructuredPostalAddress(array $constraints = null, bool $acc /** * @param Constraint[]|null $constraints */ - public function withNationality(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withNationality(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_NATIONALITY, @@ -214,7 +214,7 @@ public function withNationality(array $constraints = null, bool $acceptSelfAsser /** * @param Constraint[]|null $constraints */ - public function withPhoneNumber(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withPhoneNumber(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_PHONE_NUMBER, @@ -226,7 +226,7 @@ public function withPhoneNumber(array $constraints = null, bool $acceptSelfAsser /** * @param Constraint[]|null $constraints */ - public function withSelfie(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withSelfie(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_SELFIE, @@ -238,7 +238,7 @@ public function withSelfie(array $constraints = null, bool $acceptSelfAsserted = /** * @param Constraint[]|null $constraints */ - public function withDocumentDetails(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withDocumentDetails(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_DOCUMENT_DETAILS, @@ -250,7 +250,7 @@ public function withDocumentDetails(array $constraints = null, bool $acceptSelfA /** * @param Constraint[]|null $constraints */ - public function withDocumentImages(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withDocumentImages(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_DOCUMENT_IMAGES, @@ -262,7 +262,7 @@ public function withDocumentImages(array $constraints = null, bool $acceptSelfAs /** * @param Constraint[]|null $constraints */ - public function withEmail(array $constraints = null, bool $acceptSelfAsserted = null): self + public function withEmail(?array $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_EMAIL_ADDRESS, diff --git a/src/Identity/Policy/WantedAttribute.php b/src/Identity/Policy/WantedAttribute.php index dc34da79..47005af1 100644 --- a/src/Identity/Policy/WantedAttribute.php +++ b/src/Identity/Policy/WantedAttribute.php @@ -29,10 +29,10 @@ class WantedAttribute implements \JsonSerializable */ public function __construct( string $name, - string $derivation = null, + ?string $derivation = null, bool $optional = false, - bool $acceptSelfAsserted = null, - array $constraints = null + ?bool $acceptSelfAsserted = null, + ?array $constraints = null ) { Validation::notEmptyString($name, 'name'); $this->name = $name; diff --git a/src/Identity/ReceiptBuilder.php b/src/Identity/ReceiptBuilder.php index 0851a088..1ec378f2 100644 --- a/src/Identity/ReceiptBuilder.php +++ b/src/Identity/ReceiptBuilder.php @@ -42,14 +42,14 @@ public function withSessionId(string $sessionId): self return $this; } - public function withRememberMeId(string $rememberMeId = null): self + public function withRememberMeId(?string $rememberMeId = null): self { $this->rememberMeId = $rememberMeId; return $this; } - public function withParentRememberMeId(string $parentRememberMeId = null): self + public function withParentRememberMeId(?string $parentRememberMeId = null): self { $this->parentRememberMeId = $parentRememberMeId; @@ -63,28 +63,28 @@ public function withTimestamp(\DateTime $timestamp): self return $this; } - public function withApplicationContent(ApplicationProfile $profile, ExtraData $extraData = null): self + public function withApplicationContent(ApplicationProfile $profile, ?ExtraData $extraData = null): self { $this->applicationContent = new ApplicationContent($profile, $extraData); return $this; } - public function withUserContent(UserProfile $profile = null, ExtraData $extraData = null): self + public function withUserContent(?UserProfile $profile = null, ?ExtraData $extraData = null): self { $this->userContent = new UserContent($profile, $extraData); return $this; } - public function withError(string $error = null): self + public function withError(?string $error = null): self { $this->error = $error; return $this; } - public function withErrorReason(ErrorReason $errorReason = null): self + public function withErrorReason(?ErrorReason $errorReason = null): self { $this->errorReason = $errorReason; diff --git a/src/Identity/ReceiptParser.php b/src/Identity/ReceiptParser.php index 904ef7b7..39fae9ee 100644 --- a/src/Identity/ReceiptParser.php +++ b/src/Identity/ReceiptParser.php @@ -21,7 +21,7 @@ class ReceiptParser */ private $logger; - public function __construct(LoggerInterface $logger = null) + public function __construct(?LoggerInterface $logger = null) { $this->logger = $logger ?? new Logger(); } diff --git a/src/Profile/Attribute.php b/src/Profile/Attribute.php index 1117f728..39f1d8b2 100644 --- a/src/Profile/Attribute.php +++ b/src/Profile/Attribute.php @@ -36,7 +36,7 @@ class Attribute * @param Anchor[] $anchors * @param string|null $id */ - public function __construct(string $name, $value, array $anchors, string $id = null) + public function __construct(string $name, $value, array $anchors, ?string $id = null) { $this->name = $name; $this->value = $value; diff --git a/src/Profile/ExtraData/AttributeIssuanceDetails.php b/src/Profile/ExtraData/AttributeIssuanceDetails.php index 019350c6..31d6e9ca 100644 --- a/src/Profile/ExtraData/AttributeIssuanceDetails.php +++ b/src/Profile/ExtraData/AttributeIssuanceDetails.php @@ -25,10 +25,10 @@ class AttributeIssuanceDetails /** * @param string $token - * @param \DateTime $expiryDate + * @param \DateTime|null $expiryDate * @param \Yoti\Profile\ExtraData\AttributeDefinition[] $issuingAttributes */ - public function __construct(string $token, \DateTime $expiryDate = null, array $issuingAttributes = []) + public function __construct(string $token, ?\DateTime $expiryDate = null, array $issuingAttributes = []) { $this->token = $token; diff --git a/src/Profile/Util/Attribute/AnchorConverter.php b/src/Profile/Util/Attribute/AnchorConverter.php index a13d6147..93f9b130 100644 --- a/src/Profile/Util/Attribute/AnchorConverter.php +++ b/src/Profile/Util/Attribute/AnchorConverter.php @@ -60,8 +60,31 @@ private static function decodeAnchorValue(string $extEncodedValue): string { $encodedBER = ASN1::extractBER($extEncodedValue); $decodedValArr = ASN1::decodeBER($encodedBER); + if (isset($decodedValArr[0]['content'][0]['content'])) { - return $decodedValArr[0]['content'][0]['content']; + $value = $decodedValArr[0]['content'][0]['content']; + + if (!is_string($value)) { + return ''; + } + + $detectionOrder = mb_detect_order(); + $encoding = mb_detect_encoding($value, is_array($detectionOrder) ? $detectionOrder : null, true); + + if (is_string($encoding)) { + if ($encoding !== 'UTF-8') { + // PHPStan implies $value is string, $encoding is valid string, so result is string. + return mb_convert_encoding($value, 'UTF-8', $encoding); + } + // It is UTF-8 + return $value; + } else { // $encoding is false (detection failed) + if (!mb_check_encoding($value, 'UTF-8')) { + // PHPStan implies $value is string, so result is string. + return mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1'); + } + return $value; // It's valid UTF-8 despite detection failing + } } return ''; } @@ -108,16 +131,28 @@ private static function convertCertToX509(string $certificate): \stdClass $X509 = new X509(); $X509Data = $X509->loadX509($certificate); - /** We need because of new 3.0 version phpseclib @link https://github.com/phpseclib/phpseclib/issues/1738 */ array_walk_recursive($X509Data, function (&$item): void { - if (is_string($item) && mb_detect_encoding($item) != 'ASCII') { - $item = base64_encode($item); + if (is_string($item)) { + $detectionOrder = mb_detect_order(); + $encoding = mb_detect_encoding($item, is_array($detectionOrder) ? $detectionOrder : null, true); + + if (is_string($encoding)) { + if ($encoding !== 'UTF-8' && $encoding !== 'ASCII') { + // PHPStan implies $item is string, $encoding is valid string, so result is string. + // The 'else' branch for base64_encode was deemed unreachable by PHPStan. + $item = mb_convert_encoding($item, 'UTF-8', $encoding); + } + // If $encoding is 'UTF-8' or 'ASCII', $item is left as is. + } else { // $encoding is false (detection failed) + if (!mb_check_encoding($item, 'UTF-8') && !mb_check_encoding($item, 'ASCII')) { + $item = base64_encode($item); + } + // If it's valid UTF-8/ASCII despite detection failing, $item is left as is. + } } }); $decodedX509Data = Json::decode(Json::encode(Json::convertFromLatin1ToUtf8Recursively($X509Data)), false); - // Ensure serial number is cast to string. - // @see \phpseclib\Math\BigInteger::__toString() $decodedX509Data ->tbsCertificate ->serialNumber diff --git a/src/ShareUrl/Policy/DynamicPolicyBuilder.php b/src/ShareUrl/Policy/DynamicPolicyBuilder.php index 4d3ba083..f7a1164b 100644 --- a/src/ShareUrl/Policy/DynamicPolicyBuilder.php +++ b/src/ShareUrl/Policy/DynamicPolicyBuilder.php @@ -77,8 +77,8 @@ public function withWantedAttribute(WantedAttribute $wantedAttribute): self */ public function withWantedAttributeByName( string $name, - Constraints $constraints = null, - bool $acceptSelfAsserted = null + ?Constraints $constraints = null, + ?bool $acceptSelfAsserted = null ): self { $wantedAttributeBuilder = (new WantedAttributeBuilder()) ->withName($name); @@ -100,7 +100,7 @@ public function withWantedAttributeByName( * * @return $this */ - public function withFamilyName(Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withFamilyName(?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_FAMILY_NAME, @@ -115,7 +115,7 @@ public function withFamilyName(Constraints $constraints = null, bool $acceptSelf * * @return self */ - public function withGivenNames(Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withGivenNames(?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_GIVEN_NAMES, @@ -130,7 +130,7 @@ public function withGivenNames(Constraints $constraints = null, bool $acceptSelf * * @return self */ - public function withFullName(Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withFullName(?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_FULL_NAME, @@ -145,7 +145,7 @@ public function withFullName(Constraints $constraints = null, bool $acceptSelfAs * * @return $this */ - public function withDateOfBirth(Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withDateOfBirth(?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_DATE_OF_BIRTH, @@ -161,7 +161,7 @@ public function withDateOfBirth(Constraints $constraints = null, bool $acceptSel * * @return $this */ - public function withAgeOver(int $age, Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withAgeOver(int $age, ?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withAgeDerivedAttribute( UserProfile::AGE_OVER . (string) $age, @@ -177,7 +177,7 @@ public function withAgeOver(int $age, Constraints $constraints = null, bool $acc * * @return $this */ - public function withAgeUnder(int $age, Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withAgeUnder(int $age, ?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withAgeDerivedAttribute( UserProfile::AGE_UNDER . (string) $age, @@ -195,8 +195,8 @@ public function withAgeUnder(int $age, Constraints $constraints = null, bool $ac */ public function withAgeDerivedAttribute( string $derivation, - Constraints $constraints = null, - bool $acceptSelfAsserted = null + ?Constraints $constraints = null, + ?bool $acceptSelfAsserted = null ): self { $wantedAttributeBuilder = (new WantedAttributeBuilder()) ->withName(UserProfile::ATTR_DATE_OF_BIRTH) @@ -216,7 +216,7 @@ public function withAgeDerivedAttribute( * * @return $this */ - public function withGender(Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withGender(?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_GENDER, @@ -231,7 +231,7 @@ public function withGender(Constraints $constraints = null, bool $acceptSelfAsse * * @return $this */ - public function withPostalAddress(Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withPostalAddress(?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_POSTAL_ADDRESS, @@ -246,8 +246,10 @@ public function withPostalAddress(Constraints $constraints = null, bool $acceptS * * @return $this */ - public function withStructuredPostalAddress(Constraints $constraints = null, bool $acceptSelfAsserted = null): self - { + public function withStructuredPostalAddress( + ?Constraints $constraints = null, + ?bool $acceptSelfAsserted = null + ): self { return $this->withWantedAttributeByName( UserProfile::ATTR_STRUCTURED_POSTAL_ADDRESS, $constraints, @@ -261,7 +263,7 @@ public function withStructuredPostalAddress(Constraints $constraints = null, boo * * @return $this */ - public function withNationality(Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withNationality(?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_NATIONALITY, @@ -276,7 +278,7 @@ public function withNationality(Constraints $constraints = null, bool $acceptSel * * @return $this */ - public function withPhoneNumber(Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withPhoneNumber(?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_PHONE_NUMBER, @@ -291,7 +293,7 @@ public function withPhoneNumber(Constraints $constraints = null, bool $acceptSel * * @return $this */ - public function withSelfie(Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withSelfie(?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_SELFIE, @@ -306,7 +308,7 @@ public function withSelfie(Constraints $constraints = null, bool $acceptSelfAsse * * @return $this */ - public function withDocumentDetails(Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withDocumentDetails(?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_DOCUMENT_DETAILS, @@ -321,7 +323,7 @@ public function withDocumentDetails(Constraints $constraints = null, bool $accep * * @return $this */ - public function withDocumentImages(Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withDocumentImages(?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_DOCUMENT_IMAGES, @@ -336,7 +338,7 @@ public function withDocumentImages(Constraints $constraints = null, bool $accept * * @return $this */ - public function withEmail(Constraints $constraints = null, bool $acceptSelfAsserted = null): self + public function withEmail(?Constraints $constraints = null, ?bool $acceptSelfAsserted = null): self { return $this->withWantedAttributeByName( UserProfile::ATTR_EMAIL_ADDRESS, @@ -406,7 +408,7 @@ public function withIdentityProfileRequirements($identityProfileRequirements): s } /** - * Use an Identity Profile Requirement object for the share + * Use an Advanced Identity Profile Requirement object for the share * * @param object $advancedIdentityProfileRequirements * @return $this diff --git a/src/ShareUrl/Policy/WantedAttribute.php b/src/ShareUrl/Policy/WantedAttribute.php index a5b0f2ed..d176070a 100644 --- a/src/ShareUrl/Policy/WantedAttribute.php +++ b/src/ShareUrl/Policy/WantedAttribute.php @@ -32,17 +32,24 @@ class WantedAttribute implements \JsonSerializable */ private $acceptSelfAsserted; + /** + * @var bool|null + */ + private $optional; + /** * @param string $name * @param string $derivation * @param bool $acceptSelfAsserted * @param \Yoti\ShareUrl\Policy\Constraints $constraints + * @param bool $optional */ public function __construct( string $name, - string $derivation = null, - bool $acceptSelfAsserted = null, - Constraints $constraints = null + ?string $derivation = null, + ?bool $acceptSelfAsserted = null, + ?Constraints $constraints = null, + ?bool $optional = null ) { Validation::notEmptyString($name, 'name'); $this->name = $name; @@ -50,6 +57,7 @@ public function __construct( $this->derivation = $derivation; $this->acceptSelfAsserted = $acceptSelfAsserted; $this->constraints = $constraints; + $this->optional = $optional; } /** @@ -97,6 +105,14 @@ public function getAcceptSelfAsserted(): ?bool return $this->acceptSelfAsserted; } + /** + * @return bool|null + */ + public function getOptional(): ?bool + { + return $this->optional; + } + /** * @inheritDoc * @@ -106,7 +122,7 @@ public function jsonSerialize(): array { $json = [ 'name' => $this->getName(), - 'optional' => false, + 'optional' => $this->getOptional(), ]; if ($this->getDerivation() !== null) { @@ -121,6 +137,12 @@ public function jsonSerialize(): array $json['accept_self_asserted'] = $this->getAcceptSelfAsserted(); } + if ($this->getOptional() !== null) { + $json['optional'] = $this->getOptional(); + } + + + return $json; } diff --git a/src/ShareUrl/Policy/WantedAttributeBuilder.php b/src/ShareUrl/Policy/WantedAttributeBuilder.php index 37f381e7..9995cfdb 100644 --- a/src/ShareUrl/Policy/WantedAttributeBuilder.php +++ b/src/ShareUrl/Policy/WantedAttributeBuilder.php @@ -29,6 +29,10 @@ class WantedAttributeBuilder */ private $acceptSelfAsserted; + /** + * @var bool|null + */ + private $optional = false; /** * @param string $name * @@ -73,6 +77,16 @@ public function withAcceptSelfAsserted(?bool $acceptSelfAsserted = true): self return $this; } + /** + * @param bool $optional + * + * @return $this + */ + public function withOptional(?bool $optional = false): self + { + $this->optional = $optional; + return $this; + } /** * @return \Yoti\ShareUrl\Policy\WantedAttribute */ @@ -82,7 +96,8 @@ public function build(): WantedAttribute $this->name, $this->derivation, $this->acceptSelfAsserted, - $this->constraints + $this->constraints, + $this->optional ); } } diff --git a/src/Util/Json.php b/src/Util/Json.php index 8be63f78..81f7bdb7 100644 --- a/src/Util/Json.php +++ b/src/Util/Json.php @@ -65,7 +65,7 @@ private static function validate(): void public static function convertFromLatin1ToUtf8Recursively($dat) { if (is_string($dat)) { - return utf8_encode($dat); + return mb_convert_encoding($dat, 'UTF-8', 'ISO-8859-1'); } elseif (is_array($dat)) { $ret = []; foreach ($dat as $i => $d) { diff --git a/tests/Aml/ResultTest.php b/tests/Aml/ResultTest.php index 76dd42d0..86f83eaf 100644 --- a/tests/Aml/ResultTest.php +++ b/tests/Aml/ResultTest.php @@ -21,6 +21,11 @@ class ResultTest extends TestCase */ public $amlResult; + /** + * @var \PHPUnit\Framework\MockObject\MockObject&\Psr\Http\Message\ResponseInterface + */ + private $responseMock; + public function setup(): void { $this->responseMock = $this->createMock(ResponseInterface::class); diff --git a/tests/Auth/AuthenticationTokenGeneratorTest.php b/tests/Auth/AuthenticationTokenGeneratorTest.php new file mode 100644 index 00000000..7d2e4dd6 --- /dev/null +++ b/tests/Auth/AuthenticationTokenGeneratorTest.php @@ -0,0 +1,54 @@ +assertInstanceOf(\Yoti\Auth\Builder::class, $builder); + } + + /** + * @test + * @covers ::generate + */ + public function shouldThrowOnEmptyScopes() + { + $pemFile = PemFile::fromFilePath(TestData::PEM_FILE); + + $generator = new AuthenticationTokenGenerator( + self::SOME_SDK_ID, + $pemFile, + function () { + return self::SOME_JWT_ID; + }, + self::SOME_AUTH_URL + ); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('scopes must not be empty'); + + $generator->generate([]); + } +} diff --git a/tests/Auth/BuilderTest.php b/tests/Auth/BuilderTest.php new file mode 100644 index 00000000..d1fbb157 --- /dev/null +++ b/tests/Auth/BuilderTest.php @@ -0,0 +1,139 @@ +withSdkId(self::SOME_SDK_ID) + ->withPemFile($pemFile) + ->build(); + + $this->assertInstanceOf(AuthenticationTokenGenerator::class, $generator); + } + + /** + * @test + * @covers ::withSdkId + * @covers ::withPemFilePath + * @covers ::build + */ + public function shouldBuildWithPemFilePath() + { + $generator = (new Builder()) + ->withSdkId(self::SOME_SDK_ID) + ->withPemFilePath(TestData::PEM_FILE) + ->build(); + + $this->assertInstanceOf(AuthenticationTokenGenerator::class, $generator); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenSdkIdIsEmpty() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("'sdkId' must not be empty or null"); + + $pemFile = PemFile::fromFilePath(TestData::PEM_FILE); + + (new Builder()) + ->withSdkId('') + ->withPemFile($pemFile) + ->build(); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenSdkIdIsMissing() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("'sdkId' must not be empty or null"); + + $pemFile = PemFile::fromFilePath(TestData::PEM_FILE); + + (new Builder()) + ->withPemFile($pemFile) + ->build(); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenPemFileIsMissing() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("'pemFile' must not be null"); + + (new Builder()) + ->withSdkId(self::SOME_SDK_ID) + ->build(); + } + + /** + * @test + * @covers ::withJwtIdSupplier + * @covers ::build + */ + public function shouldAcceptCustomJwtIdSupplier() + { + $pemFile = PemFile::fromFilePath(TestData::PEM_FILE); + + $generator = (new Builder()) + ->withSdkId(self::SOME_SDK_ID) + ->withPemFile($pemFile) + ->withJwtIdSupplier(function () { + return 'custom-jwt-id'; + }) + ->build(); + + $this->assertInstanceOf(AuthenticationTokenGenerator::class, $generator); + } + + /** + * @test + * @covers ::withAuthApiUrl + * @covers ::build + */ + public function shouldAcceptCustomAuthApiUrl() + { + $pemFile = PemFile::fromFilePath(TestData::PEM_FILE); + + $generator = (new Builder()) + ->withSdkId(self::SOME_SDK_ID) + ->withPemFile($pemFile) + ->withAuthApiUrl(self::SOME_AUTH_URL) + ->build(); + + $this->assertInstanceOf(AuthenticationTokenGenerator::class, $generator); + } +} diff --git a/tests/Auth/CreateAuthenticationTokenResponseTest.php b/tests/Auth/CreateAuthenticationTokenResponseTest.php new file mode 100644 index 00000000..bb821b0d --- /dev/null +++ b/tests/Auth/CreateAuthenticationTokenResponseTest.php @@ -0,0 +1,91 @@ + self::SOME_ACCESS_TOKEN, + 'token_type' => self::SOME_TOKEN_TYPE, + 'expires_in' => self::SOME_EXPIRES_IN, + 'scope' => self::SOME_SCOPE, + ]); + + $this->assertEquals(self::SOME_ACCESS_TOKEN, $response->getAccessToken()); + $this->assertEquals(self::SOME_TOKEN_TYPE, $response->getTokenType()); + $this->assertEquals(self::SOME_EXPIRES_IN, $response->getExpiresIn()); + $this->assertEquals(self::SOME_SCOPE, $response->getScope()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getExpiresIn + * @covers ::getScope + */ + public function shouldHandleMissingOptionalFields() + { + $response = new CreateAuthenticationTokenResponse([ + 'access_token' => self::SOME_ACCESS_TOKEN, + 'token_type' => self::SOME_TOKEN_TYPE, + ]); + + $this->assertEquals(self::SOME_ACCESS_TOKEN, $response->getAccessToken()); + $this->assertEquals(self::SOME_TOKEN_TYPE, $response->getTokenType()); + $this->assertNull($response->getExpiresIn()); + $this->assertNull($response->getScope()); + } + + /** + * @test + * @covers ::__construct + */ + public function shouldHandleEmptyResponseData() + { + $response = new CreateAuthenticationTokenResponse([]); + + $this->assertEquals('', $response->getAccessToken()); + $this->assertEquals('', $response->getTokenType()); + $this->assertNull($response->getExpiresIn()); + $this->assertNull($response->getScope()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getExpiresIn + */ + public function shouldCastExpiresInToInteger() + { + $response = new CreateAuthenticationTokenResponse([ + 'access_token' => self::SOME_ACCESS_TOKEN, + 'token_type' => self::SOME_TOKEN_TYPE, + 'expires_in' => '7200', + ]); + + $this->assertSame(7200, $response->getExpiresIn()); + } +} diff --git a/tests/DigitalIdentityClientBuilderTest.php b/tests/DigitalIdentityClientBuilderTest.php new file mode 100644 index 00000000..abd741d2 --- /dev/null +++ b/tests/DigitalIdentityClientBuilderTest.php @@ -0,0 +1,131 @@ +withClientSdkId(self::SOME_SDK_ID) + ->withPemFilePath(TestData::PEM_FILE) + ->build(); + + $this->assertInstanceOf(DigitalIdentityClient::class, $client); + } + + /** + * @test + * @covers ::withAuthenticationToken + * @covers ::build + */ + public function shouldBuildWithAuthenticationToken() + { + $client = DigitalIdentityClient::builder() + ->withAuthenticationToken(self::SOME_AUTH_TOKEN) + ->build(); + + $this->assertInstanceOf(DigitalIdentityClient::class, $client); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenAuthTokenSetWithSdkId() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Must not supply sdkId or PEM file when using an authentication token'); + + DigitalIdentityClient::builder() + ->withAuthenticationToken(self::SOME_AUTH_TOKEN) + ->withClientSdkId(self::SOME_SDK_ID) + ->build(); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenAuthTokenSetWithPem() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Must not supply sdkId or PEM file when using an authentication token'); + + DigitalIdentityClient::builder() + ->withAuthenticationToken(self::SOME_AUTH_TOKEN) + ->withPemFilePath(TestData::PEM_FILE) + ->build(); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenNoSdkIdForSignedRequest() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An sdkId and PEM file must be provided when not using an authentication token'); + + DigitalIdentityClient::builder() + ->withPemFilePath(TestData::PEM_FILE) + ->build(); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenNoPemForSignedRequest() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An sdkId and PEM file must be provided when not using an authentication token'); + + DigitalIdentityClient::builder() + ->withClientSdkId(self::SOME_SDK_ID) + ->build(); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenNothingProvided() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An sdkId and PEM file must be provided when not using an authentication token'); + + DigitalIdentityClient::builder() + ->build(); + } + + /** + * @test + * @covers ::withAuthenticationToken + * @covers ::build + */ + public function shouldThrowOnEmptyAuthToken() + { + $this->expectException(\InvalidArgumentException::class); + + DigitalIdentityClient::builder() + ->withAuthenticationToken('') + ->build(); + } +} diff --git a/tests/DocScan/DocScanClientBuilderTest.php b/tests/DocScan/DocScanClientBuilderTest.php new file mode 100644 index 00000000..826c0952 --- /dev/null +++ b/tests/DocScan/DocScanClientBuilderTest.php @@ -0,0 +1,149 @@ +withClientSdkId(self::SOME_SDK_ID) + ->withPemFilePath(TestData::PEM_FILE) + ->build(); + + $this->assertInstanceOf(DocScanClient::class, $client); + } + + /** + * @test + * @covers ::withAuthenticationToken + * @covers ::build + */ + public function shouldBuildWithAuthenticationToken() + { + $client = DocScanClient::builder() + ->withAuthenticationToken(self::SOME_AUTH_TOKEN) + ->build(); + + $this->assertInstanceOf(DocScanClient::class, $client); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenAuthTokenSetWithSdkId() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Must not supply sdkId or PEM file when using an authentication token'); + + DocScanClient::builder() + ->withAuthenticationToken(self::SOME_AUTH_TOKEN) + ->withClientSdkId(self::SOME_SDK_ID) + ->build(); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenAuthTokenSetWithPem() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Must not supply sdkId or PEM file when using an authentication token'); + + DocScanClient::builder() + ->withAuthenticationToken(self::SOME_AUTH_TOKEN) + ->withPemFilePath(TestData::PEM_FILE) + ->build(); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenNoSdkIdForSignedRequest() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An sdkId and PEM file must be provided when not using an authentication token'); + + DocScanClient::builder() + ->withPemFilePath(TestData::PEM_FILE) + ->build(); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenNoPemForSignedRequest() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An sdkId and PEM file must be provided when not using an authentication token'); + + DocScanClient::builder() + ->withClientSdkId(self::SOME_SDK_ID) + ->build(); + } + + /** + * @test + * @covers ::build + */ + public function shouldThrowWhenNothingProvided() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An sdkId and PEM file must be provided when not using an authentication token'); + + DocScanClient::builder() + ->build(); + } + + /** + * @test + * @covers ::withOptions + * @covers ::build + */ + public function shouldAcceptCustomOptions() + { + $client = DocScanClient::builder() + ->withAuthenticationToken(self::SOME_AUTH_TOKEN) + ->withOptions([Config::SDK_IDENTIFIER => 'CustomSDK']) + ->build(); + + $this->assertInstanceOf(DocScanClient::class, $client); + } + + /** + * @test + * @covers ::withAuthenticationToken + * @covers ::build + */ + public function shouldThrowOnEmptyAuthToken() + { + $this->expectException(\InvalidArgumentException::class); + + DocScanClient::builder() + ->withAuthenticationToken('') + ->build(); + } +} diff --git a/tests/DocScan/Session/Create/ApplicantProfileBuilderTest.php b/tests/DocScan/Session/Create/ApplicantProfileBuilderTest.php new file mode 100644 index 00000000..bb000cf3 --- /dev/null +++ b/tests/DocScan/Session/Create/ApplicantProfileBuilderTest.php @@ -0,0 +1,142 @@ +withFullName(self::SOME_FULL_NAME) + ->build(); + + $this->assertEquals(self::SOME_FULL_NAME, $profile->getFullName()); + } + + /** + * @test + * @covers ::build + * @covers ::withDateOfBirth + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::__construct + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::getDateOfBirth + */ + public function shouldBuildWithDateOfBirth() + { + $profile = (new ApplicantProfileBuilder()) + ->withDateOfBirth(self::SOME_DATE_OF_BIRTH) + ->build(); + + $this->assertEquals(self::SOME_DATE_OF_BIRTH, $profile->getDateOfBirth()); + } + + /** + * @test + * @covers ::build + * @covers ::withNamePrefix + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::__construct + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::getNamePrefix + */ + public function shouldBuildWithNamePrefix() + { + $profile = (new ApplicantProfileBuilder()) + ->withNamePrefix(self::SOME_NAME_PREFIX) + ->build(); + + $this->assertEquals(self::SOME_NAME_PREFIX, $profile->getNamePrefix()); + } + + /** + * @test + * @covers ::build + * @covers ::withStructuredPostalAddress + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::__construct + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::getStructuredPostalAddress + */ + public function shouldBuildWithStructuredPostalAddress() + { + $address = (new StructuredPostalAddressBuilder()) + ->withBuildingNumber('74') + ->withPostalCode('E143RN') + ->build(); + + $profile = (new ApplicantProfileBuilder()) + ->withStructuredPostalAddress($address) + ->build(); + + $this->assertEquals('74', $profile->getStructuredPostalAddress()->getBuildingNumber()); + $this->assertEquals('E143RN', $profile->getStructuredPostalAddress()->getPostalCode()); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::jsonSerialize + */ + public function shouldCorrectlySerializeWithAllProperties() + { + $address = (new StructuredPostalAddressBuilder()) + ->withAddressFormat(1) + ->withBuildingNumber('74') + ->withAddressLine1('AddressLine1') + ->withTownCity('CityName') + ->withPostalCode('E143RN') + ->withCountryIso('GBR') + ->withCountry('United Kingdom') + ->build(); + + $profile = (new ApplicantProfileBuilder()) + ->withFullName(self::SOME_FULL_NAME) + ->withDateOfBirth(self::SOME_DATE_OF_BIRTH) + ->withNamePrefix(self::SOME_NAME_PREFIX) + ->withStructuredPostalAddress($address) + ->build(); + + $json = json_encode($profile); + + $this->assertStringContainsString('"full_name":"John Doe"', $json); + $this->assertStringContainsString('"date_of_birth":"1988-11-02"', $json); + $this->assertStringContainsString('"name_prefix":"Mr"', $json); + $this->assertStringContainsString('"structured_postal_address"', $json); + $this->assertStringContainsString('"building_number":"74"', $json); + $this->assertStringContainsString('"country_iso":"GBR"', $json); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::jsonSerialize + */ + public function shouldSerializeWithoutNullValues() + { + $profile = (new ApplicantProfileBuilder()) + ->withFullName(self::SOME_FULL_NAME) + ->build(); + + $this->assertJsonStringEqualsJsonString( + json_encode([ + 'full_name' => self::SOME_FULL_NAME, + ]), + json_encode($profile) + ); + } +} diff --git a/tests/DocScan/Session/Create/ResourceCreationContainerBuilderTest.php b/tests/DocScan/Session/Create/ResourceCreationContainerBuilderTest.php new file mode 100644 index 00000000..4ad23b4d --- /dev/null +++ b/tests/DocScan/Session/Create/ResourceCreationContainerBuilderTest.php @@ -0,0 +1,93 @@ +withFullName('John Doe') + ->withDateOfBirth('1988-11-02') + ->build(); + + $container = (new ResourceCreationContainerBuilder()) + ->withApplicantProfile($applicantProfile) + ->build(); + + $this->assertEquals($applicantProfile, $container->getApplicantProfile()); + $this->assertEquals('John Doe', $container->getApplicantProfile()->getFullName()); + $this->assertEquals('1988-11-02', $container->getApplicantProfile()->getDateOfBirth()); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\ResourceCreationContainer::jsonSerialize + */ + public function shouldCorrectlySerializeApplicantProfile() + { + $address = (new StructuredPostalAddressBuilder()) + ->withAddressFormat(1) + ->withBuildingNumber('74') + ->withAddressLine1('AddressLine1') + ->withTownCity('CityName') + ->withPostalCode('E143RN') + ->withCountryIso('GBR') + ->withCountry('United Kingdom') + ->build(); + + $applicantProfile = (new ApplicantProfileBuilder()) + ->withFullName('John Doe') + ->withDateOfBirth('1988-11-02') + ->withNamePrefix('Mr') + ->withStructuredPostalAddress($address) + ->build(); + + $container = (new ResourceCreationContainerBuilder()) + ->withApplicantProfile($applicantProfile) + ->build(); + + $json = json_encode($container); + + $this->assertStringContainsString('"applicant_profile"', $json); + $this->assertStringContainsString('"full_name":"John Doe"', $json); + $this->assertStringContainsString('"date_of_birth":"1988-11-02"', $json); + $this->assertStringContainsString('"name_prefix":"Mr"', $json); + $this->assertStringContainsString('"building_number":"74"', $json); + $this->assertStringContainsString('"country_iso":"GBR"', $json); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\ResourceCreationContainer::jsonSerialize + */ + public function shouldSerializeWithoutNullValues() + { + $container = (new ResourceCreationContainerBuilder()) + ->build(); + + $this->assertJsonStringEqualsJsonString( + json_encode(new \stdClass()), + json_encode($container) + ); + } +} diff --git a/tests/DocScan/Session/Create/SdkConfigBuilderTest.php b/tests/DocScan/Session/Create/SdkConfigBuilderTest.php index 3c84f5d2..3c879bc3 100644 --- a/tests/DocScan/Session/Create/SdkConfigBuilderTest.php +++ b/tests/DocScan/Session/Create/SdkConfigBuilderTest.php @@ -23,7 +23,11 @@ class SdkConfigBuilderTest extends TestCase private const SOME_CATEGORY = 'someCategory'; private const SOME_NUMBER_RETRIES = 5; private const SOME_BIOMETRIC_CONSENT_FLOW = 'someBiometricConsentFlow'; - + private const SOME_DARK_MODE = 'someDarkMode'; + private const SOME_PRIMARY_COLOUR_DARK_MODE = 'somePrimaryColourDarkMode'; + private const SOME_BRAND_ID = 'someBrandId'; + private const SOME_SCREEN_IDENTIFIER = 'someScreenIdentifier'; + private const ANOTHER_SCREEN_IDENTIFIER = 'anotherScreenIdentifier'; /** * @test @@ -38,6 +42,7 @@ class SdkConfigBuilderTest extends TestCase * @covers ::withErrorUrl * @covers ::withPrivacyPolicyUrl * @covers ::withAllowHandoff + * @covers ::withBrandId * @covers \Yoti\DocScan\Session\Create\SdkConfig::__construct * @covers \Yoti\DocScan\Session\Create\SdkConfig::getAllowedCaptureMethods * @covers \Yoti\DocScan\Session\Create\SdkConfig::getPrimaryColour @@ -49,6 +54,9 @@ class SdkConfigBuilderTest extends TestCase * @covers \Yoti\DocScan\Session\Create\SdkConfig::getErrorUrl * @covers \Yoti\DocScan\Session\Create\SdkConfig::getPrivacyPolicyUrl * @covers \Yoti\DocScan\Session\Create\SdkConfig::getAllowHandoff + * @covers \Yoti\DocScan\Session\Create\SdkConfig::getDarkMode + * @covers \Yoti\DocScan\Session\Create\SdkConfig::getPrimaryColourDarkMode + * @covers \Yoti\DocScan\Session\Create\SdkConfig::getBrandId */ public function shouldCorrectlyBuildSdkConfig() { @@ -63,6 +71,10 @@ public function shouldCorrectlyBuildSdkConfig() ->withErrorUrl(self::SOME_ERROR_URL) ->withPrivacyPolicyUrl(self::SOME_PRIVACY_POLICY_URL) ->withAllowHandoff(true) + ->withBiometricConsentFlow(self::SOME_BIOMETRIC_CONSENT_FLOW) + ->withDarkMode(self::SOME_DARK_MODE) + ->withPrimaryColourDarkMode(self::SOME_PRIMARY_COLOUR_DARK_MODE) + ->withBrandId(self::SOME_BRAND_ID) ->build(); $this->assertEquals(self::SOME_CAPTURE_METHOD, $result->getAllowedCaptureMethods()); @@ -74,7 +86,11 @@ public function shouldCorrectlyBuildSdkConfig() $this->assertEquals(self::SOME_SUCCESS_URL, $result->getSuccessUrl()); $this->assertEquals(self::SOME_ERROR_URL, $result->getErrorUrl()); $this->assertEquals(self::SOME_PRIVACY_POLICY_URL, $result->getPrivacyPolicyUrl()); + $this->assertEquals(self::SOME_BIOMETRIC_CONSENT_FLOW, $result->getBiometricConsentFlow()); $this->assertTrue($result->getAllowHandoff()); + $this->assertEquals(self::SOME_DARK_MODE, $result->getDarkMode()); + $this->assertEquals(self::SOME_PRIMARY_COLOUR_DARK_MODE, $result->getPrimaryColourDarkMode()); + $this->assertEquals(self::SOME_BRAND_ID, $result->getBrandId()); } /** @@ -121,6 +137,8 @@ public function shouldProduceTheCorrectJsonString() ->withPrivacyPolicyUrl(self::SOME_PRIVACY_POLICY_URL) ->withAllowHandoff(true) ->withBiometricConsentFlow(self::SOME_BIOMETRIC_CONSENT_FLOW) + ->withPrimaryColourDarkMode(self::SOME_PRIMARY_COLOUR_DARK_MODE) + ->withDarkMode(self::SOME_DARK_MODE) ->build(); $expected = [ @@ -134,7 +152,9 @@ public function shouldProduceTheCorrectJsonString() 'error_url' => self::SOME_ERROR_URL, 'privacy_policy_url' => self::SOME_PRIVACY_POLICY_URL, 'allow_handoff' => true, - 'biometric_consent_flow' => self::SOME_BIOMETRIC_CONSENT_FLOW + 'biometric_consent_flow' => self::SOME_BIOMETRIC_CONSENT_FLOW, + 'dark_mode' => self::SOME_DARK_MODE, + 'primary_colour_dark_mode' => self::SOME_PRIMARY_COLOUR_DARK_MODE ]; $this->assertJsonStringEqualsJsonString(json_encode($expected), json_encode($result)); @@ -291,4 +311,133 @@ public function attemptsConfigurationShouldAllowMultipleCategories(): void ->getIdDocumentTextDataExtraction() ); } + + /** + * @test + * @covers ::withDarkModeAuto + */ + public function shouldSetCorrectValueWithDarkModeAuto() + { + $result = (new SdkConfigBuilder()) + ->withDarkModeAuto() + ->build(); + + $this->assertEquals('AUTO', $result->getDarkMode()); + } + + /** + * @test + * @covers ::withDarkModeOn + */ + public function shouldSetCorrectValueWithDarkModeOn() + { + $result = (new SdkConfigBuilder()) + ->withDarkModeOn() + ->build(); + + $this->assertEquals('ON', $result->getDarkMode()); + } + + /** + * @test + * @covers ::withDarkModeOff + */ + public function shouldSetCorrectValueWithDarkModeOff() + { + $result = (new SdkConfigBuilder()) + ->withDarkModeOff() + ->build(); + + $this->assertEquals('OFF', $result->getDarkMode()); + } + + /** + * @test + * @covers ::withSuppressedScreens + * @covers \Yoti\DocScan\Session\Create\SdkConfig::getSuppressedScreens + */ + public function shouldSetSuppressedScreensArray() + { + $suppressedScreens = [self::SOME_SCREEN_IDENTIFIER, self::ANOTHER_SCREEN_IDENTIFIER]; + + $result = (new SdkConfigBuilder()) + ->withSuppressedScreens($suppressedScreens) + ->build(); + + $this->assertEquals($suppressedScreens, $result->getSuppressedScreens()); + } + + /** + * @test + * @covers ::withSuppressedScreen + * @covers \Yoti\DocScan\Session\Create\SdkConfig::getSuppressedScreens + */ + public function shouldAddSingleSuppressedScreen() + { + $result = (new SdkConfigBuilder()) + ->withSuppressedScreen(self::SOME_SCREEN_IDENTIFIER) + ->build(); + + $this->assertEquals([self::SOME_SCREEN_IDENTIFIER], $result->getSuppressedScreens()); + } + + /** + * @test + * @covers ::withSuppressedScreen + * @covers \Yoti\DocScan\Session\Create\SdkConfig::getSuppressedScreens + */ + public function shouldAddMultipleSuppressedScreensIndividually() + { + $result = (new SdkConfigBuilder()) + ->withSuppressedScreen(self::SOME_SCREEN_IDENTIFIER) + ->withSuppressedScreen(self::ANOTHER_SCREEN_IDENTIFIER) + ->build(); + + $expectedScreens = [self::SOME_SCREEN_IDENTIFIER, self::ANOTHER_SCREEN_IDENTIFIER]; + $this->assertEquals($expectedScreens, $result->getSuppressedScreens()); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\SdkConfig::getSuppressedScreens + */ + public function shouldReturnNullWhenNoSuppressedScreensSet() + { + $result = (new SdkConfigBuilder()) + ->build(); + + $this->assertNull($result->getSuppressedScreens()); + } + + /** + * @test + * @covers ::withSuppressedScreens + * @covers \Yoti\DocScan\Session\Create\SdkConfig::jsonSerialize + */ + public function shouldIncludeSuppressedScreensInJsonSerialization() + { + $suppressedScreens = [self::SOME_SCREEN_IDENTIFIER, self::ANOTHER_SCREEN_IDENTIFIER]; + + $result = (new SdkConfigBuilder()) + ->withSuppressedScreens($suppressedScreens) + ->build(); + + $jsonData = $result->jsonSerialize(); + $this->assertEquals($suppressedScreens, $jsonData->suppressed_screens); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\SdkConfig::jsonSerialize + */ + public function shouldNotIncludeSuppressedScreensInJsonWhenNull() + { + $result = (new SdkConfigBuilder()) + ->build(); + + $jsonData = $result->jsonSerialize(); + $this->assertFalse(property_exists($jsonData, 'suppressed_screens')); + } } diff --git a/tests/DocScan/Session/Create/SessionSpecificationBuilderTest.php b/tests/DocScan/Session/Create/SessionSpecificationBuilderTest.php index 3f3640b7..f1d274b2 100644 --- a/tests/DocScan/Session/Create/SessionSpecificationBuilderTest.php +++ b/tests/DocScan/Session/Create/SessionSpecificationBuilderTest.php @@ -9,6 +9,7 @@ use Yoti\DocScan\Session\Create\IbvOptions; use Yoti\DocScan\Session\Create\ImportToken; use Yoti\DocScan\Session\Create\NotificationConfig; +use Yoti\DocScan\Session\Create\ResourceCreationContainer; use Yoti\DocScan\Session\Create\SdkConfig; use Yoti\DocScan\Session\Create\SessionSpecificationBuilder; use Yoti\DocScan\Session\Create\Task\RequestedTask; @@ -63,11 +64,21 @@ class SessionSpecificationBuilderTest extends TestCase */ private $identityProfileRequirements; + /** + * @var object + */ + private $advancedIdentityProfileRequirements; + /** * @var ImportToken */ private $importTokenMock; + /** + * @var ResourceCreationContainer + */ + private $resourcesMock; + public function setup(): void { $this->sdkConfigMock = $this->createMock(SdkConfig::class); @@ -89,6 +100,11 @@ public function setup(): void $this->importTokenMock = $this->createMock(ImportToken::class); + $this->resourcesMock = $this->createMock(ResourceCreationContainer::class); + $this->resourcesMock + ->method('jsonSerialize') + ->willReturn((object)['applicant_profile' => (object)['full_name' => 'John Doe']]); + $this->subject = (object)[1 => 'some']; $this->identityProfileRequirements = (object)[ @@ -98,6 +114,17 @@ public function setup(): void 'objective' => 'STANDARD' ] ]; + + $this->advancedIdentityProfileRequirements = (object)[ + 'profiles' => [ + [ + 'trust_framework' => 'UK_TFIDA', + 'schemes' => [ + ['type' => 'DBS', 'objective' => 'STANDARD'] + ] + ] + ] + ]; } /** @@ -532,4 +559,115 @@ public function shouldReturnCorrectJsonStringWithImportToken() json_encode($sessionSpecification) ); } + + /** + * @test + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::getAdvancedIdentityProfileRequirements + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::__construct + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::withAdvancedIdentityProfileRequirements + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::build + */ + public function shouldBuildWithAdvancedIdentityProfileRequirements() + { + $sessionSpecificationResult = (new SessionSpecificationBuilder()) + ->withAdvancedIdentityProfileRequirements($this->advancedIdentityProfileRequirements) + ->build(); + + $this->assertEquals( + $this->advancedIdentityProfileRequirements, + $sessionSpecificationResult->getAdvancedIdentityProfileRequirements() + ); + } + + /** + * @test + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::getAdvancedIdentityProfileRequirements + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::__construct + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::build + */ + public function shouldNotImplicitlySetAValueForAdvancedIdentityProfileRequirements() + { + $sessionSpecificationResult = (new SessionSpecificationBuilder()) + ->build(); + + $this->assertNull($sessionSpecificationResult->getAdvancedIdentityProfileRequirements()); + } + + /** + * @test + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::jsonSerialize + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::withAdvancedIdentityProfileRequirements + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::build + */ + public function shouldReturnCorrectJsonStringWithAdvancedIdentityProfileRequirements() + { + $sessionSpecification = (new SessionSpecificationBuilder()) + ->withAdvancedIdentityProfileRequirements($this->advancedIdentityProfileRequirements) + ->build(); + + $this->assertJsonStringEqualsJsonString( + json_encode([ + 'requested_checks' => [], + 'requested_tasks' => [], + 'required_documents' => [], + 'create_identity_profile_preview' => false, + 'advanced_identity_profile_requirements' => $this->advancedIdentityProfileRequirements, + ]), + json_encode($sessionSpecification) + ); + } + + /** + * @test + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::getResources + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::__construct + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::withResources + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::build + */ + public function shouldBuildWithResources() + { + $sessionSpecificationResult = (new SessionSpecificationBuilder()) + ->withResources($this->resourcesMock) + ->build(); + + $this->assertEquals($this->resourcesMock, $sessionSpecificationResult->getResources()); + } + + /** + * @test + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::getResources + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::__construct + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::build + */ + public function shouldNotImplicitlySetAValueForResources() + { + $sessionSpecificationResult = (new SessionSpecificationBuilder()) + ->build(); + + $this->assertNull($sessionSpecificationResult->getResources()); + } + + /** + * @test + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::jsonSerialize + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::withResources + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::build + */ + public function shouldReturnCorrectJsonStringWithResources() + { + $sessionSpecification = (new SessionSpecificationBuilder()) + ->withResources($this->resourcesMock) + ->build(); + + $this->assertJsonStringEqualsJsonString( + json_encode([ + 'requested_checks' => [], + 'requested_tasks' => [], + 'required_documents' => [], + 'create_identity_profile_preview' => false, + 'resources' => $this->resourcesMock, + ]), + json_encode($sessionSpecification) + ); + } } diff --git a/tests/DocScan/Session/Create/StructuredPostalAddressBuilderTest.php b/tests/DocScan/Session/Create/StructuredPostalAddressBuilderTest.php new file mode 100644 index 00000000..ad4f6a80 --- /dev/null +++ b/tests/DocScan/Session/Create/StructuredPostalAddressBuilderTest.php @@ -0,0 +1,121 @@ +withAddressFormat(self::SOME_ADDRESS_FORMAT) + ->withBuildingNumber(self::SOME_BUILDING_NUMBER) + ->withAddressLine1(self::SOME_ADDRESS_LINE_1) + ->withTownCity(self::SOME_TOWN_CITY) + ->withPostalCode(self::SOME_POSTAL_CODE) + ->withCountryIso(self::SOME_COUNTRY_ISO) + ->withCountry(self::SOME_COUNTRY) + ->withFormattedAddress(self::SOME_FORMATTED_ADDRESS) + ->build(); + + $this->assertEquals(self::SOME_ADDRESS_FORMAT, $address->getAddressFormat()); + $this->assertEquals(self::SOME_BUILDING_NUMBER, $address->getBuildingNumber()); + $this->assertEquals(self::SOME_ADDRESS_LINE_1, $address->getAddressLine1()); + $this->assertEquals(self::SOME_TOWN_CITY, $address->getTownCity()); + $this->assertEquals(self::SOME_POSTAL_CODE, $address->getPostalCode()); + $this->assertEquals(self::SOME_COUNTRY_ISO, $address->getCountryIso()); + $this->assertEquals(self::SOME_COUNTRY, $address->getCountry()); + $this->assertEquals(self::SOME_FORMATTED_ADDRESS, $address->getFormattedAddress()); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\StructuredPostalAddress::jsonSerialize + */ + public function shouldCorrectlySerialize() + { + $address = (new StructuredPostalAddressBuilder()) + ->withAddressFormat(self::SOME_ADDRESS_FORMAT) + ->withBuildingNumber(self::SOME_BUILDING_NUMBER) + ->withAddressLine1(self::SOME_ADDRESS_LINE_1) + ->withTownCity(self::SOME_TOWN_CITY) + ->withPostalCode(self::SOME_POSTAL_CODE) + ->withCountryIso(self::SOME_COUNTRY_ISO) + ->withCountry(self::SOME_COUNTRY) + ->withFormattedAddress(self::SOME_FORMATTED_ADDRESS) + ->build(); + + $this->assertJsonStringEqualsJsonString( + json_encode([ + 'address_format' => self::SOME_ADDRESS_FORMAT, + 'building_number' => self::SOME_BUILDING_NUMBER, + 'address_line1' => self::SOME_ADDRESS_LINE_1, + 'town_city' => self::SOME_TOWN_CITY, + 'postal_code' => self::SOME_POSTAL_CODE, + 'country_iso' => self::SOME_COUNTRY_ISO, + 'country' => self::SOME_COUNTRY, + 'formatted_address' => self::SOME_FORMATTED_ADDRESS, + ]), + json_encode($address) + ); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\StructuredPostalAddress::jsonSerialize + */ + public function shouldSerializeWithoutNullValues() + { + $address = (new StructuredPostalAddressBuilder()) + ->withBuildingNumber(self::SOME_BUILDING_NUMBER) + ->withPostalCode(self::SOME_POSTAL_CODE) + ->build(); + + $this->assertJsonStringEqualsJsonString( + json_encode([ + 'building_number' => self::SOME_BUILDING_NUMBER, + 'postal_code' => self::SOME_POSTAL_CODE, + ]), + json_encode($address) + ); + } +} diff --git a/tests/DocScan/Session/Retrieve/AdvancedIdentityProfilePreviewResponseTest.php b/tests/DocScan/Session/Retrieve/AdvancedIdentityProfilePreviewResponseTest.php new file mode 100644 index 00000000..54dad827 --- /dev/null +++ b/tests/DocScan/Session/Retrieve/AdvancedIdentityProfilePreviewResponseTest.php @@ -0,0 +1,47 @@ + [ + 'id' => 'SOME_ID', + 'type' => 'JSON', + 'created' => '2021-06-11T11:39:24Z', + 'last_updated' => '2021-06-11T11:39:24Z', + ] + ]; + + $result = new AdvancedIdentityProfilePreviewResponse($data); + + $this->assertInstanceOf(AdvancedIdentityProfilePreviewResponse::class, $result); + $this->assertInstanceOf(MediaResponse::class, $result->getMedia()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getMedia + */ + public function shouldHandleMissingMedia(): void + { + $result = new AdvancedIdentityProfilePreviewResponse([]); + + $this->assertNull($result->getMedia()); + } +} diff --git a/tests/DocScan/Session/Retrieve/AdvancedIdentityProfileResponseTest.php b/tests/DocScan/Session/Retrieve/AdvancedIdentityProfileResponseTest.php new file mode 100644 index 00000000..d4b446d3 --- /dev/null +++ b/tests/DocScan/Session/Retrieve/AdvancedIdentityProfileResponseTest.php @@ -0,0 +1,97 @@ + 'UK_TFIDA', + 'schemes_compliance' => [ + 0 => [ + 'scheme' => [ + 'type' => 'DBS', + 'objective' => 'STANDARD', + ], + 'requirements_met' => true, + 'requirements_not_met_info' => 'some string here', + ], + ], + 'media' => [ + ], + ]; + + /** + * @test + * @covers ::__construct + * @covers ::getIdentityProfileReport + * @covers ::getFailureReason + * @covers ::getResult + * @covers ::getSubjectId + */ + public function shouldCreatedCorrectly(): void + { + $testData = [ + 'subject_id' => self::SUBJECT_ID, + 'result' => self::RESULT, + 'failure_reason' => [ + 'reason_code' => self::REASON_CODE, + 'requirements_not_met_details' => [ + 0 => [ + 'failure_type' => self::FAILURE_TYPE, + 'document_type' => self::DOCUMENT_TYPE, + 'document_country_iso_code' => self::DOCUMENT_COUNTRY_ISO_CODE, + 'audit_id' => self::AUDIT_ID, + 'details' => self::DETAILS + ] + ] + ], + 'identity_profile_report' => self::IDENTITY_PROFILE_REPORT, + ]; + + $result = new AdvancedIdentityProfileResponse($testData); + $this->assertEquals(self::RESULT, $result->getResult()); + $this->assertEquals(self::SUBJECT_ID, $result->getSubjectId()); + $this->assertEquals((object)self::IDENTITY_PROFILE_REPORT, $result->getIdentityProfileReport()); + $this->assertInstanceOf(FailureReasonResponse::class, $result->getFailureReason()); + $this->assertEquals(self::REASON_CODE, $result->getFailureReason()->getReasonCode()); + $requirementNotMetDetailsResponse = $result->getFailureReason()->getRequirementNotMetDetails(); + $this->assertEquals(self::FAILURE_TYPE, $requirementNotMetDetailsResponse->getFailureType()); + $this->assertEquals(self::DOCUMENT_TYPE, $requirementNotMetDetailsResponse->getDocumentType()); + $this->assertEquals(self::AUDIT_ID, $requirementNotMetDetailsResponse->getAuditId()); + $this->assertEquals(self::DETAILS, $requirementNotMetDetailsResponse->getDetails()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getFailureReason + * @covers ::getIdentityProfileReport + */ + public function shouldHandleMissingOptionalFields(): void + { + $testData = [ + 'result' => self::RESULT, + ]; + + $result = new AdvancedIdentityProfileResponse($testData); + $this->assertEquals(self::RESULT, $result->getResult()); + $this->assertEquals('', $result->getSubjectId()); + $this->assertNull($result->getFailureReason()); + $this->assertNull($result->getIdentityProfileReport()); + } +} diff --git a/tests/DocScan/Session/Retrieve/ApplicantProfileResourceResponseTest.php b/tests/DocScan/Session/Retrieve/ApplicantProfileResourceResponseTest.php new file mode 100644 index 00000000..645deaf2 --- /dev/null +++ b/tests/DocScan/Session/Retrieve/ApplicantProfileResourceResponseTest.php @@ -0,0 +1,81 @@ + self::SOME_ID, + 'source' => [ + 'type' => self::SOME_SOURCE_TYPE, + ], + 'media' => [ + 'id' => self::SOME_MEDIA_ID, + 'type' => self::SOME_MEDIA_TYPE, + 'created' => self::SOME_CREATED_AT, + 'last_updated' => self::SOME_LAST_UPDATED, + ], + 'created_at' => self::SOME_CREATED_AT, + 'last_updated' => self::SOME_LAST_UPDATED, + 'tasks' => [], + ]; + + $result = new ApplicantProfileResourceResponse($input); + + $this->assertEquals(self::SOME_ID, $result->getId()); + $this->assertNotNull($result->getSource()); + $this->assertInstanceOf(MediaResponse::class, $result->getMedia()); + $this->assertEquals(self::SOME_MEDIA_ID, $result->getMedia()->getId()); + $this->assertEquals(self::SOME_MEDIA_TYPE, $result->getMedia()->getType()); + $this->assertEquals( + DateTime::stringToDateTime(self::SOME_CREATED_AT), + $result->getCreatedAt() + ); + $this->assertEquals( + DateTime::stringToDateTime(self::SOME_LAST_UPDATED), + $result->getLastUpdated() + ); + $this->assertCount(0, $result->getTasks()); + } + + /** + * @test + * @covers ::__construct + */ + public function shouldNotThrowExceptionWhenMissingValues() + { + $result = new ApplicantProfileResourceResponse([]); + + $this->assertNull($result->getId()); + $this->assertNull($result->getMedia()); + $this->assertNull($result->getCreatedAt()); + $this->assertNull($result->getLastUpdated()); + $this->assertCount(0, $result->getTasks()); + } +} diff --git a/tests/DocScan/Session/Retrieve/BreakdownResponseTest.php b/tests/DocScan/Session/Retrieve/BreakdownResponseTest.php index dc92d2fd..9e4e9a30 100644 --- a/tests/DocScan/Session/Retrieve/BreakdownResponseTest.php +++ b/tests/DocScan/Session/Retrieve/BreakdownResponseTest.php @@ -25,11 +25,14 @@ class BreakdownResponseTest extends TestCase ], ]; + private const SOME_PROCESS = 'AUTOMATED'; + /** * @test * @covers ::__construct * @covers ::getSubCheck * @covers ::getResult + * @covers ::getProcess * @covers ::getDetails * @covers \Yoti\DocScan\Session\Retrieve\DetailsResponse::__construct * @covers \Yoti\DocScan\Session\Retrieve\DetailsResponse::getName @@ -40,6 +43,7 @@ public function shouldBuildCorrectly() $input = [ 'sub_check' => self::SOME_SUB_CHECK, 'result' => self::SOME_RESULT, + 'process' => self::SOME_PROCESS, 'details' => self::SOME_DETAILS, ]; @@ -47,6 +51,7 @@ public function shouldBuildCorrectly() $this->assertEquals(self::SOME_SUB_CHECK, $result->getSubCheck()); $this->assertEquals(self::SOME_RESULT, $result->getResult()); + $this->assertEquals(self::SOME_PROCESS, $result->getProcess()); $details = $result->getDetails(); for ($i = 0; $i < count(self::SOME_DETAILS); $i++) { @@ -61,6 +66,7 @@ public function shouldBuildCorrectly() * @covers ::__construct * @covers ::getSubCheck * @covers ::getResult + * @covers ::getProcess * @covers ::getDetails */ public function shouldNotThrowExceptionWhenValuesAreMissing() @@ -71,6 +77,7 @@ public function shouldNotThrowExceptionWhenValuesAreMissing() $this->assertNull($result->getSubCheck()); $this->assertNull($result->getResult()); + $this->assertNull($result->getProcess()); $this->assertCount(0, $result->getDetails()); } } diff --git a/tests/DocScan/Session/Retrieve/Configuration/SessionConfigurationResponseTest.php b/tests/DocScan/Session/Retrieve/Configuration/SessionConfigurationResponseTest.php index fe724975..7a73a6e4 100644 --- a/tests/DocScan/Session/Retrieve/Configuration/SessionConfigurationResponseTest.php +++ b/tests/DocScan/Session/Retrieve/Configuration/SessionConfigurationResponseTest.php @@ -14,6 +14,8 @@ class SessionConfigurationResponseTest extends TestCase private const SOME_CLIENT_SESSION_TTL = 12345678; private const SOME_SESSION_ID = 'SOME_SESSION_ID'; private const SOME_REQUESTED_CHECKS = ['SOME_CHECK', 'SOME_ANOTHER_CHECK']; + private const SOME_SCREEN_IDENTIFIER = 'someScreenIdentifier'; + private const ANOTHER_SCREEN_IDENTIFIER = 'anotherScreenIdentifier'; private const SOME_CAPTURE = [ 'biometric_consent' => 'SOME_STRING', 'required_resources' => [ @@ -61,4 +63,82 @@ public function shouldBuildCorrectly() $this->assertCount(2, $result->getRequestedChecks()); } + + /** + * @test + * @covers ::__construct + * @covers ::getSdkConfig + * @covers ::getSuppressedScreens + */ + public function shouldBuildWithSdkConfig() + { + $sdkConfig = [ + 'primary_colour' => '#123456', + 'suppressed_screens' => [self::SOME_SCREEN_IDENTIFIER, self::ANOTHER_SCREEN_IDENTIFIER] + ]; + + $sessionData = [ + 'client_session_token_ttl' => self::SOME_CLIENT_SESSION_TTL, + 'session_id' => self::SOME_SESSION_ID, + 'requested_checks' => self::SOME_REQUESTED_CHECKS, + 'capture' => self::SOME_CAPTURE, + 'sdk_config' => $sdkConfig + ]; + + $result = new SessionConfigurationResponse($sessionData); + + $this->assertEquals($sdkConfig, $result->getSdkConfig()); + $this->assertEquals( + [self::SOME_SCREEN_IDENTIFIER, self::ANOTHER_SCREEN_IDENTIFIER], + $result->getSuppressedScreens() + ); + } + + /** + * @test + * @covers ::__construct + * @covers ::getSdkConfig + * @covers ::getSuppressedScreens + */ + public function shouldReturnNullForSdkConfigWhenNotPresent() + { + $sessionData = [ + 'client_session_token_ttl' => self::SOME_CLIENT_SESSION_TTL, + 'session_id' => self::SOME_SESSION_ID, + 'requested_checks' => self::SOME_REQUESTED_CHECKS, + 'capture' => self::SOME_CAPTURE + ]; + + $result = new SessionConfigurationResponse($sessionData); + + $this->assertNull($result->getSdkConfig()); + $this->assertNull($result->getSuppressedScreens()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getSdkConfig + * @covers ::getSuppressedScreens + */ + public function shouldReturnNullForSuppressedScreensWhenNotInSdkConfig() + { + $sdkConfig = [ + 'primary_colour' => '#123456' + // No suppressed_screens + ]; + + $sessionData = [ + 'client_session_token_ttl' => self::SOME_CLIENT_SESSION_TTL, + 'session_id' => self::SOME_SESSION_ID, + 'requested_checks' => self::SOME_REQUESTED_CHECKS, + 'capture' => self::SOME_CAPTURE, + 'sdk_config' => $sdkConfig + ]; + + $result = new SessionConfigurationResponse($sessionData); + + $this->assertEquals($sdkConfig, $result->getSdkConfig()); + $this->assertNull($result->getSuppressedScreens()); + } } diff --git a/tests/DocScan/Session/Retrieve/GetSessionResultTest.php b/tests/DocScan/Session/Retrieve/GetSessionResultTest.php index 0d879a12..ee30dddb 100644 --- a/tests/DocScan/Session/Retrieve/GetSessionResultTest.php +++ b/tests/DocScan/Session/Retrieve/GetSessionResultTest.php @@ -4,6 +4,8 @@ namespace Yoti\Test\DocScan\Session\Retrieve; +use Yoti\DocScan\Session\Retrieve\AdvancedIdentityProfilePreviewResponse; +use Yoti\DocScan\Session\Retrieve\AdvancedIdentityProfileResponse; use Yoti\DocScan\Session\Retrieve\AuthenticityCheckResponse; use Yoti\DocScan\Session\Retrieve\CheckResponse; use Yoti\DocScan\Session\Retrieve\GetSessionResult; @@ -296,4 +298,60 @@ public function shouldParseImportTokenResponse() $this->assertInstanceOf(MediaResponse::class, $result->getImportToken()->getMedia()); $this->assertEquals('SOME_REASON', $result->getImportToken()->getFailureReason()); } + + /** + * @test + * @covers ::getAdvancedIdentityProfile + * @covers ::__construct + */ + public function shouldParseAdvancedIdentityProfileResponse() + { + $input = [ + 'advanced_identity_profile' => self::IDENTITY_PROFILE, + ]; + + $result = new GetSessionResult($input); + + $this->assertInstanceOf(AdvancedIdentityProfileResponse::class, $result->getAdvancedIdentityProfile()); + } + + /** + * @test + * @covers ::getAdvancedIdentityProfilePreview + * @covers ::__construct + */ + public function shouldParseAdvancedIdentityProfilePreviewResponse() + { + $input = [ + 'advanced_identity_profile_preview' => [ + 'media' => [ + 'id' => 'SOME_ID', + 'type' => 'JSON', + 'created' => '2021-06-11T11:39:24Z', + 'last_updated' => '2021-06-11T11:39:24Z', + ] + ], + ]; + + $result = new GetSessionResult($input); + + $this->assertInstanceOf( + AdvancedIdentityProfilePreviewResponse::class, + $result->getAdvancedIdentityProfilePreview() + ); + } + + /** + * @test + * @covers ::getAdvancedIdentityProfile + * @covers ::getAdvancedIdentityProfilePreview + * @covers ::__construct + */ + public function shouldReturnNullWhenAdvancedIdentityProfileNotPresent() + { + $result = new GetSessionResult([]); + + $this->assertNull($result->getAdvancedIdentityProfile()); + $this->assertNull($result->getAdvancedIdentityProfilePreview()); + } } diff --git a/tests/DocScan/Session/Retrieve/PageResponseTest.php b/tests/DocScan/Session/Retrieve/PageResponseTest.php index 91306f12..89527f10 100644 --- a/tests/DocScan/Session/Retrieve/PageResponseTest.php +++ b/tests/DocScan/Session/Retrieve/PageResponseTest.php @@ -56,6 +56,25 @@ public function testGetFrames() $this->containsOnlyInstancesOf(FrameResponse::class, $pageResponse->getFrames()); } + /** + * @covers ::__construct + * @covers ::getExtractionImageIds + */ + public function testGetExtractionImageIds() + { + $extractionImageIds = [ + '066a9372-0a52-4fe4-a026-866f8aee6fcb', + '9b0c9c0a-ff30-41ed-815b-d95d63271d45', + ]; + + $pageResponse = new PageResponse([ + 'extraction_image_ids' => $extractionImageIds, + ]); + + $this->assertCount(2, $pageResponse->getExtractionImageIds()); + $this->assertEquals($extractionImageIds, $pageResponse->getExtractionImageIds()); + } + /** * @test * @covers ::__construct @@ -67,5 +86,6 @@ public function shouldNotThrowExceptionWhenMissingValues() $this->assertNull($result->getCaptureMethod()); $this->assertNull($result->getMedia()); $this->assertEquals([], $result->getFrames()); + $this->assertEquals([], $result->getExtractionImageIds()); } } diff --git a/tests/DocScan/Session/Retrieve/ResourceContainerTest.php b/tests/DocScan/Session/Retrieve/ResourceContainerTest.php index dbc8c2c3..f73beb81 100644 --- a/tests/DocScan/Session/Retrieve/ResourceContainerTest.php +++ b/tests/DocScan/Session/Retrieve/ResourceContainerTest.php @@ -4,7 +4,9 @@ namespace Yoti\Test\DocScan\Session\Retrieve; +use Yoti\DocScan\Session\Retrieve\ApplicantProfileResourceResponse; use Yoti\DocScan\Session\Retrieve\ResourceContainer; +use Yoti\DocScan\Session\Retrieve\ShareCodeResourceResponse; use Yoti\DocScan\Session\Retrieve\StaticLivenessResourceResponse; use Yoti\DocScan\Session\Retrieve\ZoomLivenessResourceResponse; use Yoti\Test\TestCase; @@ -27,6 +29,8 @@ class ResourceContainerTest extends TestCase * @covers ::parseFaceCapture * @covers ::parseSupplementaryDocuments * @covers ::getSupplementaryDocuments + * @covers ::parseShareCodes + * @covers ::getShareCodes */ public function shouldBuildCorrectly() { @@ -46,7 +50,14 @@ public function shouldBuildCorrectly() ], 'face_capture' => [ ['id' => 'SOME_ID'] - ] + ], + 'share_codes' => [ + ['id' => 'share-code-1'], + ['id' => 'share-code-2'], + ], + 'applicant_profiles' => [ + ['id' => 'applicant-profile-1'], + ], ]; $result = new ResourceContainer($input); @@ -57,6 +68,8 @@ public function shouldBuildCorrectly() $this->assertCount(1, $result->getStaticLivenessResources()); $this->assertCount(2, $result->getSupplementaryDocuments()); $this->assertCount(1, $result->getFaceCapture()); + $this->assertCount(2, $result->getShareCodes()); + $this->assertCount(1, $result->getApplicantProfiles()); } /** @@ -69,6 +82,8 @@ public function shouldNotThrowExceptionWhenMissingValues() $this->assertCount(0, $result->getIdDocuments()); $this->assertCount(0, $result->getLivenessCapture()); + $this->assertCount(0, $result->getShareCodes()); + $this->assertCount(0, $result->getApplicantProfiles()); } /** @@ -129,4 +144,79 @@ public function shouldFilterZoomLivenessResources(): void $this->assertCount(3, $result->getLivenessCapture()); $this->assertCount(2, $result->getZoomLivenessResources()); } + + /** + * @test + * @covers ::parseShareCodes + * @covers ::getShareCodes + */ + public function shouldParseShareCodes(): void + { + $input = [ + 'share_codes' => [ + [ + 'id' => 'share-code-1', + 'source' => ['type' => 'END_USER'], + 'created_at' => '2026-01-14T10:00:00Z', + 'last_updated' => '2026-01-14T11:00:00Z', + 'tasks' => [], + ], + [ + 'id' => 'share-code-2', + 'source' => ['type' => 'END_USER'], + 'created_at' => '2026-01-14T12:00:00Z', + 'last_updated' => '2026-01-14T13:00:00Z', + 'tasks' => [], + ], + ], + ]; + + $result = new ResourceContainer($input); + + $this->assertCount(2, $result->getShareCodes()); + $this->assertContainsOnlyInstancesOf( + ShareCodeResourceResponse::class, + $result->getShareCodes() + ); + $this->assertEquals('share-code-1', $result->getShareCodes()[0]->getId()); + $this->assertEquals('share-code-2', $result->getShareCodes()[1]->getId()); + } + + /** + * @test + * @covers ::parseApplicantProfiles + * @covers ::getApplicantProfiles + */ + public function shouldParseApplicantProfiles(): void + { + $input = [ + 'applicant_profiles' => [ + [ + 'id' => '3fa85f64-5717-4562-b3fc-2c963f66afa6', + 'source' => ['type' => 'END_USER'], + 'media' => [ + 'id' => 'media-id-123', + 'type' => 'IMAGE', + 'created' => '2021-06-11T11:39:24Z', + 'last_updated' => '2021-06-11T11:39:24Z', + ], + 'created_at' => '2021-06-11T11:39:24Z', + 'last_updated' => '2021-06-11T11:39:24Z', + 'tasks' => [], + ], + ], + ]; + + $result = new ResourceContainer($input); + + $this->assertCount(1, $result->getApplicantProfiles()); + $this->assertContainsOnlyInstancesOf( + ApplicantProfileResourceResponse::class, + $result->getApplicantProfiles() + ); + $this->assertEquals( + '3fa85f64-5717-4562-b3fc-2c963f66afa6', + $result->getApplicantProfiles()[0]->getId() + ); + } } diff --git a/tests/DocScan/Session/Retrieve/ShareCodeMediaResponseTest.php b/tests/DocScan/Session/Retrieve/ShareCodeMediaResponseTest.php new file mode 100644 index 00000000..a6b5fa3b --- /dev/null +++ b/tests/DocScan/Session/Retrieve/ShareCodeMediaResponseTest.php @@ -0,0 +1,70 @@ + [ + 'id' => 'some-media-id', + 'type' => 'IMAGE', + 'created' => '2026-01-14T10:00:00Z', + 'last_updated' => '2026-01-14T11:00:00Z', + ], + ]; + + $result = new ShareCodeMediaResponse($input); + + $this->assertInstanceOf(MediaResponse::class, $result->getMedia()); + $this->assertEquals('some-media-id', $result->getMedia()->getId()); + $this->assertEquals('IMAGE', $result->getMedia()->getType()); + $this->assertNotNull($result->getMedia()->getCreated()); + $this->assertNotNull($result->getMedia()->getLastUpdated()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getMedia + */ + public function shouldHandleMissingMedia() + { + $result = new ShareCodeMediaResponse([]); + + $this->assertNull($result->getMedia()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getMedia + */ + public function shouldHandleEmptyMediaObject() + { + $input = [ + 'media' => [], + ]; + + $result = new ShareCodeMediaResponse($input); + + $this->assertInstanceOf(MediaResponse::class, $result->getMedia()); + $this->assertNull($result->getMedia()->getId()); + $this->assertNull($result->getMedia()->getType()); + } +} diff --git a/tests/DocScan/Session/Retrieve/ShareCodeResourceResponseTest.php b/tests/DocScan/Session/Retrieve/ShareCodeResourceResponseTest.php new file mode 100644 index 00000000..d4528ed0 --- /dev/null +++ b/tests/DocScan/Session/Retrieve/ShareCodeResourceResponseTest.php @@ -0,0 +1,327 @@ + 'share-code-123', + 'source' => ['type' => 'END_USER'], + 'created_at' => '2026-01-14T10:00:00Z', + 'last_updated' => '2026-01-14T11:00:00Z', + 'lookup_profile' => [ + 'media' => ['id' => 'media-1', 'type' => 'JSON'], + ], + 'returned_profile' => [ + 'media' => ['id' => 'media-2', 'type' => 'JSON'], + ], + 'id_photo' => [ + 'media' => ['id' => 'media-3', 'type' => 'IMAGE'], + ], + 'file' => [ + 'media' => ['id' => 'media-4', 'type' => 'PDF'], + ], + 'tasks' => [ + [ + 'type' => self::VERIFY_SHARE_CODE_TASK, + 'id' => 'task-123', + 'state' => 'DONE', + 'created' => '2026-01-14T10:00:00Z', + 'last_updated' => '2026-01-14T11:00:00Z', + 'generated_media' => [ + ['id' => 'gm-1', 'type' => 'PDF'], + ['id' => 'gm-2', 'type' => 'IMAGE'], + ], + ], + ], + ]; + + $result = new ShareCodeResourceResponse($input); + + $this->assertEquals('share-code-123', $result->getId()); + $this->assertInstanceOf(\DateTime::class, $result->getCreatedAt()); + $this->assertInstanceOf(\DateTime::class, $result->getLastUpdated()); + + $this->assertInstanceOf(ShareCodeMediaResponse::class, $result->getLookupProfile()); + $this->assertInstanceOf(MediaResponse::class, $result->getLookupProfile()->getMedia()); + $this->assertEquals('media-1', $result->getLookupProfile()->getMedia()->getId()); + $this->assertEquals('JSON', $result->getLookupProfile()->getMedia()->getType()); + + $this->assertInstanceOf(ShareCodeMediaResponse::class, $result->getReturnedProfile()); + $this->assertEquals('media-2', $result->getReturnedProfile()->getMedia()->getId()); + + $this->assertInstanceOf(ShareCodeMediaResponse::class, $result->getIdPhoto()); + $this->assertEquals('media-3', $result->getIdPhoto()->getMedia()->getId()); + $this->assertEquals('IMAGE', $result->getIdPhoto()->getMedia()->getType()); + + $this->assertInstanceOf(ShareCodeMediaResponse::class, $result->getFile()); + $this->assertEquals('media-4', $result->getFile()->getMedia()->getId()); + $this->assertEquals('PDF', $result->getFile()->getMedia()->getType()); + + $this->assertCount(1, $result->getVerifyShareCodeTasks()); + $this->assertContainsOnlyInstancesOf( + VerifyShareCodeTaskResponse::class, + $result->getVerifyShareCodeTasks() + ); + $this->assertEquals('task-123', $result->getVerifyShareCodeTasks()[0]->getId()); + $this->assertEquals('DONE', $result->getVerifyShareCodeTasks()[0]->getState()); + } + + /** + * @test + * @covers ::__construct + */ + public function shouldNotThrowExceptionWhenMissingValues() + { + $result = new ShareCodeResourceResponse([]); + + $this->assertNull($result->getId()); + $this->assertNull($result->getCreatedAt()); + $this->assertNull($result->getLastUpdated()); + $this->assertNull($result->getLookupProfile()); + $this->assertNull($result->getReturnedProfile()); + $this->assertNull($result->getIdPhoto()); + $this->assertNull($result->getFile()); + $this->assertCount(0, $result->getVerifyShareCodeTasks()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getVerifyShareCodeTasks + */ + public function shouldFilterVerifyShareCodeTasks() + { + $input = [ + 'id' => 'share-code-mixed', + 'tasks' => [ + [ + 'type' => self::VERIFY_SHARE_CODE_TASK, + 'id' => 'task-verify', + 'state' => 'DONE', + ], + [ + 'type' => 'OTHER_TASK_TYPE', + 'id' => 'task-other', + 'state' => 'PENDING', + ], + ], + ]; + + $result = new ShareCodeResourceResponse($input); + + $this->assertCount(2, $result->getTasks()); + $this->assertCount(1, $result->getVerifyShareCodeTasks()); + $this->assertEquals('task-verify', $result->getVerifyShareCodeTasks()[0]->getId()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getVerifyShareCodeTasks + */ + public function shouldHandleMultipleVerifyShareCodeTasks() + { + $input = [ + 'id' => 'share-code-multi', + 'tasks' => [ + [ + 'type' => self::VERIFY_SHARE_CODE_TASK, + 'id' => 'task-1', + 'state' => 'PENDING', + ], + [ + 'type' => self::VERIFY_SHARE_CODE_TASK, + 'id' => 'task-2', + 'state' => 'DONE', + ], + ], + ]; + + $result = new ShareCodeResourceResponse($input); + + $this->assertCount(2, $result->getVerifyShareCodeTasks()); + $this->assertEquals('task-1', $result->getVerifyShareCodeTasks()[0]->getId()); + $this->assertEquals('task-2', $result->getVerifyShareCodeTasks()[1]->getId()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getVerifyShareCodeTasks + */ + public function shouldHandleNoTasks() + { + $input = [ + 'id' => 'share-code-no-tasks', + 'tasks' => [], + ]; + + $result = new ShareCodeResourceResponse($input); + + $this->assertCount(0, $result->getVerifyShareCodeTasks()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getLookupProfile + * @covers ::getReturnedProfile + * @covers ::getIdPhoto + * @covers ::getFile + */ + public function shouldHandlePartialMediaFields() + { + $input = [ + 'id' => 'share-code-partial', + 'lookup_profile' => [ + 'media' => ['id' => 'media-1', 'type' => 'JSON'], + ], + ]; + + $result = new ShareCodeResourceResponse($input); + + $this->assertInstanceOf(ShareCodeMediaResponse::class, $result->getLookupProfile()); + $this->assertEquals('media-1', $result->getLookupProfile()->getMedia()->getId()); + $this->assertNull($result->getReturnedProfile()); + $this->assertNull($result->getIdPhoto()); + $this->assertNull($result->getFile()); + } + + /** + * @test + * @covers ::__construct + */ + public function shouldHandleEmptyMediaObjects() + { + $input = [ + 'id' => 'share-code-empty-media', + 'id_photo' => [], + ]; + + $result = new ShareCodeResourceResponse($input); + + $this->assertInstanceOf(ShareCodeMediaResponse::class, $result->getIdPhoto()); + $this->assertNull($result->getIdPhoto()->getMedia()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getCreatedAt + * @covers ::getLastUpdated + * @covers ::getLookupProfile + * @covers ::getReturnedProfile + * @covers ::getIdPhoto + * @covers ::getFile + * @covers ::getVerifyShareCodeTasks + */ + public function shouldHandleFullRealisticPayload() + { + $input = [ + 'id' => 'abc12345-6789-abcd-ef01-234567890abc', + 'source' => ['type' => 'END_USER'], + 'created_at' => '2026-02-05T11:33:46Z', + 'last_updated' => '2026-02-05T11:33:50Z', + 'lookup_profile' => [ + 'media' => [ + 'id' => 'df419a66-0449-41cf-a795-6dfaa993d1f6', + 'type' => 'JSON', + 'created' => '2026-02-05T11:33:46Z', + 'last_updated' => '2026-02-05T11:33:50Z', + ], + ], + 'returned_profile' => [ + 'media' => [ + 'id' => 'f2152059-2868-47c9-8f5f-64966c1b66b0', + 'type' => 'JSON', + 'created' => '2026-02-05T11:33:46Z', + 'last_updated' => '2026-02-05T11:33:50Z', + ], + ], + 'id_photo' => [ + 'media' => [ + 'id' => '45e4ee9d-a77b-4007-afe9-ab7067687aff', + 'type' => 'IMAGE', + 'created' => '2026-02-05T11:33:46Z', + 'last_updated' => '2026-02-05T11:33:50Z', + ], + ], + 'file' => [ + 'media' => [ + 'id' => 'c83a9f12-1234-5678-9abc-def012345678', + 'type' => 'PDF', + 'created' => '2026-02-05T11:33:46Z', + 'last_updated' => '2026-02-05T11:33:50Z', + ], + ], + 'tasks' => [ + [ + 'type' => self::VERIFY_SHARE_CODE_TASK, + 'id' => '73141aa9-a01f-4de9-9281-1b11cda7ab75', + 'state' => 'DONE', + 'created' => '2026-02-05T11:33:46Z', + 'last_updated' => '2026-02-05T11:33:50Z', + 'generated_media' => [ + ['id' => 'df419a66-0449-41cf-a795-6dfaa993d1f6', 'type' => 'PDF'], + ['id' => '45e4ee9d-a77b-4007-afe9-ab7067687aff', 'type' => 'IMAGE'], + ['id' => 'f2152059-2868-47c9-8f5f-64966c1b66b0', 'type' => 'JSON'], + ], + ], + ], + ]; + + $result = new ShareCodeResourceResponse($input); + + $this->assertEquals('abc12345-6789-abcd-ef01-234567890abc', $result->getId()); + $this->assertInstanceOf(\DateTime::class, $result->getCreatedAt()); + $this->assertInstanceOf(\DateTime::class, $result->getLastUpdated()); + + $this->assertNotNull($result->getLookupProfile()); + $this->assertEquals('df419a66-0449-41cf-a795-6dfaa993d1f6', $result->getLookupProfile()->getMedia()->getId()); + $this->assertEquals('JSON', $result->getLookupProfile()->getMedia()->getType()); + $this->assertNotNull($result->getLookupProfile()->getMedia()->getCreated()); + $this->assertNotNull($result->getLookupProfile()->getMedia()->getLastUpdated()); + + $this->assertNotNull($result->getReturnedProfile()); + $this->assertEquals('f2152059-2868-47c9-8f5f-64966c1b66b0', $result->getReturnedProfile()->getMedia()->getId()); + + $this->assertNotNull($result->getIdPhoto()); + $this->assertEquals('45e4ee9d-a77b-4007-afe9-ab7067687aff', $result->getIdPhoto()->getMedia()->getId()); + $this->assertEquals('IMAGE', $result->getIdPhoto()->getMedia()->getType()); + + $this->assertNotNull($result->getFile()); + $this->assertEquals('c83a9f12-1234-5678-9abc-def012345678', $result->getFile()->getMedia()->getId()); + $this->assertEquals('PDF', $result->getFile()->getMedia()->getType()); + + $this->assertCount(1, $result->getVerifyShareCodeTasks()); + $this->assertEquals('DONE', $result->getVerifyShareCodeTasks()[0]->getState()); + $this->assertCount(3, $result->getVerifyShareCodeTasks()[0]->getGeneratedMedia()); + } +} diff --git a/tests/DocScan/Session/Retrieve/TaskRecommendationReasonResponseTest.php b/tests/DocScan/Session/Retrieve/TaskRecommendationReasonResponseTest.php new file mode 100644 index 00000000..80c0a74c --- /dev/null +++ b/tests/DocScan/Session/Retrieve/TaskRecommendationReasonResponseTest.php @@ -0,0 +1,48 @@ + self::SOME_VALUE, + 'detail' => self::SOME_DETAIL, + ]; + + $result = new TaskRecommendationReasonResponse($input); + + $this->assertEquals(self::SOME_VALUE, $result->getValue()); + $this->assertEquals(self::SOME_DETAIL, $result->getDetail()); + } + + /** + * @test + * @covers ::__construct + */ + public function shouldNotThrowExceptionWhenMissingValues() + { + $result = new TaskRecommendationReasonResponse([]); + + $this->assertNull($result->getValue()); + $this->assertNull($result->getDetail()); + } +} diff --git a/tests/DocScan/Session/Retrieve/TaskRecommendationResponseTest.php b/tests/DocScan/Session/Retrieve/TaskRecommendationResponseTest.php new file mode 100644 index 00000000..46c265b9 --- /dev/null +++ b/tests/DocScan/Session/Retrieve/TaskRecommendationResponseTest.php @@ -0,0 +1,55 @@ + self::SOME_VALUE, + 'reason' => [ + 'value' => self::SOME_REASON_VALUE, + 'detail' => self::SOME_REASON_DETAIL, + ], + ]; + + $result = new TaskRecommendationResponse($input); + + $this->assertEquals(self::SOME_VALUE, $result->getValue()); + $this->assertInstanceOf(TaskRecommendationReasonResponse::class, $result->getReason()); + $this->assertEquals(self::SOME_REASON_VALUE, $result->getReason()->getValue()); + $this->assertEquals(self::SOME_REASON_DETAIL, $result->getReason()->getDetail()); + } + + /** + * @test + * @covers ::__construct + */ + public function shouldNotThrowExceptionWhenMissingValues() + { + $result = new TaskRecommendationResponse([]); + + $this->assertNull($result->getValue()); + $this->assertNull($result->getReason()); + } +} diff --git a/tests/DocScan/Session/Retrieve/TaskResponseTest.php b/tests/DocScan/Session/Retrieve/TaskResponseTest.php index 2b63366b..b3483119 100644 --- a/tests/DocScan/Session/Retrieve/TaskResponseTest.php +++ b/tests/DocScan/Session/Retrieve/TaskResponseTest.php @@ -7,6 +7,8 @@ use Yoti\DocScan\Session\Retrieve\GeneratedCheckResponse; use Yoti\DocScan\Session\Retrieve\GeneratedMedia; use Yoti\DocScan\Session\Retrieve\GeneratedTextDataCheckResponse; +use Yoti\DocScan\Session\Retrieve\TaskRecommendationReasonResponse; +use Yoti\DocScan\Session\Retrieve\TaskRecommendationResponse; use Yoti\DocScan\Session\Retrieve\TaskResponse; use Yoti\Test\TestCase; use Yoti\Util\DateTime; @@ -25,6 +27,9 @@ class TaskResponseTest extends TestCase private const SOME_UNKNOWN_TYPE = 'someUnknownType'; private const ID_DOCUMENT_TEXT_DATA_CHECK = 'ID_DOCUMENT_TEXT_DATA_CHECK'; private const SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK = 'SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK'; + private const SOME_RECOMMENDATION_VALUE = 'MUST_TRY_AGAIN'; + private const SOME_RECOMMENDATION_REASON_VALUE = 'USER_ERROR'; + private const SOME_RECOMMENDATION_REASON_DETAIL = 'NO_DOCUMENT'; /** * @var TaskResponse @@ -57,6 +62,13 @@ public function setup(): void [], [], ], + 'recommendation' => [ + 'value' => self::SOME_RECOMMENDATION_VALUE, + 'reason' => [ + 'value' => self::SOME_RECOMMENDATION_REASON_VALUE, + 'detail' => self::SOME_RECOMMENDATION_REASON_DETAIL, + ], + ], ]); } @@ -185,5 +197,22 @@ public function shouldNotThrowExceptionWhenAllMissingValuesExceptType() $this->assertNull($result->getLastUpdated()); $this->assertCount(0, $result->getGeneratedChecks()); $this->assertCount(0, $result->getGeneratedMedia()); + $this->assertNull($result->getRecommendation()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getRecommendation + */ + public function shouldReturnRecommendation() + { + $recommendation = $this->taskResponse->getRecommendation(); + + $this->assertInstanceOf(TaskRecommendationResponse::class, $recommendation); + $this->assertEquals(self::SOME_RECOMMENDATION_VALUE, $recommendation->getValue()); + $this->assertInstanceOf(TaskRecommendationReasonResponse::class, $recommendation->getReason()); + $this->assertEquals(self::SOME_RECOMMENDATION_REASON_VALUE, $recommendation->getReason()->getValue()); + $this->assertEquals(self::SOME_RECOMMENDATION_REASON_DETAIL, $recommendation->getReason()->getDetail()); } } diff --git a/tests/DocScan/Session/Retrieve/VerifyShareCodeTaskResponseTest.php b/tests/DocScan/Session/Retrieve/VerifyShareCodeTaskResponseTest.php new file mode 100644 index 00000000..b7d4dbca --- /dev/null +++ b/tests/DocScan/Session/Retrieve/VerifyShareCodeTaskResponseTest.php @@ -0,0 +1,62 @@ + self::VERIFY_SHARE_CODE_TASK, + 'id' => 'some-task-id', + 'state' => 'DONE', + 'created' => '2026-01-14T10:00:00Z', + 'last_updated' => '2026-01-14T11:00:00Z', + 'generated_media' => [ + ['id' => 'media-1', 'type' => 'PDF'], + ['id' => 'media-2', 'type' => 'IMAGE'], + ], + ]; + + $result = new VerifyShareCodeTaskResponse($input); + + $this->assertInstanceOf(TaskResponse::class, $result); + $this->assertEquals(self::VERIFY_SHARE_CODE_TASK, $result->getType()); + $this->assertEquals('some-task-id', $result->getId()); + $this->assertEquals('DONE', $result->getState()); + $this->assertNotNull($result->getCreated()); + $this->assertNotNull($result->getLastUpdated()); + $this->assertCount(2, $result->getGeneratedMedia()); + } + + /** + * @test + * @covers ::__construct + */ + public function shouldNotThrowExceptionWhenMissingValues() + { + $result = new VerifyShareCodeTaskResponse([]); + + $this->assertNull($result->getType()); + $this->assertNull($result->getId()); + $this->assertNull($result->getState()); + $this->assertNull($result->getCreated()); + $this->assertNull($result->getLastUpdated()); + $this->assertCount(0, $result->getGeneratedMedia()); + } +} diff --git a/tests/Http/AuthStrategy/BearerTokenStrategyTest.php b/tests/Http/AuthStrategy/BearerTokenStrategyTest.php new file mode 100644 index 00000000..6231f583 --- /dev/null +++ b/tests/Http/AuthStrategy/BearerTokenStrategyTest.php @@ -0,0 +1,68 @@ +createAuthHeaders(self::SOME_HTTP_METHOD, self::SOME_ENDPOINT, null); + + $this->assertArrayHasKey('Authorization', $headers); + $this->assertEquals('Bearer ' . self::SOME_TOKEN, $headers['Authorization']); + } + + /** + * @test + * @covers ::createQueryParams + */ + public function shouldReturnEmptyQueryParams() + { + $strategy = new BearerTokenStrategy(self::SOME_TOKEN); + $params = $strategy->createQueryParams(); + + $this->assertIsArray($params); + $this->assertEmpty($params); + } + + /** + * @test + * @covers ::__construct + */ + public function shouldThrowOnEmptyToken() + { + $this->expectException(\InvalidArgumentException::class); + new BearerTokenStrategy(''); + } + + /** + * @test + * @covers ::createAuthHeaders + */ + public function shouldReturnOnlyAuthorizationHeader() + { + $strategy = new BearerTokenStrategy(self::SOME_TOKEN); + $headers = $strategy->createAuthHeaders('POST', '/endpoint', null); + + $this->assertCount(1, $headers); + $this->assertArrayHasKey('Authorization', $headers); + } +} diff --git a/tests/Http/AuthStrategy/NoAuthStrategyTest.php b/tests/Http/AuthStrategy/NoAuthStrategyTest.php new file mode 100644 index 00000000..e671d467 --- /dev/null +++ b/tests/Http/AuthStrategy/NoAuthStrategyTest.php @@ -0,0 +1,40 @@ +createAuthHeaders('GET', '/endpoint', null); + + $this->assertIsArray($headers); + $this->assertEmpty($headers); + } + + /** + * @test + * @covers ::createQueryParams + */ + public function shouldReturnEmptyQueryParams() + { + $strategy = new NoAuthStrategy(); + $params = $strategy->createQueryParams(); + + $this->assertIsArray($params); + $this->assertEmpty($params); + } +} diff --git a/tests/Http/AuthStrategy/SignedRequestStrategyTest.php b/tests/Http/AuthStrategy/SignedRequestStrategyTest.php new file mode 100644 index 00000000..1d44b54b --- /dev/null +++ b/tests/Http/AuthStrategy/SignedRequestStrategyTest.php @@ -0,0 +1,133 @@ +createAuthHeaders(self::SOME_HTTP_METHOD, self::SOME_ENDPOINT, null); + + $this->assertArrayHasKey('X-Yoti-Auth-Digest', $headers); + $this->assertNotEmpty($headers['X-Yoti-Auth-Digest']); + } + + /** + * @test + * @covers ::createAuthHeaders + */ + public function shouldReturnDigestHeaderWithPayload() + { + $pemFile = PemFile::fromFilePath(TestData::PEM_FILE); + $strategy = new SignedRequestStrategy($pemFile); + + $payload = Payload::fromString('some payload content'); + $headers = $strategy->createAuthHeaders('POST', self::SOME_ENDPOINT, $payload); + + $this->assertArrayHasKey('X-Yoti-Auth-Digest', $headers); + $this->assertNotEmpty($headers['X-Yoti-Auth-Digest']); + } + + /** + * @test + * @covers ::createQueryParams + */ + public function shouldReturnNonceAndTimestampQueryParams() + { + $pemFile = PemFile::fromFilePath(TestData::PEM_FILE); + $strategy = new SignedRequestStrategy($pemFile); + + $params = $strategy->createQueryParams(); + + $this->assertArrayHasKey('nonce', $params); + $this->assertArrayHasKey('timestamp', $params); + $this->assertNotEmpty($params['nonce']); + $this->assertNotEmpty($params['timestamp']); + } + + /** + * @test + * @covers ::createQueryParams + */ + public function shouldIncludeNonceAsUuidFormat() + { + $pemFile = PemFile::fromFilePath(TestData::PEM_FILE); + $strategy = new SignedRequestStrategy($pemFile); + + $params = $strategy->createQueryParams(); + + // UUID v4 pattern: 8-4-4-4-12 hex chars + $this->assertMatchesRegularExpression( + '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', + $params['nonce'] + ); + } + + /** + * @test + * @covers ::__construct + * @covers ::createQueryParams + */ + public function shouldIncludeSdkIdWhenProvided() + { + $pemFile = PemFile::fromFilePath(TestData::PEM_FILE); + $strategy = new SignedRequestStrategy($pemFile, self::SOME_SDK_ID); + + $params = $strategy->createQueryParams(); + + $this->assertArrayHasKey('sdkId', $params); + $this->assertEquals(self::SOME_SDK_ID, $params['sdkId']); + } + + /** + * @test + * @covers ::createQueryParams + */ + public function shouldNotIncludeSdkIdWhenNotProvided() + { + $pemFile = PemFile::fromFilePath(TestData::PEM_FILE); + $strategy = new SignedRequestStrategy($pemFile); + + $params = $strategy->createQueryParams(); + + $this->assertArrayNotHasKey('sdkId', $params); + } + + /** + * @test + * @covers ::createQueryParams + */ + public function shouldReturnDifferentNonceEachTime() + { + $pemFile = PemFile::fromFilePath(TestData::PEM_FILE); + $strategy = new SignedRequestStrategy($pemFile); + + $params1 = $strategy->createQueryParams(); + $params2 = $strategy->createQueryParams(); + + $this->assertNotEquals($params1['nonce'], $params2['nonce']); + } +} diff --git a/tests/Http/RequestBuilderTest.php b/tests/Http/RequestBuilderTest.php index 75558344..a8b4e9f0 100644 --- a/tests/Http/RequestBuilderTest.php +++ b/tests/Http/RequestBuilderTest.php @@ -336,10 +336,13 @@ public function testBuildWithoutBaseUrl() public function testBuildWithoutPem() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Pem file must be provided to Yoti\\Http\\RequestBuilder'); + $this->expectExceptionMessage( + 'Either an AuthStrategy or a PEM file must be provided to Yoti\\Http\\RequestBuilder' + ); (new RequestBuilder()) ->withBaseUrl(self::SOME_BASE_URL) + ->withGet() ->build(); } diff --git a/tests/Profile/BaseProfileTest.php b/tests/Profile/BaseProfileTest.php index 71fce4bb..20c7b122 100644 --- a/tests/Profile/BaseProfileTest.php +++ b/tests/Profile/BaseProfileTest.php @@ -138,7 +138,7 @@ public function testGetAttributeById() $givenNamesAttribute = new ProtobufAttribute([ 'name' => self::SOME_ATTRIBUTE, - 'value' => utf8_decode('Alan'), + 'value' => mb_convert_encoding('Alan', 'ISO-8859-1', 'UTF-8'), 'content_type' => self::CONTENT_TYPE_STRING, ]); $newAttribute = AttributeConverter::convertToYotiAttribute($givenNamesAttribute); diff --git a/tests/Profile/Util/EncryptedDataTest.php b/tests/Profile/Util/EncryptedDataTest.php index 1680c9a8..4d1e8741 100644 --- a/tests/Profile/Util/EncryptedDataTest.php +++ b/tests/Profile/Util/EncryptedDataTest.php @@ -28,6 +28,11 @@ class EncrypedDataTest extends TestCase */ private $wrappedKey; + /** + * @var \Yoti\Protobuf\Compubapi\EncryptedData + */ + private $encryptedDataProto; + /** * Setup test data. */ diff --git a/tests/ShareUrl/Policy/WantedAttributeBuilderTest.php b/tests/ShareUrl/Policy/WantedAttributeBuilderTest.php index 5abb542e..2b1c20f3 100644 --- a/tests/ShareUrl/Policy/WantedAttributeBuilderTest.php +++ b/tests/ShareUrl/Policy/WantedAttributeBuilderTest.php @@ -32,6 +32,7 @@ public function testBuild() $wantedAttribute = (new WantedAttributeBuilder()) ->withName($someName) ->withDerivation($someDerivation) + ->withOptional(false) ->build(); $expectedJsonData = [ @@ -91,6 +92,7 @@ public function testAcceptSelfAsserted() $wantedAttributeDefault = (new WantedAttributeBuilder()) ->withName($someName) ->withAcceptSelfAsserted() + ->withOptional(false) ->build(); $this->assertEquals(json_encode($expectedJsonData), json_encode($wantedAttributeDefault)); @@ -124,6 +126,7 @@ public function testWithoutAcceptSelfAsserted() $wantedAttribute = (new WantedAttributeBuilder()) ->withName($someName) ->withAcceptSelfAsserted(false) + ->withOptional(false) ->build(); $this->assertEquals(json_encode($expectedJsonData), json_encode($wantedAttribute)); @@ -149,6 +152,7 @@ public function testWithConstraints() $wantedAttribute = (new WantedAttributeBuilder()) ->withName($someName) + ->withOptional(false) ->withConstraints($constraints) ->build(); diff --git a/tests/Util/JsonTest.php b/tests/Util/JsonTest.php index df232c6e..30a9b29b 100644 --- a/tests/Util/JsonTest.php +++ b/tests/Util/JsonTest.php @@ -81,18 +81,27 @@ public function testWithoutNullValues() */ public function testConvertFromLatin1ToUtf8Recursively() { - $latin1String = utf8_decode('éàê'); - $latin1Array = [utf8_decode('éàê'), utf8_decode('çî')]; - $nestedLatin1Array = [utf8_decode('éàê'), [utf8_decode('çî'), utf8_decode('üñ')]]; + $latin1String = mb_convert_encoding('éàê', 'ISO-8859-1', 'UTF-8'); + $latin1Array = [ + mb_convert_encoding('éàê', 'ISO-8859-1', 'UTF-8'), + mb_convert_encoding('çî', 'ISO-8859-1', 'UTF-8') + ]; + $nestedLatin1Array = [ + mb_convert_encoding('éàê', 'ISO-8859-1', 'UTF-8'), + [ + mb_convert_encoding('çî', 'ISO-8859-1', 'UTF-8'), + mb_convert_encoding('üñ', 'ISO-8859-1', 'UTF-8') + ] + ]; $latin1Object = new \stdClass(); - $latin1Object->property1 = utf8_decode('éàê'); - $latin1Object->property2 = utf8_decode('çî'); + $latin1Object->property1 = mb_convert_encoding('éàê', 'ISO-8859-1', 'UTF-8'); + $latin1Object->property2 = mb_convert_encoding('çî', 'ISO-8859-1', 'UTF-8'); $nestedLatin1Object = new \stdClass(); - $nestedLatin1Object->property = utf8_decode('çî'); + $nestedLatin1Object->property = mb_convert_encoding('çî', 'ISO-8859-1', 'UTF-8'); $latin1ObjectWithNestedObject = new \stdClass(); - $latin1ObjectWithNestedObject->property1 = utf8_decode('éàê'); + $latin1ObjectWithNestedObject->property1 = mb_convert_encoding('éàê', 'ISO-8859-1', 'UTF-8'); $latin1ObjectWithNestedObject->property2 = $nestedLatin1Object; $this->assertSame('éàê', Json::convertFromLatin1ToUtf8Recursively($latin1String));