From 9e2962d56111bb6b4be375f8d43e6beb7a9ee74d Mon Sep 17 00:00:00 2001 From: Kiran Parajuli Date: Fri, 25 Mar 2022 15:33:35 +0545 Subject: [PATCH] added graph helper Signed-off-by: Kiran Parajuli --- tests/TestHelpers/GraphHelper.php | 498 ++++++++++++++++++ tests/acceptance/config/behat.yml | 14 + .../features/apiGraphGroup/addGroup.feature | 52 ++ .../features/apiGraphUser/addUser.feature | 63 +++ .../features/bootstrap/GraphContext.php | 152 ++++++ .../features/bootstrap/bootstrap.php | 2 +- 6 files changed, 780 insertions(+), 1 deletion(-) create mode 100644 tests/TestHelpers/GraphHelper.php create mode 100644 tests/acceptance/features/apiGraphGroup/addGroup.feature create mode 100644 tests/acceptance/features/apiGraphUser/addUser.feature create mode 100644 tests/acceptance/features/bootstrap/GraphContext.php diff --git a/tests/TestHelpers/GraphHelper.php b/tests/TestHelpers/GraphHelper.php new file mode 100644 index 0000000000..87a16f542d --- /dev/null +++ b/tests/TestHelpers/GraphHelper.php @@ -0,0 +1,498 @@ + + * @copyright Copyright (c) 2022 Kiran Parajuli kiran@jankaritech.com + */ + +namespace TestHelpers; + +use Exception; +use GuzzleHttp\Exception\GuzzleException; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * A helper class for managing users and groups using the Graph API + */ +class GraphHelper { + /** + * @param string $baseUrl + * @param string $path + * + * @return string + */ + private static function getFullUrl(string $baseUrl, string $path):string { + $fullUrl = $baseUrl; + if (\substr($fullUrl, -1) !== '/') { + $fullUrl .= '/'; + } + $fullUrl .= 'graph/v1.0/' . $path; + return $fullUrl; + } + + /** + * @param string $baseUrl + * @param string $xRequestId + * @param string $method + * @param string $path + * @param string|null $body + * @param array|null $headers + * + * @return RequestInterface + */ + public static function createRequest( + string $baseUrl, + string $xRequestId, + string $method, + string $path, + ?string $body = null, + ?array $headers = [] + ): RequestInterface { + $fullUrl = self::getFullUrl($baseUrl, $path); + return HttpRequestHelper::createRequest( + $fullUrl, + $xRequestId, + $method, + $headers, + $body + ); + } + + /** + * @param string $baseUrl + * @param string $xRequestId + * @param string $adminUser + * @param string $adminPassword + * @param string $userName + * @param string $password + * @param string|null $email + * @param string|null $displayName + * + * @return ResponseInterface + */ + public static function createUser( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $userName, + string $password, + ?string $email = null, + ?string $displayName = null + ):ResponseInterface { + $payload = self::prepareCreateUserPayload( + $userName, + $password, + $email, + $displayName + ); + + $headers = ['Content-Type' => 'application/json']; + $url = self::getFullUrl($baseUrl, 'users'); + return HttpRequestHelper::post( + $url, + $xRequestId, + $adminUser, + $adminPassword, + $headers, + $payload + ); + } + + /** + * @param string $baseUrl + * @param string $xRequestId + * @param string $adminUser + * @param string $adminPassword + * @param string $userId + * @param string|null $userName + * @param string|null $password + * @param string|null $email + * @param string|null $displayName + * + * @return ResponseInterface + */ + public static function editUser( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $userId, + ?string $userName = null, + ?string $password = null, + ?string $email = null, + ?string $displayName = null + + ): ResponseInterface { + $payload = self::preparePatchUserPayload( + $userName, + $password, + $email, + $displayName + ); + $headers = ['Content-Type' => 'application/json']; + $url = self::getFullUrl($baseUrl, 'users/' . $userId); + return HttpRequestHelper::sendRequest( + $url, + $xRequestId, + "PATCH", + $adminUser, + $adminPassword, + $headers, + $payload + ); + } + + /** + * @param string $baseUrl + * @param string $xRequestId + * @param string $adminUser + * @param string $adminPassword + * @param string $userName + * + * @return ResponseInterface + * @throws GuzzleException + */ + public static function getUser( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $userName + ):ResponseInterface { + $url = self::getFullUrl($baseUrl, 'users/' . $userName); + return HttpRequestHelper::get( + $url, + $xRequestId, + $adminUser, + $adminPassword, + ["Content-Type" => "application/json"] + ); + } + + /** + * @param string $baseUrl + * @param string $xRequestId + * @param string $adminUser + * @param string $adminPassword + * @param string $userName + * + * @return ResponseInterface + * @throws GuzzleException + */ + public static function deleteUser( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $userName + ):ResponseInterface { + $url = self::getFullUrl($baseUrl, 'users/' . $userName); + return HttpRequestHelper::delete( + $url, + $xRequestId, + $adminUser, + $adminPassword, + ); + } + + /** + * can send a request to the graph api to: + * - create a group + * - update a group + * + * displayName is the only field that can be assigned/updated + * + * @param string $baseUrl + * @param string $xRequestId + * @param string $adminUser + * @param string $adminPassword + * @param string $groupName - the displayName of the group + * @param bool|null $update + * + * @return ResponseInterface + * @throws GuzzleException + */ + private static function postPatchGroup( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $groupName, + ?bool $update = false + ): ResponseInterface { + $url = ($update) + ? self::getFullUrl($baseUrl, 'groups/' . $groupName) + : self::getFullUrl($baseUrl, 'groups'); + $method = ($update) ? 'PATCH' : 'POST'; + $headers = ['Content-Type' => 'application/json']; + $payload['displayName'] = $groupName; + return HttpRequestHelper::sendRequest( + $url, + $xRequestId, + $method, + $adminUser, + $adminPassword, + $headers, + \json_encode($payload) + ); + } + + /** + * @param string $baseUrl + * @param string $xRequestId + * @param string $adminUser + * @param string $adminPassword + * @param string $groupName + * + * @return ResponseInterface + * @throws GuzzleException + */ + public static function createGroup( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $groupName + ):ResponseInterface { + return self::postPatchGroup( + $baseUrl, + $xRequestId, + $adminUser, + $adminPassword, + $groupName + ); + } + + /** + * @param string $baseUrl + * @param string $xRequestId + * @param string $adminUser + * @param string $adminPassword + * @param string $groupId + * @param string $displayName + * + * @return ResponseInterface + * @throws GuzzleException + */ + public static function updateGroup( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $groupId, + string $displayName + ):ResponseInterface { + return self::postPatchGroup( + $baseUrl, + $xRequestId, + $adminUser, + $adminPassword, + $displayName, + true + ); + } + + /** + * @param string $baseUrl + * @param string $xRequestId + * @param string $adminUser + * @param string $adminPassword + * + * @return array + * @throws Exception + */ + public static function getGroups( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword + ):array { + $url = self::getFullUrl($baseUrl, 'groups'); + $response = HttpRequestHelper::get( + $url, + $xRequestId, + $adminUser, + $adminPassword + ); + $groupsListEncoded = \json_decode($response->getBody()->getContents(), true); + if (!isset($groupsListEncoded['value'])) { + throw new Exception('No groups found'); + } else { + return $groupsListEncoded['value']; + } + } + + /** + * @param string $baseUrl + * @param string $xRequestId + * @param string $adminUser + * @param string $adminPassword + * @param string $groupId + * + * @return ResponseInterface + * @throws GuzzleException + */ + public static function deleteGroup( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $groupId + ):ResponseInterface { + $url = self::getFullUrl($baseUrl, 'groups/' . $groupId); + return HttpRequestHelper::delete( + $url, + $xRequestId, + $adminUser, + $adminPassword, + ); + } + + /** + * @param string $baseUrl + * @param string $xRequestId + * @param string $adminUser + * @param string $adminPassword + * @param string $groupId + * @param array $users expects users array with user ids [ [ 'id' => 'some_id' ], ] + * + * @return ResponseInterface + */ + public static function addUsersToGroup( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $groupId, + array $users + ):ResponseInterface { + $url = self::getFullUrl($baseUrl, 'groups/' . $groupId . '/users'); + $payload = [ + "members@odata.bind" => [] + ]; + foreach ($users as $user) { + $payload[0][] = self::getFullUrl($baseUrl, 'users/' . $user["id"]); + } + return HttpRequestHelper::post( + $url, + $xRequestId, + $adminUser, + $adminPassword, + ['Content-Type' => 'application/json'], + \json_encode($payload) + ); + } + + public static function addUserToGroup( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $userId, + string $groupId + ):ResponseInterface { + $url = self::getFullUrl($baseUrl, 'groups/' . $groupId . '/members/$ref'); + $body = [ + "@odata.id" => self::getFullUrl($baseUrl, 'users/' . $userId) + ]; + return HttpRequestHelper::post( + $url, + $xRequestId, + $adminUser, + $adminPassword, + ["application/json"], + \json_encode($body) + ); + } + + public static function removeUserFromGroup( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $userId, + string $groupId + ): ResponseInterface { + $url = self::getFullUrl($baseUrl, 'groups/' . $groupId . '/members/' . $userId . '/$ref'); + return HttpRequestHelper::delete( + $url, + $xRequestId, + $adminUser, + $adminPassword, + ); + } + + public static function getMembersList( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $userId, + string $groupId + ): bool { + $url = self::getFullUrl($baseUrl, 'groups/' . $groupId . '/members/' . $userId . '/$ref'); + return HttpRequestHelper::get( + $url, + $xRequestId, + $adminUser, + $adminPassword + ); + } + + /** + * @param string $baseUrl + * @param string $xRequestId + * @param string $adminUser + * @param string $adminPassword + * @param string $userId + * + * @return void + */ + public static function getGroupListOfAUser( + string $baseUrl, + string $xRequestId, + string $adminUser, + string $adminPassword, + string $userId + ) { + // TODO: endpoint not available https://github.com/owncloud/ocis/issues/3363 + // Not implemented yet + } + + /** + * @param string|null $userName + * @param string|null $password + * @param string|null $email + * @param string|null $displayName + * + * @return string + */ + public static function prepareCreateUserPayload( + string $userName, + string $password, + ?string $email, + ?string $displayName + ): string { + $payload['onPremisesSamAccountName'] = $userName; + $payload['passwordProfile'] = ['password' => $password]; + $payload['displayName'] = $displayName ?? $userName; + $payload['mail'] = $email ?? $userName . '@example.com'; + return \json_encode($payload); + } + public static function preparePatchUserPayload( + ?string $userName, + ?string $password, + ?string $email, + ?string $displayName + ): string { + $payload = []; + if ($userName) $payload['onPremisesSamAccountName'] = $userName; + if ($password) $payload['passwordProfile'] = ['password' => $password]; + if ($displayName) $payload['displayName'] = $displayName; + if ($email) $payload['mail'] = $email; + return \json_encode($payload); + } +} diff --git a/tests/acceptance/config/behat.yml b/tests/acceptance/config/behat.yml index 8551f5c92c..e8e46956eb 100644 --- a/tests/acceptance/config/behat.yml +++ b/tests/acceptance/config/behat.yml @@ -56,5 +56,19 @@ default: - FilesVersionsContext: - PublicWebDavContext: + apiGraphUser: + paths: + - '%paths.base%/../features/apiGraphUser' + contexts: + - GraphContext: + - FeatureContext: *common_feature_context_params + + apiGraphGroup: + paths: + - '%paths.base%/../features/apiGraphGroup' + contexts: + - GraphContext: + - FeatureContext: *common_feature_context_params + extensions: Cjm\Behat\StepThroughExtension: ~ diff --git a/tests/acceptance/features/apiGraphGroup/addGroup.feature b/tests/acceptance/features/apiGraphGroup/addGroup.feature new file mode 100644 index 0000000000..560fff4e1f --- /dev/null +++ b/tests/acceptance/features/apiGraphGroup/addGroup.feature @@ -0,0 +1,52 @@ +@api +Feature: add groups + As an administrator + I want to be able to create group using the Graph API + So that I can more easily manage access to resources by groups rather than individual users + + Scenario: + When the administrator sends a group creation request for the following groups using the graph API + | group_display_name | + | simplegroup | + | España§àôœ€ | + | नेपाली | + And the HTTP status code of responses on all endpoints should be "200" + And these groups should exist: + | groupname | + | simplegroup | + | España§àôœ€ | + | नेपाली | + + + Scenario: admin creates a group with special characters + When the administrator sends a group creation request for the following groups using the graph API + | group_display_name | comment | + | brand-new-group | dash | + | the.group | dot | + | left,right | comma | + | 0 | The "false" group | + | Finance (NP) | Space and brackets | + | Admin&Finance | Ampersand | + | admin:Pokhara@Nepal | Colon and @ | + | maint+eng | Plus sign | + | $x<=>[y*z^2]! | Maths symbols | + | Mgmt\Middle | Backslash | + | 😅 😆 | emoji | + | [group1] | brackets | + | group [ 2 ] | bracketsAndSpace | + And the HTTP status code of responses on all endpoints should be "200" + And these groups should exist: + | groupname | + | brand-new-group | + | the.group | + | left,right | + | 0 | + | Finance (NP) | + | Admin&Finance | + | admin:Pokhara@Nepal | + | maint+eng | + | $x<=>[y*z^2]! | + | Mgmt\Middle | + | 😅 😆 | + | [group1] | + | group [ 2 ] | diff --git a/tests/acceptance/features/apiGraphUser/addUser.feature b/tests/acceptance/features/apiGraphUser/addUser.feature new file mode 100644 index 0000000000..d794812399 --- /dev/null +++ b/tests/acceptance/features/apiGraphUser/addUser.feature @@ -0,0 +1,63 @@ +@api +Feature: + As an administrator + I want to be able to create user using the Graph API + So that I can manage users more easily + + + @smokeTest + Scenario: admin creates a user + Given user "brand-new-user" has been deleted + When the administrator sends a user creation request for user "brand-new-user" password "%alt1%" using the graph API + Then the HTTP status code should be "200" + And user "brand-new-user" should exist + And user "brand-new-user" should be able to upload file "filesForUpload/textfile.txt" to "/textfile.txt" + + + Scenario Outline: admin creates a user with special characters in the username + Given user "" has been deleted + When the administrator sends a user creation request for user "" password "%alt1%" using the graph API + Then the HTTP status code of responses on all endpoints should be "400" + And the graph API response should return the following error + | code | invalidRequest | + | message | username '' must be at least the local part of an email | + And user "" should not exist + Examples: + | username | + | a@-+_.b | + | a space | + + Scenario: admin creates a user and specifies a password with special characters + When the administrator sends a user creation request for the following users with password using the graph API + | username | password | + | brand-new-user1 | !@#$%^&*()-_+=[]{}:;,.<>?~ | + | brand-new-user2 | España§àôœ€ | + | brand-new-user3 | नेपाली | + And the HTTP status code of responses on all endpoints should be "200" + And the following users should exist + | username | + | brand-new-user1 | + | brand-new-user2 | + | brand-new-user3 | + And the following users should be able to upload file "filesForUpload/textfile.txt" to "/textfile.txt" + | username | + | brand-new-user1 | + | brand-new-user2 | + | brand-new-user3 | + + Scenario: admin tries to create an existing user + And user "brand-new-user" has been created with default attributes and without skeleton files + When the administrator sends a user creation request for user "brand-new-user" password "%alt1%" using the graph API + And the HTTP status code should be "500" + Then the graph API response should return the following error + | code | generalException | + | message | LDAP Result Code 68 "Entry Already Exists":{space} | + + + Scenario: admin creates a user and specifies password containing just space + Given user "brand-new-user" has been deleted + When the administrator sends a user creation request for user "brand-new-user" password " " using the graph API + And the HTTP status code should be "200" + And user "brand-new-user" should exist + And user "brand-new-user" should be able to upload file "filesForUpload/textfile.txt" to "/textfile.txt" + diff --git a/tests/acceptance/features/bootstrap/GraphContext.php b/tests/acceptance/features/bootstrap/GraphContext.php new file mode 100644 index 0000000000..1e81eb3583 --- /dev/null +++ b/tests/acceptance/features/bootstrap/GraphContext.php @@ -0,0 +1,152 @@ + + * @copyright Copyright (c) 2021 Kiran Parajuli kiran@jankaritech.com + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\TableNode; +use TestHelpers\GraphHelper; +use TestHelpers\HttpRequestHelper; +use GuzzleHttp\Exception\GuzzleException; +use PHPUnit\Framework\Assert; + +require_once "bootstrap.php"; + +/** + * Context for the provisioning specific steps using the Graph API + */ +class GraphContext implements Context { + /** + * @var FeatureContext + */ + private FeatureContext $featureContext; + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function before(BeforeScenarioScope $scope):void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context from here + $this->featureContext = $environment->getContext('FeatureContext'); + } + + /** + * @When /^the administrator sends a user creation request for user "([^"]*)" password "([^"]*)" using the graph API$/ + * + * @param string $user + * @param string $password + * + * @return void + * @throws GuzzleException + */ + public function adminSendsUserCreationRequestUsingTheGraphApi(string $user, string $password):void { + $user = $this->featureContext->getActualUsername($user); + $password = $this->featureContext->getActualPassword($password); + $response = GraphHelper::createUser( + $this->featureContext->getBaseUrl(), + $this->featureContext->getStepLineRef(), + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $user, + $password + ); + $this->featureContext->setResponse($response); + $this->featureContext->pushToLastStatusCodesArrays(); + $success = $this->featureContext->theHTTPStatusCodeWasSuccess(); + $this->featureContext->addUserToCreatedUsersList( + $user, + $password, + null, + null, + $success + ); + } + + /** + * @When /^the administrator sends a user creation request for the following users with password using the graph API$/ + * + * @return void + * @throws GuzzleException + */ + public function theAdministratorSendsAUserCreationRequestForTheFollowingUsersWithPasswordUsingTheGraphAPI(TableNode $table) { + $this->featureContext->verifyTableNodeColumns($table, ["username", "password"]); + $users = $table->getHash(); + foreach ($users as $user) { + $this->adminSendsUserCreationRequestUsingTheGraphApi($user["username"], $user["password"]); + } + } + + /** + * @Then /^the graph API response should return the following error$/ + * + * @param TableNode $body + * + * @return void + * @throws Exception + */ + public function theGraphApiResponseShouldReturnTheFollowingError(TableNode $body):void { + $this->featureContext->verifyTableNodeRows($body, ['code', 'message']); + $bodyRows = $body->getRowsHash(); + $responseData = $this->featureContext->getJsonDecodedResponse(); + // parse "{space}" to " " from the message + $bodyRows['message'] = \str_replace('{space}', ' ', $bodyRows['message']); + Assert::assertEquals( + $bodyRows['code'], + $responseData['error']['code'], + "Status code is not as expected" + ); + Assert::assertEquals( + $bodyRows['message'], + $responseData['error']['message'], + "Status message is not as expected" + ); + } + + /** + * @When /^the administrator sends a group creation request for group "([^"]*)" using the graph API$/ + * + * @param string $group + * + * @return void + * @throws GuzzleException + */ + public function adminSendsGroupCreationRequestUsingTheGraphAPI(string $group):void { + $response = GraphHelper::createGroup( + $this->featureContext->getBaseUrl(), + $this->featureContext->getStepLineRef(), + $this->featureContext->getAdminUsername(), + $this->featureContext->getAdminPassword(), + $group + ); + $this->featureContext->setResponse($response); + $justCreatedGroup = $this->featureContext->getJsonDecodedResponse(); + $this->featureContext->pushToLastStatusCodesArrays(); + $this->featureContext->addGroupToCreatedGroupsList($group, true, true, $justCreatedGroup['id']); + } + + /** + * @When /^the administrator sends a group creation request for the following groups using the graph API$/ + * + * @return void + * @throws GuzzleException + */ + public function theAdministratorSendsAGroupCreationRequestForTheFollowingGroupsUsingTheGraphAPI(TableNode $table) { + $this->featureContext->verifyTableNodeColumns($table, ["group_display_name"], ['comment']); + $groups = $table->getHash(); + foreach ($groups as $group) { + $this->adminSendsGroupCreationRequestUsingTheGraphAPI($group["group_display_name"]); + } + } +} diff --git a/tests/acceptance/features/bootstrap/bootstrap.php b/tests/acceptance/features/bootstrap/bootstrap.php index ec7e80734a..27cd09a12c 100644 --- a/tests/acceptance/features/bootstrap/bootstrap.php +++ b/tests/acceptance/features/bootstrap/bootstrap.php @@ -24,7 +24,6 @@ $pathToCore = \getenv('PATH_TO_CORE'); if ($pathToCore === false) { $pathToCore = "../core"; } - require_once $pathToCore . '/tests/acceptance/features/bootstrap/bootstrap.php'; $classLoader = new \Composer\Autoload\ClassLoader(); @@ -33,5 +32,6 @@ $classLoader->addPsr4( $pathToCore . "/tests/acceptance/features/bootstrap", true ); +$classLoader->addPsr4("TestHelpers\\", __DIR__ . "/../../../TestHelpers", true); $classLoader->register();