[
  {
    "path": ".gitignore",
    "content": ".idea\ntests/tmp/*\ntests/_output/*\npublic/*\nvendor\n\ntests/_data/non_readable_file"
  },
  {
    "path": ".travis.yml",
    "content": "dist: bionic\nlanguage: php\naddons:\n  apt:\n    packages:\n    - libengine-gost-openssl1.1\n\nbefore_install:\n    - sudo bash tests/.configure-gost-openssl.sh\n\nphp:\n  - 7.1\n  - 7.2\n  - 7.3\n  - 7.4\n  - 8.0\n\ninstall:\n  - travis_retry composer self-update\n  - travis_retry composer --version\n  - travis_retry composer update --prefer-dist --no-interaction\n\nscript:\n  - chmod 000 tests/_data/non_readable_file\n  - php vendor/codeception/codeception/codecept run\n"
  },
  {
    "path": "README.md",
    "content": "\n# Единая система идентификации и аутентификации (ЕСИА) OpenId \n\n[![Build Status](https://travis-ci.org/fr05t1k/esia.svg?branch=master)](https://travis-ci.org/fr05t1k/esia)\n\n# Описание\nКомпонент для авторизации на портале \"Госуслуги\".\n\n# Внимание!\nПолучив токен вы можете выполнять любые API запросы. Библиотека не поддерживает все существующие методы в API, а предоставляет только самые базовые. Основная цель библиотеки - получение токена.\n\n# Установка\n\nПри помощи [composer](https://getcomposer.org/download/):\n```\ncomposer require --prefer-dist fr05t1k/esia\n```\nИли добавьте в composer.json\n\n```\n\"fr05t1k/esia\" : \"^2.0\"\n```\n\n# Как использовать \n\nПример получения ссылки для авторизации\n```php\n<?php \n$config = new \\Esia\\Config([\n  'clientId' => 'INSP03211',\n  'redirectUrl' => 'http://my-site.com/response.php',\n  'portalUrl' => 'https://esia-portal1.test.gosuslugi.ru/',\n  'scope' => ['fullname', 'birthdate'],\n]);\n$esia = new \\Esia\\OpenId($config);\n$esia->setSigner(new \\Esia\\Signer\\SignerPKCS7(\n    'my-site.com.pem',\n    'my-site.com.pem',\n    'password',\n    '/tmp'\n));\n?>\n\n<a href=\"<?=$esia->buildUrl()?>\">Войти через портал госуслуги</a>\n```\n\nПосле редиректа на ваш `redirectUrl` вы получите в `$_GET['code']` код для получения токена\n\nПример получения токена и информации о пользователе\n\n```php\n\n$esia = new \\Esia\\OpenId($config);\n\n// Вы можете использовать токен в дальнейшем вместе с oid \n$token = $esia->getToken($_GET['code']);\n\n$personInfo = $esia->getPersonInfo();\n$addressInfo = $esia->getAddressInfo();\n$contactInfo = $esia->getContactInfo();\n$documentInfo = $esia->getDocInfo();\n\n```\n# Конфиг\n\n`clientId` - ID вашего приложения.\n\n`redirectUrl` - URL куда будет перенаправлен ответ с кодом.\n\n`portalUrl` - по умолчанию: `https://esia-portal1.test.gosuslugi.ru/`. Домен портала для авторизация (только домен).\n\n`codeUrlPath` - по умолчанию: `aas/oauth2/ac`. URL для получения кода.\n\n`tokenUrlPath` - по умолчанию: `aas/oauth2/te`. URL для получение токена.\n\n`scope` - по умолчанию: `fullname birthdate gender email mobile id_doc snils inn`. Запрашиваемые права у пользователя.\n\n`privateKeyPath` - путь до приватного ключа.\n\n`privateKeyPassword` - пароль от приватного ключа.\n\n`certPath` - путь до сертификата.\n\n`tmpPath` - путь до дериктории где будет проходить подпись (должна быть доступна для записи).\n\n# Токен и oid\n\nТокен - jwt токен которые вы получаете от ЕСИА для дальнейшего взаимодействия\n\noid - уникальный идентификатор владельца токена\n\n## Как получить oid?\nЕсли 2 способа:\n1. oid содержится в jwt токене, расшифровав его\n2. После получения токена oid сохраняется в config и получить можно так \n```php\n$esia->getConfig()->getOid();\n```\n\n## Переиспользование Токена\n\nДополнительно укажите токен и идентификатор в конфиге\n```php\n$config->setToken($jwt);\n$config->setOid($oid);\n```\n"
  },
  {
    "path": "_config.yml",
    "content": "theme: jekyll-theme-cayman"
  },
  {
    "path": "codeception.yml",
    "content": "actor: Tester\npaths:\n    tests: tests\n    log: tests/_output\n    data: tests/_data\n    support: tests/_support\n    envs: tests/_envs\nbootstrap: _bootstrap.php\nsettings:\n    colors: true\n    memory_limit: 1024M\nextensions:\n    enabled:\n        - Codeception\\Extension\\RunFailed\ncoverage:\n    enabled: true\n    remote: false\n    whitelist:\n            include:\n                - src/*\n\n"
  },
  {
    "path": "composer.json",
    "content": "{\n  \"name\": \"fr05t1k/esia\",\n  \"license\": \"MIT\",\n  \"description\": \"OpenID ESIA authenticating\",\n  \"keywords\": [\n    \"esia\",\n    \"openid\",\n    \"egov\"\n  ],\n  \"autoload\": {\n    \"psr-4\": {\n      \"Esia\\\\\": \"src/Esia\"\n    }\n  },\n  \"autoload-dev\": {\n    \"psr-4\": {\n      \"tests\\\\\" : \"tests\"\n    }\n  },\n  \"require\": {\n    \"php\": \"^7.1|^8.0\",\n    \"guzzlehttp/guzzle\": \"^6.1.0|^7.0\",\n    \"psr/log\": \"^1.0\",\n    \"psr/http-message\": \"^1.0\",\n    \"psr/http-client\": \"^1.0\"\n  },\n  \"suggest\": {\n    \"ext-openssl\": \"SignerPKCS7 support\"\n  },\n  \"require-dev\": {\n    \"roave/security-advisories\": \"dev-latest\",\n    \"codeception/codeception\": \"^4.0\",\n    \"codeception/module-asserts\": \"^1.3\"\n  }\n}\n"
  },
  {
    "path": "src/Esia/Config.php",
    "content": "<?php\n\nnamespace Esia;\n\nuse Esia\\Exceptions\\InvalidConfigurationException;\n\nclass Config\n{\n    private $clientId;\n    private $redirectUrl;\n    private $privateKeyPath;\n    private $certPath;\n\n    private $portalUrl = 'http://esia-portal1.test.gosuslugi.ru/';\n    private $tokenUrlPath = 'aas/oauth2/te';\n    private $codeUrlPath = 'aas/oauth2/ac';\n    private $personUrlPath = 'rs/prns';\n    private $logoutUrlPath = 'idp/ext/Logout';\n    private $privateKeyPassword = '';\n\n    private $scope = [\n        'fullname',\n        'birthdate',\n        'gender',\n        'email',\n        'mobile',\n        'id_doc',\n        'snils',\n        'inn',\n    ];\n\n    private $tmpPath = '/var/tmp';\n\n    private $responseType = 'code';\n    private $accessType = 'offline';\n\n    private $token = '';\n    private $oid = '';\n\n    /**\n     * Config constructor.\n     *\n     * @throws InvalidConfigurationException\n     */\n    public function __construct(array $config = [])\n    {\n        // Required params\n        $this->clientId = $config['clientId'] ?? $this->clientId;\n        if (!$this->clientId) {\n            throw new InvalidConfigurationException('Please provide clientId');\n        }\n\n        $this->redirectUrl = $config['redirectUrl'] ?? $this->redirectUrl;\n        if (!$this->redirectUrl) {\n            throw new InvalidConfigurationException('Please provide redirectUrl');\n        }\n\n        $this->privateKeyPath = $config['privateKeyPath'] ?? $this->privateKeyPath;\n        if (!$this->privateKeyPath) {\n            throw new InvalidConfigurationException('Please provide privateKeyPath');\n        }\n        $this->certPath = $config['certPath'] ?? $this->certPath;\n        if (!$this->certPath) {\n            throw new InvalidConfigurationException('Please provide certPath');\n        }\n\n        $this->portalUrl = $config['portalUrl'] ?? $this->portalUrl;\n        $this->tokenUrlPath = $config['tokenUrlPath'] ?? $this->tokenUrlPath;\n        $this->codeUrlPath = $config['codeUrlPath'] ?? $this->codeUrlPath;\n        $this->personUrlPath = $config['personUrlPath'] ?? $this->personUrlPath;\n        $this->logoutUrlPath = $config['logoutUrlPath'] ?? $this->logoutUrlPath;\n        $this->privateKeyPassword = $config['privateKeyPassword'] ?? $this->privateKeyPassword;\n        $this->oid = $config['oid'] ?? $this->oid;\n        $this->scope = $config['scope'] ?? $this->scope;\n        if (!is_array($this->scope)) {\n            throw new InvalidConfigurationException('scope must be array of strings');\n        }\n\n        $this->responseType = $config['responseType'] ?? $this->responseType;\n        $this->accessType = $config['accessType'] ?? $this->accessType;\n        $this->tmpPath = $config['tmpPath'] ?? $this->tmpPath;\n        $this->token = $config['token'] ?? $this->token;\n    }\n\n    public function getPortalUrl(): string\n    {\n        return $this->portalUrl;\n    }\n\n    public function getPrivateKeyPath(): string\n    {\n        return $this->privateKeyPath;\n    }\n\n    public function getPrivateKeyPassword(): string\n    {\n        return $this->privateKeyPassword;\n    }\n\n    public function getCertPath(): string\n    {\n        return $this->certPath;\n    }\n    \n    public function getOid(): string\n    {\n        return $this->oid;\n    }\n\n    public function setOid(string $oid): void\n    {\n        $this->oid = $oid;\n    }\n\n    public function getScope(): array\n    {\n        return $this->scope;\n    }\n\n    public function getScopeString(): string\n    {\n        return implode(' ', $this->scope);\n    }\n\n    public function getResponseType(): string\n    {\n        return $this->responseType;\n    }\n\n    public function getAccessType(): string\n    {\n        return $this->accessType;\n    }\n\n    public function getTmpPath(): string\n    {\n        return $this->tmpPath;\n    }\n\n    public function getToken(): ?string\n    {\n        return $this->token;\n    }\n\n    public function setToken(string $token): void\n    {\n        $this->token = $token;\n    }\n\n    public function getClientId(): string\n    {\n        return $this->clientId;\n    }\n\n    public function getRedirectUrl(): string\n    {\n        return $this->redirectUrl;\n    }\n\n    /**\n     * Return an url for request to get an access token\n     */\n    public function getTokenUrl(): string\n    {\n        return $this->portalUrl . $this->tokenUrlPath;\n    }\n\n    /**\n     * Return an url for request to get an authorization code\n     */\n    public function getCodeUrl(): string\n    {\n        return $this->portalUrl . $this->codeUrlPath;\n    }\n\n    /**\n     * @return string\n     * @throws InvalidConfigurationException\n     */\n    public function getPersonUrl(): string\n    {\n        if (!$this->oid) {\n            throw new InvalidConfigurationException('Please provide oid');\n        }\n        return $this->portalUrl . $this->personUrlPath . '/' . $this->oid;\n    }\n\n    /**\n     * Return an url for logout\n     */\n    public function getLogoutUrl(): string\n    {\n        return $this->portalUrl . $this->logoutUrlPath;\n    }\n}\n"
  },
  {
    "path": "src/Esia/Exceptions/AbstractEsiaException.php",
    "content": "<?php\n\nnamespace Esia\\Exceptions;\n\nuse Exception;\n\nabstract class AbstractEsiaException extends Exception\n{\n}\n"
  },
  {
    "path": "src/Esia/Exceptions/ForbiddenException.php",
    "content": "<?php\n\nnamespace Esia\\Exceptions;\n\nclass ForbiddenException extends AbstractEsiaException\n{\n    protected function getMessageForCode(int $code): string\n    {\n        return 'Forbidden';\n    }\n}\n"
  },
  {
    "path": "src/Esia/Exceptions/InvalidConfigurationException.php",
    "content": "<?php\n\nnamespace Esia\\Exceptions;\n\nclass InvalidConfigurationException extends AbstractEsiaException\n{\n}\n"
  },
  {
    "path": "src/Esia/Exceptions/RequestFailException.php",
    "content": "<?php\n\nnamespace Esia\\Exceptions;\n\nclass RequestFailException extends AbstractEsiaException\n{\n}\n"
  },
  {
    "path": "src/Esia/Http/Exceptions/HttpException.php",
    "content": "<?php\n\nnamespace Esia\\Http\\Exceptions;\n\nuse Psr\\Http\\Client\\ClientExceptionInterface;\nuse RuntimeException;\n\nclass HttpException extends RuntimeException implements ClientExceptionInterface\n{\n}\n"
  },
  {
    "path": "src/Esia/Http/GuzzleHttpClient.php",
    "content": "<?php\n\nnamespace Esia\\Http;\n\nuse Esia\\Http\\Exceptions\\HttpException;\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\Exception\\GuzzleException;\nuse Psr\\Http\\Client\\ClientExceptionInterface;\nuse Psr\\Http\\Client\\ClientInterface;\nuse Psr\\Http\\Message\\RequestInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\n\nclass GuzzleHttpClient implements ClientInterface\n{\n    /**\n     * @var Client\n     */\n    private $guzzle;\n\n    /**\n     * GuzzleHttpClient constructor.\n     */\n    public function __construct(Client $guzzle)\n    {\n        $this->guzzle = $guzzle;\n    }\n\n    /**\n     * Sends a PSR-7 request and returns a PSR-7 response.\n     *\n     * Every technically correct HTTP response MUST be returned as is, even if it represents a HTTP\n     * error response or a redirect instruction. The only special case is 1xx responses, which MUST\n     * be assembled in the HTTP client.\n     *\n     * The client MAY do modifications to the Request before sending it. Because PSR-7 objects are\n     * immutable, one cannot assume that the object passed to ClientInterface::sendRequest() will be the same\n     * object that is actually sent. For example the Request object that is returned by an exception MAY\n     * be a different object than the one passed to sendRequest, so comparison by reference (===) is not possible.\n     *\n     * {@link https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message-meta.md#why-value-objects}\n     *\n     * @param RequestInterface $request\n     *\n     * @return ResponseInterface\n     *\n     * @throws ClientExceptionInterface If an error happens during processing the request.\n     */\n    public function sendRequest(RequestInterface $request): ResponseInterface\n    {\n        try {\n            return $this->guzzle->send($request);\n        } catch (GuzzleException $e) {\n            throw new HttpException($e->getMessage(), $e->getCode(), $e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Esia/OpenId.php",
    "content": "<?php\n\nnamespace Esia;\n\nuse Esia\\Exceptions\\AbstractEsiaException;\nuse Esia\\Exceptions\\ForbiddenException;\nuse Esia\\Exceptions\\RequestFailException;\nuse Esia\\Http\\GuzzleHttpClient;\nuse Esia\\Signer\\Exceptions\\CannotGenerateRandomIntException;\nuse Esia\\Signer\\Exceptions\\SignFailException;\nuse Esia\\Signer\\SignerInterface;\nuse Esia\\Signer\\SignerPKCS7;\nuse Exception;\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\Exception\\BadResponseException;\nuse GuzzleHttp\\Psr7\\Request;\nuse InvalidArgumentException;\nuse Psr\\Http\\Client\\ClientExceptionInterface;\nuse Psr\\Http\\Client\\ClientInterface;\nuse Psr\\Http\\Message\\RequestInterface;\nuse Psr\\Log\\LoggerAwareTrait;\nuse Psr\\Log\\NullLogger;\nuse RuntimeException;\n\n/**\n * Class OpenId\n */\nclass OpenId\n{\n    use LoggerAwareTrait;\n\n    /**\n     * @var SignerInterface\n     */\n    private $signer;\n\n    /**\n     * Http Client\n     *\n     * @var ClientInterface\n     */\n    private $client;\n\n    /**\n     * Config\n     *\n     * @var Config\n     */\n    private $config;\n\n    public function __construct(Config $config, ClientInterface $client = null)\n    {\n        $this->config = $config;\n        $this->client = $client ?? new GuzzleHttpClient(new Client());\n        $this->logger = new NullLogger();\n        $this->signer = new SignerPKCS7(\n            $config->getCertPath(),\n            $config->getPrivateKeyPath(),\n            $config->getPrivateKeyPassword(),\n            $config->getTmpPath()\n        );\n    }\n\n    /**\n     * Replace default signer\n     */\n    public function setSigner(SignerInterface $signer): void\n    {\n        $this->signer = $signer;\n    }\n\n    /**\n     * Get config\n     */\n    public function getConfig(): Config\n    {\n        return $this->config;\n    }\n\n    /**\n     * Return an url for authentication\n     *\n     * ```php\n     *     <a href=\"<?=$esia->buildUrl()?>\">Login</a>\n     * ```\n     *\n     * @return string|false\n     * @throws SignFailException\n     */\n    public function buildUrl()\n    {\n        $timestamp = $this->getTimeStamp();\n        $state = $this->buildState();\n        $message = $this->config->getScopeString()\n            . $timestamp\n            . $this->config->getClientId()\n            . $state;\n\n        $clientSecret = $this->signer->sign($message);\n\n        $url = $this->config->getCodeUrl() . '?%s';\n\n        $params = [\n            'client_id' => $this->config->getClientId(),\n            'client_secret' => $clientSecret,\n            'redirect_uri' => $this->config->getRedirectUrl(),\n            'scope' => $this->config->getScopeString(),\n            'response_type' => $this->config->getResponseType(),\n            'state' => $state,\n            'access_type' => $this->config->getAccessType(),\n            'timestamp' => $timestamp,\n        ];\n\n        $request = http_build_query($params);\n\n        return sprintf($url, $request);\n    }\n\n    /**\n     * Return an url for logout\n     */\n    public function buildLogoutUrl(string $redirectUrl = null): string\n    {\n        $url = $this->config->getLogoutUrl() . '?%s';\n        $params = [\n            'client_id' => $this->config->getClientId(),\n        ];\n\n        if ($redirectUrl) {\n            $params['redirect_url'] = $redirectUrl;\n        }\n\n        $request = http_build_query($params);\n\n        return sprintf($url, $request);\n    }\n\n    /**\n     * Method collect a token with given code\n     *\n     * @throws SignFailException\n     * @throws AbstractEsiaException\n     */\n    public function getToken(string $code): string\n    {\n        $timestamp = $this->getTimeStamp();\n        $state = $this->buildState();\n\n        $clientSecret = $this->signer->sign(\n            $this->config->getScopeString()\n            . $timestamp\n            . $this->config->getClientId()\n            . $state\n        );\n\n        $body = [\n            'client_id' => $this->config->getClientId(),\n            'code' => $code,\n            'grant_type' => 'authorization_code',\n            'client_secret' => $clientSecret,\n            'state' => $state,\n            'redirect_uri' => $this->config->getRedirectUrl(),\n            'scope' => $this->config->getScopeString(),\n            'timestamp' => $timestamp,\n            'token_type' => 'Bearer',\n            'refresh_token' => $state,\n        ];\n\n        $payload = $this->sendRequest(\n            new Request(\n                'POST',\n                $this->config->getTokenUrl(),\n                [\n                    'Content-Type' => 'application/x-www-form-urlencoded',\n                ],\n                http_build_query($body)\n            )\n        );\n\n        $this->logger->debug('Payload: ', $payload);\n\n        $token = $payload['access_token'];\n        $this->config->setToken($token);\n\n        # get object id from token\n        $chunks = explode('.', $token);\n        $payload = json_decode($this->base64UrlSafeDecode($chunks[1]), true);\n        $this->config->setOid($payload['urn:esia:sbj_id']);\n\n        return $token;\n    }\n\n    /**\n     * Fetch person info from current person\n     *\n     * You must collect token person before\n     * calling this method\n     *\n     * @throws AbstractEsiaException\n     */\n    public function getPersonInfo(): array\n    {\n        $url = $this->config->getPersonUrl();\n\n        return $this->sendRequest(new Request('GET', $url));\n    }\n\n    /**\n     * Fetch contact info about current person\n     *\n     * You must collect token person before\n     * calling this method\n     *\n     * @throws Exceptions\\InvalidConfigurationException\n     * @throws AbstractEsiaException\n     */\n    public function getContactInfo(): array\n    {\n        $url = $this->config->getPersonUrl() . '/ctts';\n        $payload = $this->sendRequest(new Request('GET', $url));\n\n        if ($payload && $payload['size'] > 0) {\n            return $this->collectArrayElements($payload['elements']);\n        }\n\n        return $payload;\n    }\n\n\n    /**\n     * Fetch address from current person\n     *\n     * You must collect token person before\n     * calling this method\n     *\n     * @throws Exceptions\\InvalidConfigurationException\n     * @throws AbstractEsiaException\n     */\n    public function getAddressInfo(): array\n    {\n        $url = $this->config->getPersonUrl() . '/addrs';\n        $payload = $this->sendRequest(new Request('GET', $url));\n\n        if ($payload['size'] > 0) {\n            return $this->collectArrayElements($payload['elements']);\n        }\n\n        return $payload;\n    }\n\n    /**\n     * Fetch documents info about current person\n     *\n     * You must collect token person before\n     * calling this method\n     *\n     * @throws Exceptions\\InvalidConfigurationException\n     * @throws AbstractEsiaException\n     */\n    public function getDocInfo(): array\n    {\n        $url = $this->config->getPersonUrl() . '/docs';\n\n        $payload = $this->sendRequest(new Request('GET', $url));\n\n        if ($payload && $payload['size'] > 0) {\n            return $this->collectArrayElements($payload['elements']);\n        }\n\n        return $payload;\n    }\n\n    /**\n     * This method can iterate on each element\n     * and fetch entities from esia by url\n     *\n     * @throws AbstractEsiaException\n     */\n    private function collectArrayElements($elements): array\n    {\n        $result = [];\n        foreach ($elements as $elementUrl) {\n            $elementPayload = $this->sendRequest(new Request('GET', $elementUrl));\n\n            if ($elementPayload) {\n                $result[] = $elementPayload;\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * @throws AbstractEsiaException\n     */\n    private function sendRequest(RequestInterface $request): array\n    {\n        try {\n            if ($this->config->getToken()) {\n                /** @noinspection CallableParameterUseCaseInTypeContextInspection */\n                $request = $request->withHeader('Authorization', 'Bearer ' . $this->config->getToken());\n            }\n            $response = $this->client->sendRequest($request);\n            $responseBody = json_decode($response->getBody()->getContents(), true);\n\n            if (!is_array($responseBody)) {\n                throw new RuntimeException(\n                    sprintf(\n                        'Cannot decode response body. JSON error (%d): %s',\n                        json_last_error(),\n                        json_last_error_msg()\n                    )\n                );\n            }\n\n            return $responseBody;\n        } catch (ClientExceptionInterface $e) {\n            $this->logger->error('Request was failed', ['exception' => $e]);\n            $prev = $e->getPrevious();\n\n            // Only for Guzzle\n            if ($prev instanceof BadResponseException\n                && $prev->getResponse() !== null\n                && $prev->getResponse()->getStatusCode() === 403\n            ) {\n                throw new ForbiddenException('Request is forbidden', 0, $e);\n            }\n\n            throw new RequestFailException('Request is failed', 0, $e);\n        } catch (RuntimeException $e) {\n            $this->logger->error('Cannot read body', ['exception' => $e]);\n            throw new RequestFailException('Cannot read body', 0, $e);\n        } catch (InvalidArgumentException $e) {\n            $this->logger->error('Wrong header', ['exception' => $e]);\n            throw new RequestFailException('Wrong header', 0, $e);\n        }\n    }\n\n    private function getTimeStamp(): string\n    {\n        return date('Y.m.d H:i:s O');\n    }\n\n    /**\n     * Generate state with uuid\n     *\n     * @throws SignFailException\n     */\n    private function buildState(): string\n    {\n        try {\n            return sprintf(\n                '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',\n                random_int(0, 0xffff),\n                random_int(0, 0xffff),\n                random_int(0, 0xffff),\n                random_int(0, 0x0fff) | 0x4000,\n                random_int(0, 0x3fff) | 0x8000,\n                random_int(0, 0xffff),\n                random_int(0, 0xffff),\n                random_int(0, 0xffff)\n            );\n        } catch (Exception $e) {\n            throw new CannotGenerateRandomIntException('Cannot generate random integer', $e);\n        }\n    }\n\n    /**\n     * Url safe for base64\n     */\n    private function base64UrlSafeDecode(string $string): string\n    {\n        $base64 = strtr($string, '-_', '+/');\n\n        return base64_decode($base64);\n    }\n}\n"
  },
  {
    "path": "src/Esia/Signer/AbstractSignerPKCS7.php",
    "content": "<?php\n\nnamespace Esia\\Signer;\n\nuse Esia\\Signer\\Exceptions\\CannotReadCertificateException;\nuse Esia\\Signer\\Exceptions\\CannotReadPrivateKeyException;\nuse Esia\\Signer\\Exceptions\\NoSuchCertificateFileException;\nuse Esia\\Signer\\Exceptions\\NoSuchKeyFileException;\nuse Esia\\Signer\\Exceptions\\NoSuchTmpDirException;\nuse Esia\\Signer\\Exceptions\\SignFailException;\nuse Psr\\Log\\LoggerAwareTrait;\nuse Psr\\Log\\NullLogger;\n\nabstract class AbstractSignerPKCS7\n{\n    use LoggerAwareTrait;\n\n    /**\n     * Path to the certificate\n     *\n     * @var string\n     */\n    protected $certPath;\n\n    /**\n     * Path to the private key\n     *\n     * @var string\n     */\n    protected $privateKeyPath;\n\n    /**\n     * Password for the private key\n     *\n     * @var string\n     */\n    protected $privateKeyPassword;\n\n    /**\n     * SignerPKCS7 constructor.\n     */\n    public function __construct(\n        string $certPath,\n        string $privateKeyPath,\n        ?string $privateKeyPassword,\n        string $tmpPath\n    ) {\n        $this->certPath = $certPath;\n        $this->privateKeyPath = $privateKeyPath;\n        $this->privateKeyPassword = $privateKeyPassword;\n        $this->tmpPath = $tmpPath;\n        $this->logger = new NullLogger();\n    }\n\n    /**\n     * Temporary directory for message signing (must me writable)\n     *\n     * @var string\n     */\n    protected $tmpPath;\n\n    /**\n     * @throws SignFailException\n     */\n    protected function checkFilesExists(): void\n    {\n        if (!file_exists($this->certPath)) {\n            throw new NoSuchCertificateFileException('Certificate does not exist');\n        }\n        if (!is_readable($this->certPath)) {\n            throw new CannotReadCertificateException('Cannot read the certificate');\n        }\n        if (!file_exists($this->privateKeyPath)) {\n            throw new NoSuchKeyFileException('Private key does not exist');\n        }\n        if (!is_readable($this->privateKeyPath)) {\n            throw new CannotReadPrivateKeyException('Cannot read the private key');\n        }\n        if (!file_exists($this->tmpPath)) {\n            throw new NoSuchTmpDirException('Temporary folder is not found');\n        }\n        if (!is_writable($this->tmpPath)) {\n            throw new NoSuchTmpDirException('Temporary folder is not writable');\n        }\n    }\n\n    /**\n     * Generate random unique string\n     */\n    protected function getRandomString(): string\n    {\n        return md5(uniqid(mt_rand(), true));\n    }\n\n    /**\n     * Url safe for base64\n     */\n    protected function urlSafe(string $string): string\n    {\n        return rtrim(strtr(trim($string), '+/', '-_'), '=');\n    }\n}\n"
  },
  {
    "path": "src/Esia/Signer/CliSignerPKCS7.php",
    "content": "<?php\n\nnamespace Esia\\Signer;\n\nuse Esia\\Signer\\Exceptions\\SignFailException;\n\nclass CliSignerPKCS7 extends AbstractSignerPKCS7 implements SignerInterface\n{\n    /**\n     * @throws SignFailException\n     */\n    public function sign(string $message): string\n    {\n        $this->checkFilesExists();\n\n        // random unique directories for sign\n        $messageFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString();\n        $signFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString();\n        file_put_contents($messageFile, $message);\n\n        $this->run(\n            'openssl ' .\n            'smime -engine gost -sign -binary -outform DER -noattr ' .\n            '-signer ' . escapeshellarg($this->certPath) . ' ' .\n            '-inkey ' . escapeshellarg($this->privateKeyPath) . ' ' .\n            '-passin ' . escapeshellarg('pass:' . $this->privateKeyPassword) . ' ' .\n            '-in ' . escapeshellarg($messageFile) . ' ' .\n            '-out ' . escapeshellarg($signFile)\n        );\n\n        $signed = file_get_contents($signFile);\n        if ($signed === false) {\n            $message = sprintf('cannot read %s file', $signFile);\n            $this->logger->error($message);\n            throw new SignFailException($message);\n        }\n        $sign = $this->urlSafe(base64_encode($signed));\n\n        unlink($signFile);\n        unlink($messageFile);\n        return $sign;\n    }\n\n    /**\n     * @throws SignFailException\n     */\n    private function run(string $command): void\n    {\n        $process = proc_open(\n            $command,\n            [\n                ['pipe', 'w'], // stdout\n                ['pipe', 'w'], // stderr\n            ],\n            $pipes\n        );\n\n        $result = stream_get_contents($pipes[0]);\n        fclose($pipes[0]);\n\n        $errors = stream_get_contents($pipes[1]);\n        fclose($pipes[1]);\n\n        $code = proc_close($process);\n        if (0 !== $code || $result === false) {\n            $errors = $errors ?: 'unknown';\n            $this->logger->error('Sign fail');\n            $this->logger->error('SSL error: ' . $errors);\n            throw new SignFailException($errors);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Esia/Signer/Exceptions/CannotGenerateRandomIntException.php",
    "content": "<?php\n\nnamespace Esia\\Signer\\Exceptions;\n\nclass CannotGenerateRandomIntException extends SignFailException\n{\n}\n"
  },
  {
    "path": "src/Esia/Signer/Exceptions/CannotReadCertificateException.php",
    "content": "<?php\n\nnamespace Esia\\Signer\\Exceptions;\n\nclass CannotReadCertificateException extends SignFailException\n{\n}\n"
  },
  {
    "path": "src/Esia/Signer/Exceptions/CannotReadPrivateKeyException.php",
    "content": "<?php\n\nnamespace Esia\\Signer\\Exceptions;\n\nclass CannotReadPrivateKeyException extends SignFailException\n{\n}\n"
  },
  {
    "path": "src/Esia/Signer/Exceptions/NoSuchCertificateFileException.php",
    "content": "<?php\n\nnamespace Esia\\Signer\\Exceptions;\n\nclass NoSuchCertificateFileException extends SignFailException\n{\n}\n"
  },
  {
    "path": "src/Esia/Signer/Exceptions/NoSuchKeyFileException.php",
    "content": "<?php\n\nnamespace Esia\\Signer\\Exceptions;\n\nclass NoSuchKeyFileException extends SignFailException\n{\n}\n"
  },
  {
    "path": "src/Esia/Signer/Exceptions/NoSuchTmpDirException.php",
    "content": "<?php\n\nnamespace Esia\\Signer\\Exceptions;\n\nclass NoSuchTmpDirException extends SignFailException\n{\n}\n"
  },
  {
    "path": "src/Esia/Signer/Exceptions/SignFailException.php",
    "content": "<?php\n\nnamespace Esia\\Signer\\Exceptions;\n\nuse Esia\\Exceptions\\AbstractEsiaException;\n\nclass SignFailException extends AbstractEsiaException\n{\n    protected function getMessageForCode(int $code): string\n    {\n        return 'Signing is failed';\n    }\n}\n"
  },
  {
    "path": "src/Esia/Signer/SignerInterface.php",
    "content": "<?php\n\nnamespace Esia\\Signer;\n\nuse Esia\\Signer\\Exceptions\\SignFailException;\n\ninterface SignerInterface\n{\n    /**\n     * @throws SignFailException\n     */\n    public function sign(string $message): string;\n}\n"
  },
  {
    "path": "src/Esia/Signer/SignerPKCS7.php",
    "content": "<?php\n\nnamespace Esia\\Signer;\n\nuse Esia\\Signer\\Exceptions\\CannotReadCertificateException;\nuse Esia\\Signer\\Exceptions\\CannotReadPrivateKeyException;\nuse Esia\\Signer\\Exceptions\\SignFailException;\n\nclass SignerPKCS7 extends AbstractSignerPKCS7 implements SignerInterface\n{\n    private $pkcs7Flags = PKCS7_DETACHED;\n\n    public function addPKCS7Flag(int $pkcs7Flag): void\n    {\n        $this->pkcs7Flags |= $pkcs7Flag;\n    }\n\n    /**\n     * @throws SignFailException\n     */\n    public function sign(string $message): string\n    {\n        $this->checkFilesExists();\n\n        $certContent = file_get_contents($this->certPath);\n        $keyContent = file_get_contents($this->privateKeyPath);\n\n        $cert = openssl_x509_read($certContent);\n\n        if ($cert === false) {\n            throw new CannotReadCertificateException('Cannot read the certificate: ' . openssl_error_string());\n        }\n\n        $this->logger->debug('Cert: ' . print_r($cert, true), ['cert' => $cert]);\n\n        $privateKey = openssl_pkey_get_private($keyContent, $this->privateKeyPassword);\n\n        if ($privateKey === false) {\n            throw new CannotReadPrivateKeyException('Cannot read the private key: ' . openssl_error_string());\n        }\n\n        $this->logger->debug('Private key: : ' . print_r($privateKey, true), ['privateKey' => $privateKey]);\n\n        // random unique directories for sign\n        $messageFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString();\n        $signFile = $this->tmpPath . DIRECTORY_SEPARATOR . $this->getRandomString();\n        file_put_contents($messageFile, $message);\n        $signResult = openssl_pkcs7_sign(\n            $messageFile,\n            $signFile,\n            $cert,\n            $privateKey,\n            [],\n            $this->pkcs7Flags\n        );\n\n        if ($signResult) {\n            $this->logger->debug('Sign success');\n        } else {\n            $this->logger->error('Sign fail');\n            $this->logger->error('SSL error: ' . openssl_error_string());\n            throw new SignFailException('Cannot sign the message');\n        }\n\n        $signed = file_get_contents($signFile);\n\n        # split by section\n        $signed = explode(\"\\n\\n\", $signed);\n\n        # get third section which contains sign and join into one line\n        $sign = str_replace(\"\\n\", '', $this->urlSafe($signed[3]));\n\n        unlink($signFile);\n        unlink($messageFile);\n\n        return $sign;\n    }\n}\n"
  },
  {
    "path": "tests/.configure-gost-openssl.sh",
    "content": "sed -i '1iopenssl_conf=openssl_def' /usr/lib/ssl/openssl.cnf\ntee -a /usr/lib/ssl/openssl.cnf <<EOF\n\n[openssl_def]\nengines = engine_section\n\n[engine_section]\ngost = gost_section\n\n[gost_section]\nengine_id = gost\ndynamic_path = /usr/lib/x86_64-linux-gnu/engines-1.1/gost.so\ndefault_algorithms = ALL\nCRYPT_PARAMS = id-Gost28147-89-CryptoPro-A-ParamSet\n\nEOF\n"
  },
  {
    "path": "tests/_bootstrap.php",
    "content": "<?php\n// This is global bootstrap for autoloading\nrequire_once __DIR__ . '/../vendor/autoload.php';\n"
  },
  {
    "path": "tests/_data/server-gost.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBnDCCAUmgAwIBAgIJAP+MfsgMHfr6MAoGBiqFAwICAwUAMCgxCzAJBgNVBAYT\nAlJVMRkwFwYDVQQDDBBUZXN0ZXIgR09TVCAyMDAxMCAXDTE4MDUyMjA5MzQ1NloY\nDzIwNjgwNTA5MDkzNDU2WjAoMQswCQYDVQQGEwJSVTEZMBcGA1UEAwwQVGVzdGVy\nIEdPU1QgMjAwMTBjMBwGBiqFAwICEzASBgcqhQMCAiMBBgcqhQMCAh4BA0MABEDL\nFFQ4czv3WOfm3+6m84epehMScB/6vJtrLgodrpdByvtk3LJyFoujKFNamQlzeJMy\n1Pfr/62l+8BAR4x0uMFIo1AwTjAdBgNVHQ4EFgQUe3YXXQwYzL0r+vo+FHrvc+O7\n5J8wHwYDVR0jBBgwFoAUe3YXXQwYzL0r+vo+FHrvc+O75J8wDAYDVR0TBAUwAwEB\n/zAKBgYqhQMCAgMFAANBANZ+qRGMoXesUdgW2nDiFxFoJpsWSW/Njr3QY98VY/F6\n/Q5xYGGd4pvnv71Mp2FHSR6YjWDR3/yV4q6z1TBQrDE=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/_data/server-gost.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMEMCAQAwHAYGKoUDAgITMBIGByqFAwICIwEGByqFAwICHgEEIOjpBFHdMSI7NioS\nEjRMxGuikBUuBmvzIm+2JP9Z5yrK\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/_data/server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICATCCAWoCCQDuV6K/drn++DANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJS\nVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE1MTAwNjA1MjQwM1oXDTI1MTAwMzA1MjQwM1owRTELMAkG\nA1UEBhMCUlUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwKEW\nMoa/dyEGJ1OI8M07lP0r6R6Js05RoarMKJpVRbkmiO9byqcbCEiPJLZfdcNsORGZ\nc9L09YBbUjJkpy4TY+qzNOpQMoScfeHA/5LzlXqzOdDapYUuXkxHQmOfAHBR3any\nUuVuCBJABp0jxxMVTXxKtxgpfqLH2Jmr31yu6vMCAwEAATANBgkqhkiG9w0BAQUF\nAAOBgQCJwsCDtw4IoPQiVw1fpEc8aS/QlujwChvyaTJUJ96cWHQs5hz7M23rPTib\nTZ4ooJ6jbP/e7vaxpXpXShNBnnP3RVCOKX0P0t1XS1nCvr651KXlIwNWTXZxSZ72\nvl5ySU71uOSaVPjicxIaPG93IFe5X7VTV4QDanSZqQqLwZsUGQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/_data/server.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBhDCB7gIBADBFMQswCQYDVQQGEwJSVTETMBEGA1UECBMKU29tZS1TdGF0ZTEh\nMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB\nAQUAA4GNADCBiQKBgQDAoRYyhr93IQYnU4jwzTuU/SvpHomzTlGhqswomlVFuSaI\n71vKpxsISI8ktl91w2w5EZlz0vT1gFtSMmSnLhNj6rM06lAyhJx94cD/kvOVerM5\n0NqlhS5eTEdCY58AcFHdqfJS5W4IEkAGnSPHExVNfEq3GCl+osfYmavfXK7q8wID\nAQABoAAwDQYJKoZIhvcNAQEFBQADgYEAn8N4/LFU6fUO4wdBPB8uWV7+rHQURFAU\n4qzsmcl+9l2EsHsloMRZzqKnJJHsJETxfb1zzzg4dve2bJy+50EklRMylnlG3nOi\n7QKdlHyCmq9YKcra3OiyeYsQ5FnpY/WEbwFoWLt9WYwHHNrbWzSpEj8Mb7DzP8S/\nkMPnUe1fH/Q=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "tests/_data/server.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC,332C0B168FE46450\n\nT7FYR235atS5fngeCLDhNrTYRz4BBMAPcTbLy5QrMMo39hjOclqol+D/QkYY7BXC\nxerxuE7cGvQEKwr3PAjNOZ8PREDR7saGZhrPNoup1lWjtkR9RiJrNcg7Ya7fCCX4\na+dsDAjUt+gxg1IfMd39nQR2gO3VxYjdNFRwwaFPQpYMotSkCutUCTC8BbtWEm5G\nk8bNuoJPvZfgqyZSbImlXvlht6JoUBYHoJHsin/drYxCZNDDBPe+d6yYaza8Qh0g\n5a1/btkSmy8Sq5Cd2gbGICxOKDRItASvO7nBauPbGJ/N7+wt/WwT8g9XGRugzNOj\nn+a9Kmqs6NwmFmaqeAZBTp3n+EmKii5tdXPmOkF9oMRLRWJsAUkzkjwPws1i3WfX\nGQR0n8MmcDcLdQ9jCPV8SMp2VR49EjKInh3NRlfBwpSJCaoEAzt9Je0rj4b0eYFO\npMoNjSpWwVsLPJ3GTW0Pz8nUIlhOppZI2ARHGRSfwjtY8/IBPX8Ja5gGjDu/QT8u\nEIAG+bFCk5JgYa+Sz0EX5Ok9fWymqDguF4Mr/w4Xsq0vvT3wZwq6lJzSduv+W6/l\ncwncRCXyg6HLbuW+5jKPcZEF3K70zXTktLyDjZlHzqoIfBgdU/cah+X/g4N5Swfx\nvjWAlUNa5YjBk6kuuTwZ8GvlBGjrYBmN4zSHAXCngGtYb+oV06hDECHVxRBu3J+2\nyQXYnX8W/PfAAPxWIX8fXGzaVjrGh4tVicI1kHrma0vaCOtEKVamg9wsvSTw+YWn\nTDThQ+rESGmt5PPmtlrLXhHmYBp8Xd2b95iwhMHvDYNVmRLnESH/Og==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "tests/_support/Helper/Unit.php",
    "content": "<?php\nnamespace Helper;\n// here you can define custom actions\n// all public methods declared in helper class will be available in $I\n\nuse Codeception\\Module;\nuse Codeception\\TestCase;\n\nclass Unit extends Module\n{\n}\n"
  },
  {
    "path": "tests/_support/UnitTester.php",
    "content": "<?php\n\n\n/**\n * Inherited Methods\n * @method void wantToTest($text)\n * @method void wantTo($text)\n * @method void execute($callable)\n * @method void expectTo($prediction)\n * @method void expect($prediction)\n * @method void amGoingTo($argumentation)\n * @method void am($role)\n * @method void lookForwardTo($achieveValue)\n * @method void comment($description)\n * @method \\Codeception\\Lib\\Friend haveFriend($name, $actorClass = null)\n *\n * @SuppressWarnings(PHPMD)\n*/\nclass UnitTester extends \\Codeception\\Actor\n{\n    use _generated\\UnitTesterActions;\n\n   /**\n    * Define custom actions here\n    */\n}\n"
  },
  {
    "path": "tests/_support/_generated/UnitTesterActions.php",
    "content": "<?php\n//[STAMP] c2c9446f63b7a0bf8b43e42b9b1b6f87\nnamespace _generated;\n\n// This class was automatically generated by build task\n// You should not change it manually as it will be overwritten on next build\n// @codingStandardsIgnoreFile\n\nuse Codeception\\Scenario;\nuse Codeception\\Step\\Action;\n\ntrait UnitTesterActions\n{\n    /**\n     * @return Scenario\n     */\n    abstract protected function getScenario();\n\n\n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that two variables are equal. If you're comparing floating-point values,\n     * you can specify the optional \"delta\" parameter which dictates how great of a precision\n     * error are you willing to tolerate in order to consider the two values equal.\n     *\n     * Regular example:\n     * ```php\n     * <?php\n     * $I->assertEquals(5, $element->getChildrenCount());\n     * ```\n     *\n     * Floating-point example:\n     * ```php\n     * <?php\n     * $I->assertEquals(0.3, $calculator->add(0.1, 0.2), 'Calculator should add the two numbers correctly.', 0.01);\n     * ```\n     *\n     * @param        $expected\n     * @param        $actual\n     * @param string $message\n     * @param float $delta\n     * @see \\Codeception\\Module\\Asserts::assertEquals()\n     */\n    public function assertEquals($expected, $actual, $message = null, $delta = null) {\n        return $this->getScenario()->runStep(new Action('assertEquals', func_get_args()));\n    }\n\n\n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that two variables are not equal. If you're comparing floating-point values,\n     * you can specify the optional \"delta\" parameter which dictates how great of a precision\n     * error are you willing to tolerate in order to consider the two values not equal.\n     *\n     * Regular example:\n     * ```php\n     * <?php\n     * $I->assertNotEquals(0, $element->getChildrenCount());\n     * ```\n     *\n     * Floating-point example:\n     * ```php\n     * <?php\n     * $I->assertNotEquals(0.4, $calculator->add(0.1, 0.2), 'Calculator should add the two numbers correctly.', 0.01);\n     * ```\n     *\n     * @param        $expected\n     * @param        $actual\n     * @param string $message\n     * @param float $delta\n     * @see \\Codeception\\Module\\Asserts::assertNotEquals()\n     */\n    public function assertNotEquals($expected, $actual, $message = null, $delta = null) {\n        return $this->getScenario()->runStep(new Action('assertNotEquals', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that two variables are same\n     *\n     * @param        $expected\n     * @param        $actual\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertSame()\n     */\n    public function assertSame($expected, $actual, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertSame', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that two variables are not same\n     *\n     * @param        $expected\n     * @param        $actual\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertNotSame()\n     */\n    public function assertNotSame($expected, $actual, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertNotSame', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that actual is greater than expected\n     *\n     * @param        $expected\n     * @param        $actual\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertGreaterThan()\n     */\n    public function assertGreaterThan($expected, $actual, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertGreaterThan', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that actual is greater or equal than expected\n     *\n     * @param        $expected\n     * @param        $actual\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertGreaterThanOrEqual()\n     */\n    public function assertGreaterThanOrEqual($expected, $actual, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertGreaterThanOrEqual', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that actual is less than expected\n     *\n     * @param        $expected\n     * @param        $actual\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertLessThan()\n     */\n    public function assertLessThan($expected, $actual, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertLessThan', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that actual is less or equal than expected\n     *\n     * @param        $expected\n     * @param        $actual\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertLessThanOrEqual()\n     */\n    public function assertLessThanOrEqual($expected, $actual, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertLessThanOrEqual', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that haystack contains needle\n     *\n     * @param        $needle\n     * @param        $haystack\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertContains()\n     */\n    public function assertContains($needle, $haystack, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertContains', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that haystack doesn't contain needle.\n     *\n     * @param        $needle\n     * @param        $haystack\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertNotContains()\n     */\n    public function assertNotContains($needle, $haystack, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertNotContains', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that string match with pattern\n     *\n     * @param string $pattern\n     * @param string $string\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertRegExp()\n     */\n    public function assertRegExp($pattern, $string, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertRegExp', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that string not match with pattern\n     *\n     * @param string $pattern\n     * @param string $string\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertNotRegExp()\n     */\n    public function assertNotRegExp($pattern, $string, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertNotRegExp', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that a string starts with the given prefix.\n     *\n     * @param string $prefix\n     * @param string $string\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertStringStartsWith()\n     */\n    public function assertStringStartsWith($prefix, $string, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertStringStartsWith', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that a string doesn't start with the given prefix.\n     *\n     * @param string $prefix\n     * @param string $string\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertStringStartsNotWith()\n     */\n    public function assertStringStartsNotWith($prefix, $string, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertStringStartsNotWith', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that variable is empty.\n     *\n     * @param        $actual\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertEmpty()\n     */\n    public function assertEmpty($actual, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertEmpty', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that variable is not empty.\n     *\n     * @param        $actual\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertNotEmpty()\n     */\n    public function assertNotEmpty($actual, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertNotEmpty', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that variable is NULL\n     *\n     * @param        $actual\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertNull()\n     */\n    public function assertNull($actual, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertNull', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that variable is not NULL\n     *\n     * @param        $actual\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertNotNull()\n     */\n    public function assertNotNull($actual, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertNotNull', func_get_args()));\n    }\n\n\n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that condition is positive.\n     *\n     * @param        $condition\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertTrue()\n     */\n    public function assertTrue($condition, $message = null)\n    {\n        return $this->getScenario()->runStep(new Action('assertTrue', func_get_args()));\n    }\n\n\n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that the condition is NOT true (everything but true)\n     *\n     * @param        $condition\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertNotTrue()\n     */\n    public function assertNotTrue($condition, $message = null)\n    {\n        return $this->getScenario()->runStep(new Action('assertNotTrue', func_get_args()));\n    }\n\n\n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that condition is negative.\n     *\n     * @param        $condition\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertFalse()\n     */\n    public function assertFalse($condition, $message = null)\n    {\n        return $this->getScenario()->runStep(new Action('assertFalse', func_get_args()));\n    }\n\n\n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that the condition is NOT false (everything but false)\n     *\n     * @param        $condition\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertNotFalse()\n     */\n    public function assertNotFalse($condition, $message = null)\n    {\n        return $this->getScenario()->runStep(new Action('assertNotFalse', func_get_args()));\n    }\n\n\n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks if file exists\n     *\n     * @param string $filename\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertFileExists()\n     */\n    public function assertFileExists($filename, $message = null)\n    {\n        return $this->getScenario()->runStep(new Action('assertFileExists', func_get_args()));\n    }\n\n\n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks if file doesn't exist\n     *\n     * @param string $filename\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertFileNotExists()\n     */\n    public function assertFileNotExists($filename, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertFileNotExists', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * @param $expected\n     * @param $actual\n     * @param $description\n     * @see \\Codeception\\Module\\Asserts::assertGreaterOrEquals()\n     */\n    public function assertGreaterOrEquals($expected, $actual, $description = null) {\n        return $this->getScenario()->runStep(new Action('assertGreaterOrEquals', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * @param $expected\n     * @param $actual\n     * @param $description\n     * @see \\Codeception\\Module\\Asserts::assertLessOrEquals()\n     */\n    public function assertLessOrEquals($expected, $actual, $description = null) {\n        return $this->getScenario()->runStep(new Action('assertLessOrEquals', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * @param $actual\n     * @param $description\n     * @see \\Codeception\\Module\\Asserts::assertIsEmpty()\n     */\n    public function assertIsEmpty($actual, $description = null) {\n        return $this->getScenario()->runStep(new Action('assertIsEmpty', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * @param $key\n     * @param $actual\n     * @param $description\n     * @see \\Codeception\\Module\\Asserts::assertArrayHasKey()\n     */\n    public function assertArrayHasKey($key, $actual, $description = null) {\n        return $this->getScenario()->runStep(new Action('assertArrayHasKey', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * @param $key\n     * @param $actual\n     * @param $description\n     * @see \\Codeception\\Module\\Asserts::assertArrayNotHasKey()\n     */\n    public function assertArrayNotHasKey($key, $actual, $description = null) {\n        return $this->getScenario()->runStep(new Action('assertArrayNotHasKey', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Checks that array contains subset.\n     *\n     * @param array  $subset\n     * @param array  $array\n     * @param bool   $strict\n     * @param string $message\n     * @see \\Codeception\\Module\\Asserts::assertArraySubset()\n     */\n    public function assertArraySubset($subset, $array, $strict = null, $message = null) {\n        return $this->getScenario()->runStep(new Action('assertArraySubset', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * @param $expectedCount\n     * @param $actual\n     * @param $description\n     * @see \\Codeception\\Module\\Asserts::assertCount()\n     */\n    public function assertCount($expectedCount, $actual, $description = null) {\n        return $this->getScenario()->runStep(new Action('assertCount', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * @param $class\n     * @param $actual\n     * @param $description\n     * @see \\Codeception\\Module\\Asserts::assertInstanceOf()\n     */\n    public function assertInstanceOf($class, $actual, $description = null) {\n        return $this->getScenario()->runStep(new Action('assertInstanceOf', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * @param $class\n     * @param $actual\n     * @param $description\n     * @see \\Codeception\\Module\\Asserts::assertNotInstanceOf()\n     */\n    public function assertNotInstanceOf($class, $actual, $description = null) {\n        return $this->getScenario()->runStep(new Action('assertNotInstanceOf', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * @param $type\n     * @param $actual\n     * @param $description\n     * @see \\Codeception\\Module\\Asserts::assertInternalType()\n     */\n    public function assertInternalType($type, $actual, $description = null) {\n        return $this->getScenario()->runStep(new Action('assertInternalType', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Fails the test with message.\n     *\n     * @param $message\n     * @see \\Codeception\\Module\\Asserts::fail()\n     */\n    public function fail($message) {\n        return $this->getScenario()->runStep(new Action('fail', func_get_args()));\n    }\n\n \n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Handles and checks exception called inside callback function.\n     * Either exception class name or exception instance should be provided.\n     *\n     * ```php\n     * <?php\n     * $I->expectException(MyException::class, function() {\n     *     $this->doSomethingBad();\n     * });\n     *\n     * $I->expectException(new MyException(), function() {\n     *     $this->doSomethingBad();\n     * });\n     * ```\n     * If you want to check message or exception code, you can pass them with exception instance:\n     * ```php\n     * <?php\n     * // will check that exception MyException is thrown with \"Don't do bad things\" message\n     * $I->expectException(new MyException(\"Don't do bad things\"), function() {\n     *     $this->doSomethingBad();\n     * });\n     * ```\n     *\n     * @param $exception string or \\Exception\n     * @param $callback\n     *\n     * @deprecated Use expectThrowable instead\n     * @see \\Codeception\\Module\\Asserts::expectException()\n     */\n    public function expectException($exception, $callback)\n    {\n        return $this->getScenario()->runStep(new Action('expectException', func_get_args()));\n    }\n\n\n    /**\n     * [!] Method is generated. Documentation taken from corresponding module.\n     *\n     * Handles and checks throwables (Exceptions/Errors) called inside the callback function.\n     * Either throwable class name or throwable instance should be provided.\n     *\n     * ```php\n     * <?php\n     * $I->expectThrowable(MyThrowable::class, function() {\n     *     $this->doSomethingBad();\n     * });\n     *\n     * $I->expectThrowable(new MyException(), function() {\n     *     $this->doSomethingBad();\n     * });\n     * ```\n     * If you want to check message or throwable code, you can pass them with throwable instance:\n     * ```php\n     * <?php\n     * // will check that throwable MyError is thrown with \"Don't do bad things\" message\n     * $I->expectThrowable(new MyError(\"Don't do bad things\"), function() {\n     *     $this->doSomethingBad();\n     * });\n     * ```\n     *\n     * @param $throwable string or \\Throwable\n     * @param $callback\n     * @see \\Codeception\\Module\\Asserts::expectThrowable()\n     */\n    public function expectThrowable($throwable, $callback)\n    {\n        return $this->getScenario()->runStep(new Action('expectThrowable', func_get_args()));\n    }\n}\n"
  },
  {
    "path": "tests/unit/ConfigTest.php",
    "content": "<?php\n\nnamespace tests\\unit;\n\nuse Codeception\\Test\\Unit;\nuse Esia\\Config;\nuse Esia\\Exceptions\\InvalidConfigurationException;\n\n/**\n * Class ConfigTest\n *\n * @coversDefaultClass \\Esia\\Config\n */\nclass ConfigTest extends Unit\n{\n    /**\n     * Getter for scope string\n     *\n     * @throws \\Esia\\Exceptions\\InvalidConfigurationException\n     */\n    public function testGetScopeString(): void\n    {\n        $config = new Config([\n            'clientId' => 'test',\n            'redirectUrl' => 'http://google.com',\n            'privateKeyPath' => '/tmp',\n            'certPath' => '/tmp',\n            'scope' => ['test', 'test2', 'test3'],\n        ]);\n\n        $this->assertSame('test test2 test3', $config->getScopeString());\n    }\n\n    /**\n     * Data provider for @see ConfigTest::testConstruct()\n     *\n     * @return array\n     */\n    public function dataProviderForConstructor(): array\n    {\n        return [\n            'min' => [\n                [\n                    'clientId' => 'test',\n                    'redirectUrl' => 'http://google.com',\n                    'privateKeyPath' => '/tmp',\n                    'certPath' => '/tmp',\n                    'scope' => ['test', 'test2', 'test3'],\n                ],\n                null,\n            ],\n            'max' => [\n                [\n                    'clientId' => 'test',\n                    'redirectUrl' => 'http://google.com',\n                    'privateKeyPath' => '/tmp',\n                    'certPath' => '/tmp',\n                    'portalUrl' => 'google.com',\n                    'tokenUrlPath' => 'test',\n                    'codeUrlPath' => 'test',\n                    'personUrlPath' => 'test',\n                    'logoutUrlPath' => 'test',\n                    'privateKeyPassword' => 'test',\n                    'oid' => 'test',\n                    'responseType' => 'test',\n                    'accessType' => 'test',\n                    'tmpPath' => 'test',\n                    'token' => 'test',\n                    'scope' => ['test', 'test2', 'test3'],\n                ],\n                null,\n            ],\n            'No cert path' => [\n                [\n                    'clientId' => 'test',\n                    'redirectUrl' => 'http://google.com',\n                    'privateKeyPath' => '/tmp',\n                    'scope' => ['test', 'test2', 'test3'],\n                ],\n                InvalidConfigurationException::class,\n            ],\n            'No private key path' => [\n                [\n                    'clientId' => 'test',\n                    'redirectUrl' => 'http://google.com',\n                    'certPath' => '/tmp',\n                    'scope' => ['test', 'test2', 'test3'],\n                ],\n                InvalidConfigurationException::class,\n            ],\n            'No redirect url' => [\n                [\n                    'clientId' => 'test',\n                    'privateKeyPath' => '/tmp',\n                    'certPath' => '/tmp',\n                    'scope' => ['test', 'test2', 'test3'],\n                ],\n                InvalidConfigurationException::class,\n            ],\n            'No client id' => [\n                [\n                    'redirectUrl' => 'http://google.com',\n                    'privateKeyPath' => '/tmp',\n                    'certPath' => '/tmp',\n                    'scope' => ['test', 'test2', 'test3'],\n                ],\n                InvalidConfigurationException::class,\n            ],\n            'invalid scope' => [\n                [\n                    'redirectUrl' => 'http://google.com',\n                    'privateKeyPath' => '/tmp',\n                    'certPath' => '/tmp',\n                    'scope' => 'test test2 test3',\n                ],\n                InvalidConfigurationException::class,\n            ],\n        ];\n    }\n\n    /**\n     * @param $config\n     * @param string|null $expectedException\n     * @throws \\Esia\\Exceptions\\InvalidConfigurationException\n     *\n     * @dataProvider dataProviderForConstructor\n     */\n    public function testConstruct($config, string $expectedException = null): void\n    {\n        if ($expectedException) {\n            $this->expectException($expectedException);\n        }\n\n        new Config($config);\n    }\n\n    /**\n     * @throws InvalidConfigurationException\n     */\n    public function testGetTokenUrl(): void\n    {\n        $config = new Config([\n            'clientId' => 'test',\n            'redirectUrl' => 'http://google.com',\n            'privateKeyPath' => '/tmp',\n            'certPath' => '/tmp',\n            'portalUrl' => 'https://google.com/',\n            'tokenUrlPath' => 'test',\n            'scope' => ['test', 'test2', 'test3'],\n        ]);\n\n        $this->assertSame('https://google.com/test', $config->getTokenUrl());\n    }\n\n    /**\n     * @throws InvalidConfigurationException\n     */\n    public function testGetCodeUrl(): void\n    {\n        $config = new Config([\n            'clientId' => 'test',\n            'redirectUrl' => 'http://google.com',\n            'privateKeyPath' => '/tmp',\n            'certPath' => '/tmp',\n            'portalUrl' => 'https://google.com/',\n            'codeUrlPath' => 'test',\n            'scope' => ['test', 'test2', 'test3'],\n        ]);\n\n        $this->assertSame('https://google.com/test', $config->getCodeUrl());\n    }\n\n    /**\n     * @throws InvalidConfigurationException\n     */\n    public function testGetPersonUrl(): void\n    {\n        $config = new Config([\n            'clientId' => 'test',\n            'redirectUrl' => 'http://google.com',\n            'privateKeyPath' => '/tmp',\n            'certPath' => '/tmp',\n            'portalUrl' => 'https://google.com/',\n            'personUrlPath' => 'test',\n            'oid' => 'test',\n            'scope' => ['test', 'test2', 'test3'],\n        ]);\n\n        $this->assertSame('https://google.com/test/test', $config->getPersonUrl());\n    }\n    /**\n     * @throws InvalidConfigurationException\n     */\n    public function testGetPersonUrlWithoutOid(): void\n    {\n        $config = new Config([\n            'clientId' => 'test',\n            'redirectUrl' => 'http://google.com',\n            'privateKeyPath' => '/tmp',\n            'certPath' => '/tmp',\n            'portalUrl' => 'https://google.com/',\n            'personUrlPath' => 'test',\n            'scope' => ['test', 'test2', 'test3'],\n        ]);\n\n        $this->expectException(InvalidConfigurationException::class);\n        $this->assertSame('https://google.com/test/test', $config->getPersonUrl());\n    }\n    /**\n     * @throws InvalidConfigurationException\n     */\n    public function testGetLogoutUrl(): void\n    {\n        $config = new Config([\n            'clientId' => 'test',\n            'redirectUrl' => 'http://google.com',\n            'privateKeyPath' => '/tmp',\n            'certPath' => '/tmp',\n            'portalUrl' => 'https://google.com/',\n            'logoutUrlPath' => 'test',\n            'scope' => ['test', 'test2', 'test3'],\n        ]);\n\n        $this->assertSame('https://google.com/test', $config->getLogoutUrl());\n    }\n}\n"
  },
  {
    "path": "tests/unit/Http/GuzzleHttpClientTest.php",
    "content": "<?php\n\nnamespace tests\\unit\\Http;\n\nuse Codeception\\Test\\Unit;\nuse Esia\\Http\\GuzzleHttpClient;\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\Exception\\RequestException;\nuse GuzzleHttp\\Handler\\MockHandler;\nuse GuzzleHttp\\HandlerStack;\nuse GuzzleHttp\\Psr7\\Request;\nuse GuzzleHttp\\Psr7\\Response;\nuse HttpException;\nuse Psr\\Http\\Client\\ClientExceptionInterface;\n\n/**\n * Class GuzzleHttpClientTest\n *\n * @coversDefaultClass \\Esia\\Http\\GuzzleHttpClient\n */\nclass GuzzleHttpClientTest extends Unit\n{\n    /**\n     * @throws ClientExceptionInterface\n     * @throws HttpException\n     */\n    public function testSendRequest(): void\n    {\n        $mock = new MockHandler([\n            new Response(),\n            new RequestException('Error Communicating with Server', new Request('GET', 'test'))\n        ]);\n\n        $handler = HandlerStack::create($mock);\n        $guzzleClient = new Client(['handler' => $handler]);\n\n        $client = new GuzzleHttpClient($guzzleClient);\n\n        $response = $client->sendRequest(new Request('GET', '/'));\n\n        self::assertSame(200, $response->getStatusCode());\n\n        $this->expectException(ClientExceptionInterface::class);\n        $client->sendRequest(new Request('GET', '/'));\n    }\n}\n"
  },
  {
    "path": "tests/unit/OpenIdCliOpensslTest.php",
    "content": "<?php\n\nnamespace tests\\unit;\n\nuse Esia\\Config;\nuse Esia\\Exceptions\\AbstractEsiaException;\nuse Esia\\Exceptions\\InvalidConfigurationException;\nuse Esia\\OpenId;\nuse Esia\\Signer\\CliSignerPKCS7;\nuse GuzzleHttp\\Psr7\\Response;\n\nclass OpenIdCliOpensslTest extends OpenIdTest\n{\n    /**\n     * @throws InvalidConfigurationException\n     */\n    public function setUp(): void\n    {\n        $this->config = [\n            'clientId' => 'INSP03211',\n            'redirectUrl' => 'http://my-site.com/response.php',\n            'portalUrl' => 'https://esia-portal1.test.gosuslugi.ru/',\n            'privateKeyPath' => codecept_data_dir('server-gost.key'),\n            'privateKeyPassword' => 'test',\n            'certPath' => codecept_data_dir('server-gost.crt'),\n            'tmpPath' => codecept_log_dir(),\n        ];\n\n        $config = new Config($this->config);\n\n        $this->openId = new OpenId($config);\n        $this->openId->setSigner(new CliSignerPKCS7(\n            $this->config['certPath'],\n            $this->config['privateKeyPath'],\n            $this->config['privateKeyPassword'],\n            $this->config['tmpPath']\n        ));\n    }\n\n    /**\n     * @throws AbstractEsiaException\n     * @throws InvalidConfigurationException\n     */\n    public function testGetToken(): void\n    {\n        $config = new Config($this->config);\n\n        $oid = '123';\n        $oidBase64 = base64_encode('{ \"urn:esia:sbj_id\" : ' . $oid . '}');\n\n        $client = $this->buildClientWithResponses([\n            new Response(200, [], '{ \"access_token\": \"test.' . $oidBase64 . '.test\"}'),\n        ]);\n        $openId = new OpenId($config, $client);\n        $openId->setSigner(new CliSignerPKCS7(\n            $this->config['certPath'],\n            $this->config['privateKeyPath'],\n            $this->config['privateKeyPassword'],\n            $this->config['tmpPath']\n        ));\n        $token = $openId->getToken('test');\n        self::assertNotEmpty($token);\n        self::assertSame($oid, $openId->getConfig()->getOid());\n    }\n\n}\n"
  },
  {
    "path": "tests/unit/OpenIdTest.php",
    "content": "<?php\n\nnamespace tests\\unit;\n\nuse Codeception\\Test\\Unit;\nuse Esia\\Config;\nuse Esia\\Exceptions\\AbstractEsiaException;\nuse Esia\\Exceptions\\InvalidConfigurationException;\nuse Esia\\Http\\GuzzleHttpClient;\nuse Esia\\OpenId;\nuse Esia\\Signer\\Exceptions\\SignFailException;\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\Handler\\MockHandler;\nuse GuzzleHttp\\HandlerStack;\nuse GuzzleHttp\\Psr7\\Response;\nuse Psr\\Http\\Client\\ClientInterface;\n\nclass OpenIdTest extends Unit\n{\n    public $config;\n\n    /**\n     * @var OpenId\n     */\n    public $openId;\n\n    /**\n     * @throws InvalidConfigurationException\n     */\n    public function setUp(): void\n    {\n        $this->config = [\n            'clientId' => 'INSP03211',\n            'redirectUrl' => 'http://my-site.com/response.php',\n            'portalUrl' => 'https://esia-portal1.test.gosuslugi.ru/',\n            'privateKeyPath' => codecept_data_dir('server.key'),\n            'privateKeyPassword' => 'test',\n            'certPath' => codecept_data_dir('server.crt'),\n            'tmpPath' => codecept_log_dir(),\n        ];\n\n        $config = new Config($this->config);\n\n        $this->openId = new OpenId($config);\n    }\n\n    /**\n     * @throws SignFailException\n     * @throws AbstractEsiaException\n     * @throws InvalidConfigurationException\n     */\n    public function testGetToken(): void\n    {\n        $config = new Config($this->config);\n\n        $oid = '123';\n        $oidBase64 = base64_encode('{ \"urn:esia:sbj_id\" : ' . $oid . '}');\n\n        $client = $this->buildClientWithResponses([\n            new Response(200, [], '{ \"access_token\": \"test.' . $oidBase64 . '.test\"}'),\n        ]);\n        $openId = new OpenId($config, $client);\n\n        $token = $openId->getToken('test');\n        self::assertNotEmpty($token);\n        self::assertSame($oid, $openId->getConfig()->getOid());\n    }\n\n    /**\n     * @throws InvalidConfigurationException\n     * @throws AbstractEsiaException\n     */\n    public function testGetPersonInfo(): void\n    {\n        $config = new Config($this->config);\n        $oid = '123';\n        $config->setOid($oid);\n        $config->setToken('test');\n\n        $client = $this->buildClientWithResponses([\n            new Response(200, [], '{\"username\": \"test\"}'),\n        ]);\n        $openId = new OpenId($config, $client);\n\n        $info = $openId->getPersonInfo();\n        self::assertNotEmpty($info);\n        self::assertSame(['username' => 'test'], $info);\n    }\n\n    /**\n     * @throws InvalidConfigurationException\n     * @throws AbstractEsiaException\n     */\n    public function testGetContactInfo(): void\n    {\n        $config = new Config($this->config);\n        $oid = '123';\n        $config->setOid($oid);\n        $config->setToken('test');\n\n        $client = $this->buildClientWithResponses([\n            new Response(200, [], '{\"size\": 2, \"elements\": [\"phone\", \"email\"]}'),\n            new Response(200, [], '{\"phone\": \"555 555 555\"}'),\n            new Response(200, [], '{\"email\": \"test@gmail.com\"}'),\n        ]);\n        $openId = new OpenId($config, $client);\n\n        $info = $openId->getContactInfo();\n        self::assertNotEmpty($info);\n        self::assertSame([['phone' => '555 555 555'], ['email' => 'test@gmail.com']], $info);\n    }\n\n    /**\n     * @throws InvalidConfigurationException\n     * @throws AbstractEsiaException\n     */\n    public function testGetAddressInfo(): void\n    {\n        $config = new Config($this->config);\n        $oid = '123';\n        $config->setOid($oid);\n        $config->setToken('test');\n\n        $client = $this->buildClientWithResponses([\n            new Response(200, [], '{\"size\": 2, \"elements\": [\"phone\", \"email\"]}'),\n            new Response(200, [], '{\"phone\": \"555 555 555\"}'),\n            new Response(200, [], '{\"email\": \"test@gmail.com\"}'),\n        ]);\n        $openId = new OpenId($config, $client);\n\n        $info = $openId->getAddressInfo();\n        self::assertNotEmpty($info);\n        self::assertSame([['phone' => '555 555 555'], ['email' => 'test@gmail.com']], $info);\n    }\n\n    /**\n     * @throws InvalidConfigurationException\n     * @throws AbstractEsiaException\n     */\n    public function testGetDocInfo(): void\n    {\n        $config = new Config($this->config);\n        $oid = '123';\n        $config->setOid($oid);\n        $config->setToken('test');\n\n        $client = $this->buildClientWithResponses([\n            new Response(200, [], '{\"size\": 2, \"elements\": [\"phone\", \"email\"]}'),\n            new Response(200, [], '{\"phone\": \"555 555 555\"}'),\n            new Response(200, [], '{\"email\": \"test@gmail.com\"}'),\n        ]);\n        $openId = new OpenId($config, $client);\n\n        $info = $openId->getDocInfo();\n        self::assertNotEmpty($info);\n        self::assertSame([['phone' => '555 555 555'], ['email' => 'test@gmail.com']], $info);\n    }\n\n    /**\n     * @throws InvalidConfigurationException\n     */\n    public function testBuildLogoutUrl(): void\n    {\n        $config = $this->openId->getConfig();\n\n        $url = $config->getLogoutUrl() . '?client_id=' . $config->getClientId();\n        $logoutUrl = $this->openId->buildLogoutUrl();\n        self::assertSame($url, $logoutUrl);\n    }\n\n    /**\n     * @throws InvalidConfigurationException\n     */\n    public function testBuildLogoutUrlWithRedirect(): void\n    {\n        $config = $this->openId->getConfig();\n\n        $redirectUrl = 'test.example.com';\n        $url = $config->getLogoutUrl() . '?client_id=' . $config->getClientId() . '&redirect_url=' . $redirectUrl;\n        $logoutUrl = $this->openId->buildLogoutUrl($redirectUrl);\n        self::assertSame($url, $logoutUrl);\n    }\n\n    /**\n     * Client with prepared responses\n     *\n     * @param array $responses\n     * @return ClientInterface\n     */\n    protected function buildClientWithResponses(array $responses): ClientInterface\n    {\n        $mock = new MockHandler($responses);\n\n        $handler = HandlerStack::create($mock);\n        $guzzleClient = new Client(['handler' => $handler]);\n\n        return new GuzzleHttpClient($guzzleClient);\n    }\n}\n"
  },
  {
    "path": "tests/unit/Signer/SignerPKCS7Test.php",
    "content": "<?php\n\nnamespace tests\\unit\\Signer;\n\nuse Codeception\\Test\\Unit;\nuse Esia\\Signer\\Exceptions\\CannotReadCertificateException;\nuse Esia\\Signer\\Exceptions\\CannotReadPrivateKeyException;\nuse Esia\\Signer\\Exceptions\\NoSuchCertificateFileException;\nuse Esia\\Signer\\Exceptions\\NoSuchKeyFileException;\nuse Esia\\Signer\\Exceptions\\NoSuchTmpDirException;\nuse Esia\\Signer\\Exceptions\\SignFailException;\nuse Esia\\Signer\\SignerPKCS7;\n\n/**\n * Class SignerPKCS7Test\n *\n * @coversDefaultClass \\Esia\\Signer\\SignerPKCS7\n */\nclass SignerPKCS7Test extends Unit\n{\n    /**\n     * @throws SignFailException\n     */\n    public function testSign(): void\n    {\n        $signer = new SignerPKCS7(\n            codecept_data_dir('server.crt'),\n            codecept_data_dir('server.key'),\n            'test',\n            codecept_log_dir()\n        );\n\n        $sign = $signer->sign('test');\n        self::assertNotEmpty($sign);\n    }\n\n    /**\n     * @throws SignFailException\n     */\n    public function testSignCertDoesNotExists(): void\n    {\n        $signer = new SignerPKCS7(\n            '/test',\n            codecept_data_dir('server.key'),\n            'test',\n            codecept_log_dir()\n        );\n\n        $this->expectException(NoSuchCertificateFileException::class);\n        $signer->sign('test');\n    }\n\n    /**\n     * @throws SignFailException\n     */\n    public function testPrivateKeyDoesNotExists(): void\n    {\n        $signer = new SignerPKCS7(\n            codecept_data_dir('server.crt'),\n            '/test',\n            'test',\n            codecept_log_dir()\n        );\n\n        $this->expectException(NoSuchKeyFileException::class);\n        $signer->sign('test');\n    }\n\n    /**\n     * @throws SignFailException\n     */\n    public function testTmpDirDoesNotExists(): void\n    {\n        $signer = new SignerPKCS7(\n            codecept_data_dir('server.crt'),\n            codecept_data_dir('server.key'),\n            'test',\n            '/'\n        );\n\n        $this->expectException(NoSuchTmpDirException::class);\n        $signer->sign('test');\n    }\n\n    /**\n     * @throws SignFailException\n     */\n    public function testTmpDirIsNotWritable(): void\n    {\n        $signer = new SignerPKCS7(\n            codecept_data_dir('server.crt'),\n            codecept_data_dir('server.key'),\n            'test',\n            codecept_log_dir('non_writable_directory')\n        );\n\n        $this->expectException(NoSuchTmpDirException::class);\n        $signer->sign('test');\n    }\n\n    /**\n     * @throws SignFailException\n     */\n    public function testCertificateIsNotReadable(): void\n    {\n        $signer = new SignerPKCS7(\n            codecept_data_dir('non_readable_file'),\n            codecept_data_dir('server.key'),\n            'test',\n            codecept_log_dir()\n        );\n\n        $this->expectException(CannotReadCertificateException::class);\n        $signer->sign('test');\n    }\n\n    /**\n     * @throws SignFailException\n     */\n    public function testPrivateKeyIsNotReadable(): void\n    {\n        $signer = new SignerPKCS7(\n            codecept_data_dir('server.crt'),\n            codecept_data_dir('non_readable_file'),\n            'test',\n            codecept_log_dir()\n        );\n\n        $this->expectException(CannotReadPrivateKeyException::class);\n        $signer->sign('test');\n    }\n}\n"
  },
  {
    "path": "tests/unit/_bootstrap.php",
    "content": "<?php\n// Here you can initialize variables that will be available to your tests\n"
  },
  {
    "path": "tests/unit.suite.yml",
    "content": "# Codeception Test Suite Configuration\n#\n# Suite for unit (internal) tests.\n\nclass_name: UnitTester\nmodules:\n    enabled:\n        - Asserts\n        - \\Helper\\Unit"
  }
]