* @copyright Copyright (c) 2018 Artur Neumann artur@jankaritech.com * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, * as published by the Free Software Foundation; * either version 3 of the License, or any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see * */ use Behat\Behat\Context\Context; use Behat\Behat\Hook\Scope\BeforeScenarioScope; use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Exception\GuzzleException; use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; use TestHelpers\KeycloakHelper; use TestHelpers\WebDavHelper; use TestHelpers\HttpRequestHelper; use TestHelpers\BehatHelper; require_once 'bootstrap.php'; /** * context containing search related API steps */ class SearchContext implements Context { private FeatureContext $featureContext; /** * Search until the results show up. * * After a (re)start the search service answers with 5xx until it is ready, * and indexing of new uploads is async, so poll for both. * * @param string $user * @param string $pattern * @param bool $isVault * @param string|null $limit * @param string|null $scopeType * @param string|null $scope * @param string|null $spaceName * @param TableNode|null $properties * * @return ResponseInterface */ private function searchWithRetry( string $user, string $pattern, bool $isVault = false, ?string $limit = null, ?string $scopeType = null, ?string $scope = null, ?string $spaceName = null, ?TableNode $properties = null, ): ResponseInterface { $search = fn (): ResponseInterface => $this->searchFiles( $user, $pattern, $isVault, $limit, $scopeType, $scope, $spaceName, $properties, ); for ($attempt = 0; $attempt < SERVICE_READY_RETRY_COUNT; $attempt++) { \sleep($attempt === 0 ? SEARCH_INDEXING_INITIAL_WAIT_SEC : STANDARD_REQUEST_POLLING_INTERVAL_SEC); $response = $search(); // 5xx means the service is still starting up if ($response->getStatusCode() < 500) { break; } } // a client error or an existing result cannot change by polling if ($response->getStatusCode() >= 400 || $this->searchResponseHasResults($response)) { return $response; } // indexing is async, so poll for the results for ($attempt = 0; $attempt < MAX_REQUEST_RETRY_COUNT; $attempt++) { \sleep(STANDARD_REQUEST_POLLING_INTERVAL_SEC); $response = $search(); if ($this->searchResponseHasResults($response)) { return $response; } } // let the assertion step report an empty result return $response; } /** * @param ResponseInterface $response * * @return bool */ private function searchResponseHasResults(ResponseInterface $response): bool { $parsed = HttpRequestHelper::parseResponseAsXml($response); return \is_array($parsed) && isset($parsed["value"]) && !empty($parsed["value"]); } /** * @param string $user * @param string $pattern * @param bool $isVault * @param string|null $limit * @param string|null $scopeType * @param string|null $scope * @param string|null $spaceName * @param TableNode|null $properties * * @return ResponseInterface * @throws GuzzleException|JsonException */ private function searchFiles( string $user, string $pattern, bool $isVault = false, ?string $limit = null, ?string $scopeType = null, ?string $scope = null, ?string $spaceName = null, ?TableNode $properties = null, ): ResponseInterface { $user = $this->featureContext->getActualUsername($user); $baseUrl = $this->featureContext->getBaseUrl(); $password = $this->featureContext->getPasswordForUser($user); if (str_contains($pattern, '$')) { $date = explode("$", $pattern); switch ($date[1]) { case "today": $pattern = $date[0] . date('Y-m-d', strtotime('today')); break; case "yesterday": $pattern = $date[0] . date('Y-m-d', strtotime('yesterday')); break; default: throw new Exception("cannot convert the date"); } } $body = "\n" . " \n" . " \n"; if ($scope !== null) { if ($scopeType === "space") { $spaceId = $this->featureContext->spacesContext->getSpaceIdByName($user, $scope, $isVault); $pattern .= " scope:$spaceId"; } else { $resourceID = $this->featureContext->spacesContext->getResourceId( $user, $spaceName ?? "Personal", $scope, $isVault, ); $pattern .= " scope:$resourceID"; } } // Search inside vault uses 'vault:true' query token if ($isVault) { $pattern .= " AND vault:true"; } $body .= "$pattern\n"; if ($limit !== null) { $body .= " $limit\n"; } $body .= " \n"; if ($properties !== null) { $propertiesRows = $properties->getRows(); $body .= " "; foreach ($propertiesRows as $property) { $body .= "<$property[0]/>"; } $body .= " "; } $body .= " "; $davPathVersionToUse = $this->featureContext->getDavPathVersion(); // $davPath will be one of the followings: // - webdav // - dav/files // - dav/spaces $davPath = WebDavHelper::getDavPath($davPathVersionToUse); $fullUrl = WebDavHelper::sanitizeUrl("$baseUrl/$davPath"); $headers = []; if (KeycloakHelper::isTestingWithKeycloak()) { $accessToken = $this->featureContext->getOcisUserToken($user)['token']['accessToken']; $headers['Authorization'] = 'Bearer ' . $accessToken; $user = null; $password = null; } return HttpRequestHelper::sendRequest( $fullUrl, 'REPORT', $user, $password, $headers, $body, // this method's own caller (searchWithRetry) already retries on 5xx, // so the generic k8s 5xx-retry here would just double up the wait retryOn5xxForK8s: false, ); } /** * @When user :user searches for :pattern using the WebDAV API * @When user :user searches for :pattern and limits the results to :limit items using the WebDAV API * @When user :user searches for :pattern using the WebDAV API requesting these properties: * @When user :user searches for :pattern and limits the results to :limit items using the WebDAV API requesting these properties: * * @param string $user * @param string $pattern * @param string|null $limit * @param TableNode|null $properties * * @return void * @throws Exception|GuzzleException */ public function userSearchesUsingWebDavAPI( string $user, string $pattern, ?string $limit = null, ?TableNode $properties = null, ): void { $response = $this->searchWithRetry($user, $pattern, false, $limit, null, null, null, $properties); $this->featureContext->setResponse($response); } /** * @When user :user searches for :pattern in vault using the WebDAV API * * @param string $user * @param string $pattern * * @return void */ public function userSearchesInVaultUsingWebDavAPI( string $user, string $pattern, ): void { $response = $this->searchWithRetry($user, $pattern, true); $this->featureContext->setResponse($response); } /** * @Then file/folder :path in the search result of user :user should contain these properties: * * @param string $path * @param string $user * @param TableNode $properties * * @return void * @throws Exception */ public function fileOrFolderInTheSearchResultShouldContainProperties( string $path, string $user, TableNode $properties, ): void { $user = $this->featureContext->getActualUsername($user); $this->featureContext->verifyTableNodeColumns($properties, ['name', 'value']); $properties = $properties->getHash(); $fileResult = $this->featureContext->findEntryFromSearchResponse( $path, ); Assert::assertNotFalse( $fileResult, "could not find file/folder '$path'", ); foreach ($properties as $property) { $property['value'] = $this->featureContext->substituteInLineCodes( $property['value'], $user, ); $fileResultProperty = $fileResult->xpath("d:propstat//" . $property['name']); if ($fileResultProperty) { Assert::assertMatchesRegularExpression( "/" . $property['value'] . "/", \trim((string)$fileResultProperty[0]), ); continue; } throw new Error("Could not find property '" . $property['name'] . "'"); } } /** * 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 $this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext'); } /** * @Then /^the search result should contain these (?:files|entries) with highlight on keyword "([^"]*)"/ * * @param TableNode $expectedFiles * @param string $expectedContent * * @return void * * @throws Exception */ public function theSearchResultShouldContainEntriesWithHighlight( TableNode $expectedFiles, string $expectedContent, ): void { $this->featureContext->verifyTableNodeColumnsCount($expectedFiles, 1); $elementRows = $expectedFiles->getRows(); $foundEntries = $this->featureContext->findEntryFromSearchResponse( null, true, ); foreach ($elementRows as $expectedFile) { $filename = $expectedFile[0]; $content = $foundEntries[$filename]; // Extract the content between the tags preg_match('/(.*?)<\/mark>/s', $content, $matches); $actualContent = $matches[1] ?? ''; // Remove any leading/trailing whitespace for comparison $actualContent = trim($actualContent); Assert::assertEquals( $expectedContent, $actualContent, "Expected text highlight to be '$expectedContent' but found '$actualContent'", ); } } /** * @When /^user "([^"]*)" searches for "([^"]*)" inside (folder|space) "([^"]*)"(| in vault)? using the WebDAV API$/ * * @param string $user * @param string $pattern * @param string $scopeType * @param string $scope * @param string $isVault * * @return void * @throws Exception|GuzzleException */ public function userSearchesInsideFolderOrSpaceUsingWebDavAPI( string $user, string $pattern, string $scopeType, string $scope, string $isVault, ): void { $isVault = trim($isVault) === 'in vault'; $response = $this->searchWithRetry($user, $pattern, $isVault, null, $scopeType, $scope); $this->featureContext->setResponse($response); } /** * @When /^user "([^"]*)" searches for "([^"]*)" inside (folder) "([^"]*)" in space "([^"]*)"(| in vault)? using the WebDAV API$/ * * @param string $user * @param string $pattern * @param string $scopeType * @param string $scope * @param string $spaceName * @param string $isVault * * @return void * @throws Exception|GuzzleException */ public function userSearchesInsideFolderInSpaceUsingWebDavAPI( string $user, string $pattern, string $scopeType, string $scope, string $spaceName, string $isVault, ): void { $isVault = trim($isVault) === 'in vault'; $response = $this->searchWithRetry($user, $pattern, $isVault, null, $scopeType, $scope, $spaceName); $this->featureContext->setResponse($response); } }