diff --git a/.travis/composer.json b/.travis/composer.json index f6d54e81a..4c5962f0c 100644 --- a/.travis/composer.json +++ b/.travis/composer.json @@ -8,7 +8,9 @@ "composer/installers": "^1.6", "drupal-composer/drupal-scaffold": "^2.5.4", "wikimedia/composer-merge-plugin": "dev-capture-input-options", - "zaporylie/composer-drupal-optimizations": "^1.0" + "zaporylie/composer-drupal-optimizations": "^1.0", + "drupal/console": "~1.0", + "drush/drush": "^9.7" }, "repositories": [ { diff --git a/README.md b/README.md index 1bfcd469a..ec05f2e5a 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Please note that Team APIs and Monetization APIs are not currently supported on 2. Click **Extend** in the Drupal administration menu. 3. Select the **Apigee Edge** module. 4. Click **Install**. +5. Configure the [connection to your Apigee org](https://www.drupal.org/docs/8/modules/apigee-edge/configure-the-connection-to-apigee-edge) ## Notes diff --git a/apigee_edge.services.yml b/apigee_edge.services.yml index f0b31bd00..d6ef8f983 100644 --- a/apigee_edge.services.yml +++ b/apigee_edge.services.yml @@ -12,6 +12,11 @@ services: apigee_edge.cli: class: Drupal\apigee_edge\CliService + arguments: ['@apigee_edge.apigee_edge_mgmt_cli_service'] + + apigee_edge.apigee_edge_mgmt_cli_service: + class: Drupal\apigee_edge\Command\Util\ApigeeEdgeManagementCliService + arguments: ['@http_client'] apigee_edge.sdk_connector: class: Drupal\apigee_edge\SDKConnector diff --git a/composer.json b/composer.json index 051886385..0ed6d0d31 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "description": "Apigee Edge for Drupal.", "require": { "php": ">=7.1", + "ext-json": "*", "apigee/apigee-client-php": "^2.0.4", "cweagans/composer-patches": "^1.6.5", "drupal/core": "~8.7", diff --git a/console.services.yml b/console.services.yml index d1f563876..85060d6df 100644 --- a/console.services.yml +++ b/console.services.yml @@ -4,3 +4,8 @@ services: arguments: ['@apigee_edge.cli', '@logger.log_message_parser', '@logger.factory'] tags: - { name: drupal.command } + apigee_edge.edge_role_create: + class: Drupal\apigee_edge\Command\CreateEdgeRoleCommand + arguments: ['@apigee_edge.cli', '@logger.log_message_parser', '@logger.factory'] + tags: + - { name: drupal.command } diff --git a/console/translations/en/apigee_edge.role.create.yml b/console/translations/en/apigee_edge.role.create.yml new file mode 100644 index 000000000..19db3c8a7 --- /dev/null +++ b/console/translations/en/apigee_edge.role.create.yml @@ -0,0 +1,13 @@ +description: 'Create Apigee role for Drupal' +help: 'Create a custom Apigee role that limits permissions for Drupal connections to the Apigee API. ' +arguments: + org: 'The Apigee Edge org to create the role in' + email: 'An Apigee user email address with orgadmin role for this org' +options: + password: 'Password for the Apigee orgadmin user (if not passed in you will be prompted)' + base-url: 'Base URL to use, defaults to public cloud URL https://api.enterprise.apigee.com/v1' + role-name: 'The role to create in the Apigee Edge org, defaults to "drupalportal"' + force: 'Force running of permissions on a role that already exists. Note that permissions are only added, any current permissions not not removed.' +questions: + password: 'Enter password for Apigee orgadmin user' +messages: diff --git a/drush.services.yml b/drush.services.yml index ecdecdb61..1aa1a49f1 100644 --- a/drush.services.yml +++ b/drush.services.yml @@ -1,6 +1,6 @@ services: apigee_edge.commands: class: \Drupal\apigee_edge\Commands\ApigeeEdgeCommands - arguments: ['@apigee_edge.cli'] + arguments: ['@apigee_edge.cli', '@apigee_edge.apigee_edge_mgmt_cli_service'] tags: - { name: drush.command } diff --git a/src/CliService.php b/src/CliService.php index f3f4bedda..8f5f2ada4 100644 --- a/src/CliService.php +++ b/src/CliService.php @@ -1,7 +1,7 @@ apigeeEdgeManagementCliService = $apigeeEdgeManagementCliService; + } + /** * {@inheritdoc} */ @@ -52,4 +70,29 @@ public function sync(StyleInterface $io, callable $t) { } } + /** + * {@inheritdoc} + */ + public function createEdgeRoleForDrupal( + StyleInterface $io, + callable $t, + string $org, + string $email, + string $password, + ?string $base_url, + ?string $role_name, + ?bool $force + ) { + $this->apigeeEdgeManagementCliService->createEdgeRoleForDrupal( + $io, + $t, + $org, + $email, + $password, + $base_url, + $role_name, + $force + ); + } + } diff --git a/src/CliServiceInterface.php b/src/CliServiceInterface.php index 762f030cd..f5bee24a0 100644 --- a/src/CliServiceInterface.php +++ b/src/CliServiceInterface.php @@ -36,4 +36,35 @@ interface CliServiceInterface { */ public function sync(StyleInterface $io, callable $t); + /** + * Create an Apigee role for Drupal use. + * + * @param \Symfony\Component\Console\Style\StyleInterface $io + * The IO interface of the CLI tool calling the method. + * @param callable $t + * The translation function akin to t(). + * @param string $org + * The organization to connect to. + * @param string $email + * The email of an Edge user with org admin role to make Edge API calls. + * @param string $password + * The password of an Edge user with org admin role to make Edge API calls. + * @param string|null $base_url + * The base url of the Edge API. + * @param string|null $role_name + * The role name to add the permissions to. + * @param bool|null $force + * Force permissions to be set even if role exists. + */ + public function createEdgeRoleForDrupal( + StyleInterface $io, + callable $t, + string $org, + string $email, + string $password, + ?string $base_url, + ?string $role_name, + ?bool $force + ); + } diff --git a/src/Command/CommandBase.php b/src/Command/CommandBase.php index f3b153154..061df9d65 100644 --- a/src/Command/CommandBase.php +++ b/src/Command/CommandBase.php @@ -24,7 +24,7 @@ use Drupal\Console\Core\Style\DrupalStyle; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\Logger\LogMessageParserInterface; -use Symfony\Component\Console\Command\Command; +use Drupal\Console\Core\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; @@ -115,7 +115,7 @@ protected function setupLogger(OutputInterface $output) { * @return \Symfony\Component\Console\Style\StyleInterface * The IO interface. */ - protected function getIo(): StyleInterface { + public function getIo(): StyleInterface { return $this->io; } diff --git a/src/Command/CreateEdgeRoleCommand.php b/src/Command/CreateEdgeRoleCommand.php new file mode 100644 index 000000000..8217682d3 --- /dev/null +++ b/src/Command/CreateEdgeRoleCommand.php @@ -0,0 +1,110 @@ +setName('apigee_edge:role:create') + ->setDescription($this->trans('commands.apigee_edge.role.create.description')) + ->setHelp('commands.apigee_edge.role.create.help') + ->addArgument( + 'org', + InputArgument::REQUIRED, + $this->trans('commands.apigee_edge.role.create.arguments.org') + ) + ->addArgument( + 'email', + InputArgument::REQUIRED, + $this->trans('commands.apigee_edge.role.create.arguments.email') + ) + ->addOption( + 'password', + 'p', + InputArgument::OPTIONAL, + $this->trans('commands.apigee_edge.role.create.options.password') + ) + ->addOption( + 'base-url', + 'b', + InputArgument::OPTIONAL, + $this->trans('commands.apigee_edge.role.create.options.base-url') + ) + ->addOption( + 'role-name', + 'r', + InputArgument::OPTIONAL, + $this->trans('commands.apigee_edge.role.create.options.role-name') + )->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + $this->trans('commands.apigee_edge.role.create.options.force') + ); + + } + + /** + * {@inheritdoc} + */ + public function interact(InputInterface $input, OutputInterface $output) { + $this->setupIo($input, $output); + $password = $input->getOption('password'); + if (!$password) { + $password = $this->getIo()->askHidden( + $this->trans('commands.apigee_edge.role.create.questions.password') + ); + $input->setOption('password', $password); + } + } + + /** + * {@inheritdoc} + */ + public function execute(InputInterface $input, OutputInterface $output) { + $this->setupIo($input, $output); + $org = $input->getArgument('org'); + $email = $input->getArgument('email'); + $password = $input->getOption('password'); + $base_url = $input->getOption('base-url'); + $role_name = $input->getOption('role-name'); + $force = $input->getOption('force'); + + $this->cliService->createEdgeRoleForDrupal($this->getIo(), 't', $org, $email, $password, $base_url, $role_name, $force); + } + +} diff --git a/src/Command/Util/ApigeeEdgeManagementCliService.php b/src/Command/Util/ApigeeEdgeManagementCliService.php new file mode 100644 index 000000000..65d8361f5 --- /dev/null +++ b/src/Command/Util/ApigeeEdgeManagementCliService.php @@ -0,0 +1,340 @@ +httpClient = $http_client; + } + + /** + * {@inheritdoc} + */ + public function createEdgeRoleForDrupal(StyleInterface $io, + callable $t, + string $org, + string $email, + string $password, + ?string $base_url, + ?string $role_name, + bool $force) { + + // Set default base URL if var is null or empty string. + if (empty($base_url)) { + $base_url = ApigeeClientInterface::DEFAULT_ENDPOINT; + } + else { + // Validate it is a valid URL. + if (!UrlHelper::isValid($base_url, TRUE)) { + $io->error($t('Base URL is not valid.')); + return; + } + } + + // Set default if null or empty string. + $role_name = $role_name ?: self::DEFAULT_ROLE_NAME; + + if (!$this->isValidEdgeCredentials($io, $t, $org, $email, $password, $base_url)) { + return; + } + + $does_role_exist = $this->doesRoleExist($org, $email, $password, $base_url, $role_name); + + // If role does not exist and force flag is not used, throw error. + if ($does_role_exist && !$force) { + $io->error('Role ' . $role_name . ' already exists.'); + $io->note('Run with --force option to set default permissions on this role.'); + return; + } + + // Create the role if it does not exist. + if (!$does_role_exist) { + $io->text($t('Role :role does not exist. Creating role.', [':role' => $role_name])); + + $url = "{$base_url}/o/{$org}/userroles"; + try { + $this->httpClient->post($url, [ + 'body' => json_encode([ + 'role' => [$role_name], + ]), + 'auth' => [$email, $password], + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + ]); + } + catch (TransferException $exception) { + $this->handleHttpClientExceptions($exception, $io, $t, $url, $org, $email); + return; + } + } + + $this->setDefaultPermissions($io, $t, $org, $email, $password, $base_url, $role_name); + + $io->success($t('Role :role is configured. Log into apigee.com to assign a user to this role.', [':role' => $role_name])); + } + + /** + * Set default permissions for a role used for Drupal portal connections. + * + * @param \Symfony\Component\Console\Style\StyleInterface $io + * The IO interface of the CLI tool calling the method. + * @param callable $t + * The translation function akin to t(). + * @param string $org + * The Edge org to create the permissions in. + * @param string $email + * The email of an Edge user with org admin role to make Edge API calls. + * @param string $password + * The password of an Edge user email to make Edge API calls. + * @param string $base_url + * The base url of the Edge API. + * @param string $role_name + * The role name to add the permissions to. + */ + protected function setDefaultPermissions(StyleInterface $io, callable $t, string $org, string $email, string $password, string $base_url, string $role_name) { + $io->text('Setting permissions on role ' . $role_name . '.'); + + $permissions = [ + // GET access by default for all resources. + '/' => ['get'], + // Read only access to environments for analytics. + '/environments/' => ['get'], + '/environments/*/stats/*' => ['get'], + // We do not need to update/edit roles, just read them. + '/userroles' => ['get'], + // No need to create API products, only read and edit. + '/apiproducts' => ['get', 'put'], + // Full CRUD for developers. + '/developers' => ['get', 'put', 'delete'], + // Full CRUD for developer's apps. + '/developers/*/apps' => ['get', 'put', 'delete'], + '/developers/*/apps/*' => ['get', 'put', 'delete'], + // Full CRUD for companies. + '/companies' => ['get', 'put'], + '/companies/*' => ['get', 'put', 'delete'], + // Full CRUD for company apps. + '/companies/*/apps' => ['get', 'put'], + '/companies/*/apps/*' => ['get', 'put', 'delete'], + ]; + + // Resource URL for modifying permissions. + $url = $base_url . '/o/' . $org . '/userroles/' . $role_name . '/permissions'; + try { + foreach ($permissions as $path => $permission_verbs) { + $body = json_encode([ + 'path' => $path, + 'permissions' => $permission_verbs, + ]); + $io->text($path . ' -> ' . implode(',', $permission_verbs)); + $this->httpClient->post($url, [ + 'body' => $body, + 'auth' => [$email, $password], + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + ]); + } + } + catch (TransferException $exception) { + $this->handleHttpClientExceptions($exception, $io, $t, $url, $org, $email); + return; + } + } + + /** + * Check to see if role exists. + * + * @param string $org + * The Edge org to create the permissions in. + * @param string $email + * The email of an Edge user with org admin role to make Edge API calls. + * @param string $password + * The password of an Edge user with org admin role to make Edge API calls. + * @param string $base_url + * The base url of the Edge API. + * @param string $role_name + * The role name to add the permissions to. + * + * @return bool + * Returns true if the role exists, or false if it doesn't. + * + * @throws \GuzzleHttp\Exception\TransferException + */ + public function doesRoleExist(string $org, string $email, string $password, string $base_url, string $role_name) { + $url = $base_url . '/o/' . $org . '/userroles/' . $role_name; + try { + $response = $this->httpClient->get($url, [ + 'auth' => [$email, $password], + 'headers' => ['Accept' => 'application/json'], + ]); + } + catch (ClientException $exception) { + if ($exception->getCode() == 404) { + // Role does not exist. + return FALSE; + } + // Any other response was an exception. + throw $exception; + } + + // Make sure role exists. + $body = json_decode((string) $response->getBody()); + if (isset($body->name) && $body->name == $role_name) { + return TRUE; + } + else { + return FALSE; + } + } + + /** + * Validate the Apigee Edge org connection settings. + * + * @param \Symfony\Component\Console\Style\StyleInterface $io + * The IO interface of the CLI tool calling the method. + * @param callable $t + * The translation function akin to t(). + * @param string $org + * The Edge org to connect to. + * @param string $email + * The email of an Edge user with org admin role to make Edge API calls. + * @param string $password + * The password of an Edge user email to make Edge API calls. + * @param string $base_url + * The base url of the Edge API. + * + * @return bool + * Return true if the Edge API can be called, false if it cannot. + */ + public function isValidEdgeCredentials(StyleInterface $io, callable $t, string $org, string $email, string $password, string $base_url) { + $url = $base_url . '/o/' . $org; + try { + $response = $this->httpClient->get($url, [ + 'auth' => [$email, $password], + 'headers' => ['Accept' => 'application/json'], + ]); + } + catch (TransferException $exception) { + $this->handleHttpClientExceptions($exception, $io, $t, $url, $org, $email); + return FALSE; + } + + // Make sure a response is returned. + $raw_body = (string) $response->getBody(); + if (empty($raw_body)) { + $io->error($t('Response to :url returned empty. HTTP !response_code !response_reason', [ + ':url' => $url, + '!response_code' => $response->getStatusCode(), + '!response_reason' => json_last_error_msg(), + ])); + return FALSE; + } + $body = json_decode($raw_body); + + if (JSON_ERROR_NONE !== json_last_error()) { + $io->error($t('Unable to parse response from GET :url into JSON: !error ', [ + ':url' => $url, + '!error' => json_last_error_msg(), + ])); + return FALSE; + } + if (!isset($body->name)) { + $io->error($t('The response from GET :url did not contain valid org data.', [':url' => $url])); + return FALSE; + } + else { + $io->success($t('Connected to Edge org :org.', [':org' => $body->name])); + } + return TRUE; + } + + /** + * Print out helpful information to user running command when error happens. + * + * @param \GuzzleHttp\Exception\TransferException $exception + * The exception thrown. + * @param \Symfony\Component\Console\Style\StyleInterface $io + * The IO interface of the CLI tool calling the method. + * @param callable $t + * The translation function akin to t(). + * @param string $url + * The url being connected to. + * @param string $org + * The organization to connect to. + * @param string $email + * The email of an Edge user with org admin role to make Edge API calls. + */ + public function handleHttpClientExceptions(TransferException $exception, StyleInterface $io, callable $t, string $url, string $org, string $email): void { + // Display error message. + $io->error($t('Error connecting to Apigee Edge. :exception_message', [':exception_message' => $exception->getMessage()])); + + // Add a note to common situations on what could be wrong. + switch ($exception->getCode()) { + case 0: + $io->note($t('Your system may not be able to connect to :url.', [ + ':url' => $url, + ])); + return; + + case 401: + $io->note($t('Your username or password is invalid.')); + return; + + case 403: + $io->note($t('User :email may not have the orgadmin role for Apigee Edge org :org.', [ + ':email' => $email, + ':org' => $org, + ])); + return; + + case 302: + $io->note($t('Edge endpoint gives a redirect response, is the url :url does not seem to be a valid Apigee Edge endpoint.', [':url' => $url])); + return; + } + } + +} diff --git a/src/Command/Util/ApigeeEdgeManagementCliServiceInterface.php b/src/Command/Util/ApigeeEdgeManagementCliServiceInterface.php new file mode 100644 index 000000000..4194b1b74 --- /dev/null +++ b/src/Command/Util/ApigeeEdgeManagementCliServiceInterface.php @@ -0,0 +1,63 @@ +cliService = $cli_service; } @@ -57,4 +59,80 @@ public function sync() { $this->cliService->sync($this->io(), 'dt'); } + /** + * Create a custom role in an Apigee organization for Drupal usage. + * + * Create a custom Apigee role that limits permissions for Drupal connections + * to the Apigee API. + * + * @param string $org + * The Apigee Edge org to create the role in. + * @param string $email + * An Apigee user email address with orgadmin role for this org. + * @param array $options + * Drush options for the command. + * + * @option password + * Password for the Apigee orgadmin user. If not set, you will be prompted + * for the password. + * @option base-url + * Base URL to use, defaults to public cloud URL: + * https://api.enterprise.apigee.com/v1. + * @option role-name + * The role to create in the Apigee Edge org, defaults to "drupalportal". + * @option $force + * Force running of permissions on a role that already exists, defaults + * to throwing an error message if role exists. Note that permissions are + * only added, any current permissions not not removed. + * @usage drush create-edge-role myorg me@example.com + * Create "drupalportal" role as orgadmin me@example.com for org myorg. + * @usage drush create-edge-role myorg me@example.com --role-name=portal + * Create role named "portal" + * @usage drush create-edge-role myorg me@example.com --base-url=https://api.edge.example.com + * Create role on private Apigee Edge server "api.edge.example.com". + * @usage drush create-edge-role myorg me@example.com --force + * Update permissions on "drupalportal" role even if role already exists. + * @command apigee-edge:create-edge-role + * @aliases create-edge-role + */ + public function createEdgeRole( + string $org, + string $email, + array $options = [ + 'password' => NULL, + 'base-url' => NULL, + 'role-name' => NULL, + 'force' => FALSE, + ]) { + + // Call the CLI Service. + $this->cliService->createEdgeRoleForDrupal( + $this->io(), + 'dt', + $org, + $email, + $options['password'], + $options['base-url'], + $options['role-name'], + $options['force'] + ); + } + + /** + * Validate function for the createEdge method. + * + * @hook validate apigee-edge:create-edge-role + */ + public function validateCreateEdgeRole(CommandData $commandData) { + // If the user did not specify a password, then prompt for one. + $password = $commandData->input()->getOption('password'); + $email = $commandData->input()->getArgument('email'); + if (empty($password)) { + $password = $this->io()->askHidden(dt('Enter password for :email', [':email' => $email]), function ($value) { + return $value; + }); + $commandData->input()->setOption('password', $password); + } + } + } diff --git a/src/KeyEntityFormEnhancer.php b/src/KeyEntityFormEnhancer.php index 2ba0ee956..2fe71ab91 100644 --- a/src/KeyEntityFormEnhancer.php +++ b/src/KeyEntityFormEnhancer.php @@ -285,7 +285,7 @@ public function validateForm(array &$form, FormStateInterface $form_state): void return; } - // If there is a form error already do not nothing. + // If there is a form error already do not continue. if (!empty($form_state->getErrors())) { return; } diff --git a/tests/src/Functional/ApiProductAccessTest.php b/tests/src/Functional/ApiProductAccessTest.php index c34222792..8f0f1d392 100644 --- a/tests/src/Functional/ApiProductAccessTest.php +++ b/tests/src/Functional/ApiProductAccessTest.php @@ -306,8 +306,8 @@ protected function developerAppEditFormTest() { // >> Bypass user. $this->drupalLogin($this->users[self::USER_WITH_BYPASS_PERM]); - // Even if a user has bypass permission it should see only those API - // Products on an other user's add/edit form that the other user has + // Even if a user has bypass permission they should see only those API + // Products on another user's add/edit form that the other user has // access. $this->drupalGet(Url::fromRoute('entity.developer_app.add_form_for_developer', [ 'user' => $this->users[AccountInterface::AUTHENTICATED_ROLE]->id(), @@ -319,7 +319,7 @@ protected function developerAppEditFormTest() { ])); $onlyPublicProductVisible(); - // But on the its own add/edit app forms it should see all API products. + // But on the its own add/edit app forms they should see all API products. $this->drupalGet(Url::fromRoute('entity.developer_app.add_form_for_developer', [ 'user' => $this->users[self::USER_WITH_BYPASS_PERM]->id(), ])); @@ -456,7 +456,7 @@ protected function messageIfUserShouldHaveAccessByRole(string $operation, UserIn } /** - * Error message, when a user should have access because it has bypass perm. + * Error message, when a user should have access because they have bypass perm. * * @param string $operation * Operation on API product. diff --git a/tests/src/Functional/DeveloperTest.php b/tests/src/Functional/DeveloperTest.php index 158077f17..b1c1138d3 100644 --- a/tests/src/Functional/DeveloperTest.php +++ b/tests/src/Functional/DeveloperTest.php @@ -23,7 +23,6 @@ use Apigee\Edge\Api\Management\Controller\CompanyMembersController; use Apigee\Edge\Api\Management\Entity\Company; use Apigee\Edge\Api\Management\Structure\CompanyMembership; -use Drupal\apigee_edge\Entity\Developer; use Drupal\apigee_edge\Entity\DeveloperInterface; use Drupal\Core\Url; diff --git a/tests/src/Kernel/Util/ApigeeEdgeManagementCliServiceTest.php b/tests/src/Kernel/Util/ApigeeEdgeManagementCliServiceTest.php new file mode 100644 index 000000000..0dbfcf664 --- /dev/null +++ b/tests/src/Kernel/Util/ApigeeEdgeManagementCliServiceTest.php @@ -0,0 +1,217 @@ +markTestSkipped('Environment variable ' . $environment_var . ' is not set, cannot run tests. See CONTRIBUTING.md for more information.'); + } + } + + // Get environment variables for Edge connection. + $this->endpoint = getenv('APIGEE_EDGE_ENDPOINT'); + $this->organization = getenv('APIGEE_EDGE_ORGANIZATION'); + $this->orgadminEmail = getenv('APIGEE_EDGE_USERNAME'); + $this->orgadminPassword = getenv('APIGEE_EDGE_PASSWORD'); + + /** @var \GuzzleHttp\Client $client */ + $this->httpClient = $this->container->get('http_client'); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + $url = $this->endpoint . '/o/' . $this->organization . '/userroles/' . self::TEST_ROLE_NAME; + $response = $this->httpClient->get($url, [ + 'http_errors' => FALSE, + 'auth' => [$this->orgadminEmail, $this->orgadminPassword], + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + ]); + + if ($response->getStatusCode() == 200) { + $url = $this->endpoint . '/o/' . $this->organization . '/userroles/' . self::TEST_ROLE_NAME; + $this->httpClient->delete($url, [ + 'auth' => [$this->orgadminEmail, $this->orgadminPassword], + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + ]); + } + parent::tearDown(); + + } + + /** + * Fix for outbound HTTP requests fail with KernelTestBase. + * + * See comment #10: + * https://www.drupal.org/project/drupal/issues/2571475#comment-11938008 + */ + public function alter(ContainerBuilder $container) { + $container->removeDefinition('test.http_client.middleware'); + } + + /** + * Test actual call to Edge API that IsValidEdgeCredentials() uses. + */ + public function testIsValidEdgeCredentialsEdgeApi() { + $url = $this->endpoint . '/o/' . $this->organization; + $response = $this->httpClient->get($url, [ + 'auth' => [$this->orgadminEmail, $this->orgadminPassword], + 'headers' => ['Accept' => 'application/json'], + ]); + + $body = json_decode($response->getBody()); + $this->assertTrue(isset($body->name), 'Edge org entity should contain "name" attribute.'); + $this->assertEquals($this->organization, $body->name, 'Edge org name attribute should match org being called in url.'); + } + + /** + * Test Edge API response/request for doesRoleExist() + */ + public function testDoesRoleExist() { + // Role should not exist. + $url = $this->endpoint . '/o/' . $this->organization . '/userroles/' . self::TEST_ROLE_NAME; + + $response = $this->httpClient->get($url, [ + 'http_errors' => FALSE, + 'auth' => [$this->orgadminEmail, $this->orgadminPassword], + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + ]); + $this->assertEquals('404', $response->getStatusCode(), 'Role that does not exist should return 404.'); + + } + + /** + * Test Edge API for creating role and setting permissions. + */ + public function testCreateEdgeRoleAndSetPermissions() { + + $url = $this->endpoint . '/o/' . $this->organization . '/userroles'; + $response = $this->httpClient->post($url, [ + 'body' => json_encode([ + 'role' => [self::TEST_ROLE_NAME], + ]), + 'auth' => [$this->orgadminEmail, $this->orgadminPassword], + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + ]); + $this->assertEquals('201', $response->getStatusCode(), 'Role should be created.'); + + // Add permissions to this role. + $url = $this->endpoint . '/o/' . $this->organization . '/userroles/' . self::TEST_ROLE_NAME . '/permissions'; + $body = json_encode([ + 'path' => '/developers', + 'permissions' => ['get', 'put', 'delete'], + ]); + $response = $this->httpClient->post($url, [ + 'body' => $body, + 'auth' => [$this->orgadminEmail, $this->orgadminPassword], + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + ]); + $this->assertEquals('201', $response->getStatusCode(), 'Permission on role should be created.'); + } + +} diff --git a/tests/src/Unit/Command/CreateEdgeRoleCommandTest.php b/tests/src/Unit/Command/CreateEdgeRoleCommandTest.php new file mode 100644 index 000000000..491fe1a67 --- /dev/null +++ b/tests/src/Unit/Command/CreateEdgeRoleCommandTest.php @@ -0,0 +1,224 @@ +cliService = $this->prophesize(CliServiceInterface::class); + $this->logMessageParser = $this->prophesize(LogMessageParserInterface::class); + $this->loggerChannelFactory = $this->prophesize(LoggerChannelFactoryInterface::class); + $this->createEdgeRoleCommand = new CreateEdgeRoleCommand($this->cliService->reveal(), + $this->logMessageParser->reveal(), $this->loggerChannelFactory->reveal()); + + $this->input = $this->prophesize(InputInterface::class); + $this->output = $this->prophesize(OutputInterface::class); + $this->io = $this->prophesize(DrupalStyle::class); + + $this->outputFormatter = $this->prophesize(OutputFormatterInterface::class)->reveal(); + $this->output->getFormatter()->willReturn($this->outputFormatter); + $this->output->getVerbosity()->willReturn(OutputInterface::VERBOSITY_DEBUG); + } + + /** + * Calls to Drush command should pass through to CLI service. + */ + public function testCreateEdgeRole() { + $this->input->getArgument(Argument::type('string'))->willReturn('XXX'); + $this->input->getOption(Argument::type('string'))->willReturn('XXX'); + + $this->createEdgeRoleCommand->execute($this->input->reveal(), $this->output->reveal()); + + $this->cliService->createEdgeRoleForDrupal( + Argument::type(DrupalStyle::class), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('bool') + )->shouldHaveBeenCalledTimes(1); + } + + /** + * Calls to Drush command should pass through to CLI service. + */ + public function testCreateEdgeRoleForceParam() { + $this->input->getArgument(Argument::is('org'))->willReturn('myorg'); + $this->input->getArgument(Argument::is('email'))->willReturn('email@example.com'); + $this->input->getOption(Argument::is('password'))->willReturn('secret'); + $this->input->getOption(Argument::is('base-url'))->willReturn('http://base-url'); + $this->input->getOption(Argument::is('role-name'))->willReturn('custom_drupal_role'); + $this->input->getOption(Argument::is('force'))->willReturn('true'); + + $this->createEdgeRoleCommand->execute($this->input->reveal(), $this->output->reveal()); + + $this->cliService->createEdgeRoleForDrupal( + Argument::type(DrupalStyle::class), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('bool') + )->shouldHaveBeenCalledTimes(1); + } + + /** + * Test validateCreateEdgeRole function does not prompt for password. + * + * When password option is set, do not prompt for password. + */ + public function testInteractWithPasswordParam() { + + $this->input->getArgument(Argument::type('string'))->willReturn('XXX'); + $this->input->getOption('password')->willReturn('secret'); + $this->input->getOption(Argument::type('string'))->willReturn('XXX'); + $this->input->isInteractive()->willReturn(FALSE); + + $this->createEdgeRoleCommand->interact($this->input->reveal(), $this->output->reveal()); + + // Interact should not change password since it was passed in. + $this->input->getOption('password')->shouldHaveBeenCalled(); + $this->input->setOption('password')->shouldNotHaveBeenCalled(); + } + + /** + * Test validateCreateEdgeRole prompts for password. + * + * When password option not set, password should be inputted by user. + */ + public function testInteractPasswordParamEmpty() { + + $this->input->getArgument(Argument::type('string'))->willReturn('XXX'); + $this->input->getOption('password')->willReturn(NULL); + $this->input->getOption(Argument::type('string'))->willReturn('XXX'); + $this->input->setOption(Argument::type('string'), NULL)->willReturn(NULL); + $this->input->isInteractive()->willReturn(FALSE); + + $this->createEdgeRoleCommand->interact($this->input->reveal(), $this->output->reveal()); + + // Interact should not change password since it was passed in. + $this->input->getOption('password')->shouldHaveBeenCalled(); + $this->input->setOption('password', NULL)->shouldHaveBeenCalled(); + } + + } +} + +namespace { + + // phpcs:disable PSR2.Namespaces.UseDeclaration.UseAfterNamespace + use Drush\Utils\StringUtils; + + /** + * Mock out t() so function exists for tests. + * + * @param string $message + * The string with placeholders to be interpolated. + * @param array $context + * An associative array of values to be inserted into the message. + * + * @return string + * The resulting string with all placeholders filled in. + */ + function t(string $message, array $context = []): string { + return StringUtils::interpolate($message, $context); + } + +} diff --git a/tests/src/Unit/Command/Util/ApigeeEdgeManagementCliServiceTest.php b/tests/src/Unit/Command/Util/ApigeeEdgeManagementCliServiceTest.php new file mode 100644 index 000000000..e6caeaedd --- /dev/null +++ b/tests/src/Unit/Command/Util/ApigeeEdgeManagementCliServiceTest.php @@ -0,0 +1,518 @@ +httpClient = $this->prophesize(Client::class); + } + + /** + * Call createEdgeRoleForDrupal with null base URL to test default base URL. + */ + public function testCreateEdgeRoleForDrupalCustomRoleAndBaseUrl() { + // Output to user should show role created and permissions set. + $io = $this->prophesize(StyleInterface::class); + $io->success(Argument::exact('Connected to Edge org ' . $this->org . '.'))->shouldBeCalledTimes(1); + $io->success(Argument::containingString('Role ' . $this->roleName . ' is configured.'))->shouldBeCalledTimes(1); + $io->text(Argument::containingString('Role ' . $this->roleName . ' does not exist. Creating role.'))->shouldBeCalledTimes(1); + $io->text(Argument::containingString('Setting permissions on role ' . $this->roleName . '.'))->shouldBeCalledTimes(1); + $io->text(Argument::containingString('/'))->shouldBeCalledTimes(12); + + // Org should exist. + $response_org = $this->prophesize(Response::class); + $response_org->getBody() + ->shouldBeCalledTimes(1) + ->willReturn('{ "name": "' . $this->org . '" }'); + $this->httpClient + ->get(Argument::exact($this->baseUrl . '/o/' . $this->org), Argument::type('array')) + ->shouldBeCalledTimes(1) + ->willReturn($response_org->reveal()); + + // The role should not exist yet in system. + $request_role = $this->prophesize(RequestInterface::class); + $response_role = $this->prophesize(Response::class); + $response_role->getStatusCode()->willReturn(404); + $exception = new ClientException('Forbidden', $request_role->reveal(), $response_role->reveal()); + $this->httpClient + ->get(Argument::exact($this->baseUrl . '/o/' . $this->org . '/userroles/' . $this->roleName), Argument::type('array')) + ->willThrow($exception); + + // The role should be created. + $this->httpClient + ->post(Argument::exact($this->baseUrl . '/o/' . $this->org . '/userroles'), Argument::type('array')) + ->shouldBeCalledTimes(1); + + // The permissions should be set properly. + $this->httpClient + ->post(Argument::exact($this->baseUrl . '/o/' . $this->org . '/userroles/' . $this->roleName . '/permissions'), Argument::type('array')) + ->shouldBeCalledTimes(12); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $apigee_edge_management_cli_service->createEdgeRoleForDrupal($io->reveal(), [$this, 'mockDt'], $this->org, $this->email, $this->password, $this->baseUrl, $this->roleName, FALSE); + } + + /** + * Pass null role name to test using default role name. + */ + public function testCreateEdgeRoleForDrupalDefaultRoleAndBaseUrl() { + // Output to user should show role created and permissions set. + $io = $this->prophesize(StyleInterface::class); + $io->success(Argument::exact('Connected to Edge org ' . $this->org . '.'))->shouldBeCalledTimes(1); + $io->success(Argument::containingString('Role ' . ApigeeEdgeManagementCliServiceInterface::DEFAULT_ROLE_NAME . ' is configured.'))->shouldBeCalledTimes(1); + $io->text(Argument::containingString('Role ' . ApigeeEdgeManagementCliServiceInterface::DEFAULT_ROLE_NAME . ' does not exist'))->shouldBeCalledTimes(1); + $io->text(Argument::containingString('Setting permissions on role ' . ApigeeEdgeManagementCliServiceInterface::DEFAULT_ROLE_NAME . '.'))->shouldBeCalledTimes(1); + $io->text(Argument::containingString('/'))->shouldBeCalledTimes(12); + + // Org should exist. + $response_org = $this->prophesize(Response::class); + $response_org->getBody() + ->shouldBeCalledTimes(1) + ->willReturn('{ "name": "' . $this->org . '" }'); + $this->httpClient + ->get(Argument::exact(ApigeeClientInterface::DEFAULT_ENDPOINT . '/o/' . $this->org), Argument::type('array')) + ->shouldBeCalledTimes(1) + ->willReturn($response_org->reveal()); + + // The role should not exist yet in system. + $request_role = $this->prophesize(RequestInterface::class); + $response_role = $this->prophesize(Response::class); + $response_role->getStatusCode()->willReturn(404); + $exception = new ClientException('Forbidden', $request_role->reveal(), $response_role->reveal()); + $this->httpClient + ->get(Argument::exact(ApigeeClientInterface::DEFAULT_ENDPOINT . '/o/' . $this->org . '/userroles/' . ApigeeEdgeManagementCliServiceInterface::DEFAULT_ROLE_NAME), Argument::type('array')) + ->willThrow($exception); + + // The role should be created. + $this->httpClient + ->post(Argument::exact(ApigeeClientInterface::DEFAULT_ENDPOINT . '/o/' . $this->org . '/userroles'), Argument::type('array')) + ->shouldBeCalledTimes(1); + + // The permissions should be set. + $this->httpClient + ->post(Argument::exact(ApigeeClientInterface::DEFAULT_ENDPOINT . '/o/' . $this->org . '/userroles/' . ApigeeEdgeManagementCliServiceInterface::DEFAULT_ROLE_NAME . '/permissions'), Argument::type('array')) + ->shouldBeCalledTimes(12); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $apigee_edge_management_cli_service->createEdgeRoleForDrupal($io->reveal(), [$this, 'mockDt'], $this->org, $this->email, $this->password, NULL, NULL, FALSE); + } + + /** + * Allow role to get modified w/force option. + */ + public function testCreateEdgeRoleForDrupalWhenRoleExistsTestWithForceFlag() { + // Expected to output error if role does not exist. + $io = $this->prophesize(StyleInterface::class); + $io->success(Argument::exact('Connected to Edge org ' . $this->org . '.'))->shouldBeCalledTimes(1); + $io->text(Argument::containingString('Setting permissions on role ' . $this->roleName . '.'))->shouldBeCalledTimes(1); + $io->text(Argument::containingString('/'))->shouldBeCalledTimes(12); + $io->success(Argument::containingString('Role ' . $this->roleName . ' is configured.'))->shouldBeCalledTimes(1); + + // Return organization info. + $response_org = $this->prophesize(Response::class); + $response_org->getBody() + ->shouldBeCalledTimes(1) + ->willReturn('{ "name": "' . $this->org . '" }'); + $this->httpClient + ->get(Argument::exact($this->baseUrl . '/o/' . $this->org), Argument::type('array')) + ->shouldBeCalledTimes(1) + ->willReturn($response_org->reveal()); + + // Return existing role. + $response_user_role = $this->prophesize(Response::class); + $response_user_role->getBody()->willReturn('{ "name": "' . $this->roleName . '" }'); + $this->httpClient + ->get(Argument::exact($this->baseUrl . '/o/' . $this->org . '/userroles/' . $this->roleName), Argument::type('array')) + ->willReturn($response_user_role->reveal()); + + // The role should NOT be created since is already exists. + $this->httpClient + ->post(Argument::exact($this->baseUrl . '/o/' . $this->org . '/userroles'), Argument::type('array')) + ->shouldNotBeCalled(); + + // The permissions should be set. + $this->httpClient + ->post(Argument::exact($this->baseUrl . '/o/' . $this->org . '/userroles/' . $this->roleName . '/permissions'), Argument::type('array')) + ->shouldBeCalledTimes(12); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $apigee_edge_management_cli_service->createEdgeRoleForDrupal($io->reveal(), [$this, 'mockDt'], $this->org, $this->email, $this->password, $this->baseUrl, $this->roleName, TRUE); + } + + /** + * If force parameter is not passed in, do not mess with a role that exists. + */ + public function testCreateEdgeRoleForDrupalWhenRoleExistsTestNoForceFlag() { + // Expected to output error if role does not exist. + $io = $this->prophesize(StyleInterface::class); + $io->success(Argument::exact('Connected to Edge org ' . $this->org . '.'))->shouldBeCalledTimes(1); + $io->error(Argument::containingString('Role ' . $this->roleName . ' already exists.'))->shouldBeCalledTimes(1); + $io->note(Argument::containingString('Run with --force option'))->shouldBeCalled(); + + // Return organization info. + $response_org = $this->prophesize(Response::class); + $response_org->getBody() + ->shouldBeCalledTimes(1) + ->willReturn('{ "name": "' . $this->org . '" }'); + $this->httpClient + ->get(Argument::exact($this->baseUrl . '/o/' . $this->org), Argument::type('array')) + ->shouldBeCalledTimes(1) + ->willReturn($response_org->reveal()); + + // Return existing role. + $response_user_role = $this->prophesize(Response::class); + $response_user_role->getBody()->willReturn('{ "name": "' . $this->roleName . '" }'); + $this->httpClient + ->get(Argument::exact($this->baseUrl . '/o/' . $this->org . '/userroles/' . $this->roleName), Argument::type('array')) + ->willReturn($response_user_role->reveal()); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $apigee_edge_management_cli_service->createEdgeRoleForDrupal($io->reveal(), [$this, 'mockDt'], $this->org, $this->email, $this->password, $this->baseUrl, $this->roleName, FALSE); + } + + /** + * Test isValidEdgeCredentials() bad endpoint response. + */ + public function testIsValidEdgeCredentialsBadEndpoint() { + // Mimic a invalid response for the call to get org details. + $body = "

not json

"; + $response = $this->prophesize(Response::class); + $response->getBody() + ->shouldBeCalledTimes(1) + ->willReturn($body); + + // The user should see an error message. + $io = $this->prophesize(StyleInterface::class); + $io->error(Argument::containingString('Unable to parse response from GET')) + ->shouldBeCalledTimes(1); + $this->httpClient + ->get(Argument::type('string'), Argument::type('array')) + ->willReturn($response->reveal()); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $is_valid_creds = $apigee_edge_management_cli_service->isValidEdgeCredentials($io->reveal(), [$this, 'mockDt'], $this->org, $this->email, $this->password, $this->baseUrl); + + // Assert return that creds are false. + $this->assertEquals(FALSE, $is_valid_creds, 'Credentials are not valid, should return false.'); + } + + /** + * Test isValidEdgeCredentials() unauthorized response. + */ + public function testIsValidEdgeCredentialsUnauthorized() { + // Invalid password returns unauthorized 403. + $request_role = $this->prophesize(RequestInterface::class); + $response_role = $this->prophesize(Response::class); + $response_role->getStatusCode()->willReturn(403); + $exception = new ClientException('Unauthorized', $request_role->reveal(), $response_role->reveal()); + $this->httpClient + ->get(Argument::exact($this->baseUrl . '/o/' . $this->org), Argument::type('array')) + ->willThrow($exception) + ->shouldBeCalledTimes(1); + + // The user should see an error message. + $io = $this->prophesize(StyleInterface::class); + $io->error(Argument::containingString('Error connecting to Apigee Edge')) + ->shouldBeCalledTimes(1); + $io->note(Argument::containingString('may not have the orgadmin role for Apigee Edge org')) + ->shouldBeCalledTimes(1); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $is_valid_creds = $apigee_edge_management_cli_service->isValidEdgeCredentials($io->reveal(), [$this, 'mockDt'], $this->org, $this->email, $this->password, $this->baseUrl); + $this->assertEquals(FALSE, $is_valid_creds, 'Credentials are not valid, should return false.'); + } + + /** + * Should return true if creds are valid. + */ + public function testIsValidEdgeCredentialsValid() { + // Org should exist. + $response_org = $this->prophesize(Response::class); + $response_org->getBody() + ->shouldBeCalledTimes(1) + ->willReturn('{ "name": "' . $this->org . '" }'); + $this->httpClient + ->get(Argument::exact($this->baseUrl . '/o/' . $this->org), Argument::type('array')) + ->shouldBeCalledTimes(1) + ->willReturn($response_org->reveal()); + + // Errors should not be called. + $io = $this->prophesize(StyleInterface::class); + $io->error(Argument::type('string')) + ->shouldNotBeCalled(); + $io->section(Argument::type('string')) + ->shouldNotBeCalled(); + $io->text(Argument::type('string')) + ->shouldNotBeCalled(); + $io->success(Argument::type('string')) + ->shouldBeCalled(); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $is_valid_creds = $apigee_edge_management_cli_service->isValidEdgeCredentials($io->reveal(), [$this, 'mockDt'], $this->org, $this->email, $this->password, $this->baseUrl); + + // Assertions. + $this->assertEquals(TRUE, $is_valid_creds, 'Credentials are not valid, should return false.'); + } + + /** + * Validate doesRoleExist works when role does not exist. + */ + public function testDoesRoleExistTrue() { + // Return existing role. + $response_user_role = $this->prophesize(Response::class); + $response_user_role->getBody()->willReturn('{ "name": "' . $this->roleName . '" }'); + $this->httpClient + ->get(Argument::exact($this->baseUrl . '/o/' . $this->org . '/userroles/' . $this->roleName), Argument::type('array')) + ->shouldBeCalledTimes(1) + ->willReturn($response_user_role->reveal()); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $does_role_exist = $apigee_edge_management_cli_service->doesRoleExist($this->org, $this->email, $this->password, $this->baseUrl, $this->roleName); + + // Assert returned true. + $this->assertEquals(TRUE, $does_role_exist, 'Method doesRoleExist() should return true when role exists.'); + } + + /** + * Validate doesRoleExist works when role exists. + */ + public function testDoesRoleExistNotTrue() { + // The role should not exist in system. + $request_role = $this->prophesize(RequestInterface::class); + $response_role = $this->prophesize(Response::class); + $response_role->getStatusCode()->willReturn(404); + $exception = new ClientException('Forbidden', $request_role->reveal(), $response_role->reveal()); + $this->httpClient + ->get(Argument::exact($this->baseUrl . '/o/' . $this->org . '/userroles/' . $this->roleName), Argument::type('array')) + ->willThrow($exception); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $does_role_exist = $apigee_edge_management_cli_service->doesRoleExist($this->org, $this->email, $this->password, $this->baseUrl, $this->roleName); + + // Assert returns false. + $this->assertEquals(FALSE, $does_role_exist, 'Method doesRoleExist() should return false when role exists.'); + } + + /** + * Validate when exception thrown function works correctly. + */ + public function testDoesRoleExistServerErrorThrown() { + // Http client throws exception if network or server error happens. + $request = $this->prophesize(RequestInterface::class); + $response = $this->prophesize(Response::class); + $response->getStatusCode()->willReturn(500); + $exception = new ServerException('Server error.', $request->reveal(), $response->reveal()); + $this->expectException(ServerException::class); + $this->httpClient + ->get(Argument::type('string'), Argument::type('array')) + ->willThrow($exception); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $apigee_edge_management_cli_service->doesRoleExist($this->org, $this->email, $this->password, $this->baseUrl, $this->roleName); + } + + /** + * Make sure method outputs more info for error codes. + */ + public function testHandleHttpClientExceptions0Code() { + // Error message should output to user. + $io = $this->prophesize(StyleInterface::class); + $io->error(Argument::containingString('Error connecting to Apigee Edge'))->shouldBeCalledTimes(1); + $io->note(Argument::containingString('Your system may not be able to connect'))->shouldBeCalledTimes(1); + + // Create network error. + $exception = $this->prophesize(TransferException::class); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $apigee_edge_management_cli_service->handleHttpClientExceptions($exception->reveal(), $io->reveal(), [$this, 'mockDt'], 'http://api.apigee.com/test', $this->org, $this->email); + } + + /** + * Make sure method outputs more info for error codes. + */ + public function testHandleHttpClientExceptions401Code() { + // Server returns 401 unauthorized. + $request = $this->prophesize(RequestInterface::class); + $response = $this->prophesize(Response::class); + $response->getStatusCode()->willReturn(401); + $exception = new ClientException('Unauthorized', $request->reveal(), $response->reveal()); + + // Expect user friendly message displayed about error. + $io = $this->prophesize(StyleInterface::class); + $io->error(Argument::containingString('Error connecting to Apigee Edge'))->shouldBeCalledTimes(1); + $io->note(Argument::exact('Your username or password is invalid.'))->shouldBeCalledTimes(1); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $apigee_edge_management_cli_service->handleHttpClientExceptions($exception, $io->reveal(), [$this, 'mockDt'], 'http://api.apigee.com/test', $this->org, $this->email); + } + + /** + * Make sure method outputs more info for error codes. + */ + public function testHandleHttpClientExceptions403Code() { + // Server returns 403 forbidden. + $request = $this->prophesize(RequestInterface::class); + $response = $this->prophesize(Response::class); + $response->getStatusCode()->willReturn(403); + $exception = new ClientException('Forbidden', $request->reveal(), $response->reveal()); + + // Expect error messages. + $io = $this->prophesize(StyleInterface::class); + $io->error(Argument::containingString('Error connecting to Apigee Edge'))->shouldBeCalledTimes(1); + $io->note(Argument::containingString('User ' . $this->email . ' may not have the orgadmin role'))->shouldBeCalledTimes(1); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $apigee_edge_management_cli_service->handleHttpClientExceptions($exception, $io->reveal(), [$this, 'mockDt'], 'http://api.apigee.com/test', $this->org, $this->email); + } + + /** + * Make sure method outputs more info for error codes. + */ + public function testHandleHttpClientExceptions302Code() { + // Return a 302 redirection response, which Apigee API would not do. + $request = $this->prophesize(RequestInterface::class); + $response = $this->prophesize(Response::class); + $response->getStatusCode()->willReturn(302); + $exception = new ClientException('Forbidden', $request->reveal(), $response->reveal()); + + // User should see error message. + $io = $this->prophesize(StyleInterface::class); + $io->error(Argument::containingString('Error connecting to Apigee Edge'))->shouldBeCalledTimes(1); + $io->note(Argument::containingString('the url ' . $this->baseUrl . '/test' . ' does not seem to be a valid Apigee Edge endpoint.'))->shouldBeCalledTimes(1); + + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $apigee_edge_management_cli_service->handleHttpClientExceptions($exception, $io->reveal(), [$this, 'mockDt'], $this->baseUrl . '/test', $this->org, $this->email); + } + + /** + * Test setDefaultPermissions method. + * + * @throws \ReflectionException + */ + public function testSetDefaultPermissions() { + // The permissions POST call will be made 12 times. + $this->httpClient->post(Argument::type('string'), Argument::type('array'))->shouldBeCalledTimes(12); + + // Make method under test not private. + $apigee_edge_management_cli_service = new ApigeeEdgeManagementCliService($this->httpClient->reveal()); + $apigee_edge_management_cli_service_reflection = new ReflectionClass($apigee_edge_management_cli_service); + $method_set_default_permissions = $apigee_edge_management_cli_service_reflection->getMethod('setDefaultPermissions'); + $method_set_default_permissions->setAccessible(TRUE); + + // Create input params. + $io = $this->prophesize(StyleInterface::class); + $args = [ + $io->reveal(), + [$this, 'mockDt'], + $this->org, + $this->email, + $this->password, + $this->baseUrl, + $this->roleName, + ]; + + // Make call. + $method_set_default_permissions->invokeArgs($apigee_edge_management_cli_service, $args); + } + + /** + * Mock translation method. + * + * @param string $message + * The message to return. + * @param array $context + * The context of vars to replace. + * + * @return string + * The message with context. + */ + public function mockDt(string $message, array $context = []): string { + // Do the same thing as Drush dt(). + return StringUtils::interpolate($message, $context); + } + +} diff --git a/tests/src/Unit/Commands/ApigeeEdgeCommandsTest.php b/tests/src/Unit/Commands/ApigeeEdgeCommandsTest.php new file mode 100644 index 000000000..a97470977 --- /dev/null +++ b/tests/src/Unit/Commands/ApigeeEdgeCommandsTest.php @@ -0,0 +1,227 @@ +cliService = $this->prophesize(CliServiceInterface::class); + $this->apigeeEdgeCommands = new ApigeeEdgeCommands($this->cliService->reveal()); + + // Set io in DrushCommands to a mock. + $apigee_edge_commands_reflection = new ReflectionClass($this->apigeeEdgeCommands); + $reflection_io_property = $apigee_edge_commands_reflection->getProperty('io'); + $reflection_io_property->setAccessible(TRUE); + $this->io = $this->prophesize(DrushStyle::class); + $reflection_io_property->setValue($this->apigeeEdgeCommands, $this->io->reveal()); + + $this->io->askHidden(Argument::type('string'), Argument::any()) + ->willReturn('I<3APIS!'); + } + + /** + * Calls to Drush command should pass through to CLI service. + */ + public function testCreateEdgeRole() { + + $drush_options = [ + 'password' => 'opensesame', + 'base-url' => 'http://api.apigee.com/v1', + 'role-name' => 'portalRole', + 'force' => 'FALSE', + ]; + + $this->apigeeEdgeCommands->createEdgeRole('orgA', 'emailA', $drush_options); + + $this->cliService->createEdgeRoleForDrupal( + Argument::type(DrushStyle::class), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('bool') + ) + ->shouldHaveBeenCalledTimes(1); + + } + + /** + * Test validateCreateEdgeRole function does not prompt for password. + * + * When password option is set, do not prompt for password. + */ + public function testValidatePasswordParam() { + + $command_data_input = $this->prophesize(InputInterface::class); + $command_data_input->getOption('password')->willReturn('secret'); + $command_data_input->getArgument('email')->willReturn('email.example.com'); + $command_data = $this->prophesize(CommandData::class); + $command_data->input()->willReturn($command_data_input->reveal()); + + $this->apigeeEdgeCommands->validateCreateEdgeRole($command_data->reveal()); + + // Make sure password was not prompted to user. + $command_data_input->getOption('password')->shouldHaveBeenCalled(); + $this->io->askHidden(Argument::type('string'), Argument::any()) + ->shouldNotBeCalled(); + $command_data_input->setOption()->shouldNotHaveBeenCalled(); + } + + /** + * Test validateCreateEdgeRole prompts for password. + * + * When password option not set, password should be inputted by user. + */ + public function testValidatePasswordParamEmpty() { + + $command_data_input = $this->prophesize(InputInterface::class); + $command_data_input->getOption('password')->willReturn(NULL); + $command_data_input->setOption(Argument::type('string'), Argument::type('string'))->willReturn(); + $command_data_input->getArgument('email')->willReturn('email.example.com'); + $command_data = $this->prophesize(CommandData::class); + $command_data->input()->willReturn($command_data_input->reveal()); + + $this->apigeeEdgeCommands->validateCreateEdgeRole($command_data->reveal()); + + // Make sure password not requested. + $command_data_input->getOption('password')->shouldHaveBeenCalled(); + $this->io->askHidden(Argument::type('string'), Argument::any()) + ->shouldBeCalled(); + $command_data_input->setOption('password', 'I<3APIS!') + ->shouldHaveBeenCalled(); + } + + /** + * Test calling with force function when role already exists. + */ + public function testCreateEdgeEdgeRoleWithForceParam() { + $drush_options = [ + 'password' => 'opensesame', + 'base-url' => 'http://api.apigee.com/v1', + 'role-name' => 'portalRole', + 'force' => TRUE, + ]; + + $this->apigeeEdgeCommands->createEdgeRole('orgA', 'emailA', $drush_options); + + $this->cliService->createEdgeRoleForDrupal( + Argument::type(DrushStyle::class), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + TRUE + ) + ->shouldHaveBeenCalledTimes(1); + } + + /** + * Test calling when role exists but force flag not given, should error. + */ + public function testCreateEdgeEdgeRoleWithoutForceParam() { + $drush_options = [ + 'password' => 'opensesame', + 'base-url' => 'http://api.apigee.com/v1', + 'role-name' => 'portalRole', + 'force' => FALSE, + ]; + + $this->apigeeEdgeCommands->createEdgeRole('orgA', 'emailA', $drush_options); + + $this->cliService->createEdgeRoleForDrupal( + Argument::type(DrushStyle::class), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + Argument::type('string'), + FALSE + ) + ->shouldHaveBeenCalledTimes(1); + } + + } +} + +namespace { + + // phpcs:disable PSR2.Namespaces.UseDeclaration.UseAfterNamespace + use Drush\Utils\StringUtils; + + /** + * Mock out dt() so function exists for tests. + * + * @param string $message + * The string with placeholders to be interpolated. + * @param array $context + * An associative array of values to be inserted into the message. + * + * @return string + * The resulting string with all placeholders filled in. + */ + function dt(string $message, array $context = []): string { + return StringUtils::interpolate($message, $context); + } + +}