Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,325 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2021 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 <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Exception\GuzzleException;
use TestHelpers\HttpRequestHelper;
use TestHelpers\BehatHelper;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use splitbrain\PHPArchive\Tar;
use splitbrain\PHPArchive\Zip;
use splitbrain\PHPArchive\Archive;
require_once 'bootstrap.php';
/**
* Context for Archiver specific steps
*/
class ArchiverContext implements Context {
/**
* @var FeatureContext
*/
private FeatureContext $featureContext;
private SpacesContext $spacesContext;
/**
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
*
* @throws Exception
*/
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');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
}
/**
* @param string $query
*
* @return string
*/
public function getArchiverUrl(string $query): string {
return $this->featureContext->getBaseUrl() . '/archiver?' . $query;
}
/**
* @param string $type
*
* @return Archive
*/
public function getArchiveClass(string $type): Archive {
if ($type === 'zip') {
return new Zip();
} elseif ($type === 'tar') {
return new Tar();
} else {
throw new Exception('Unknown archive type: ' . $type);
}
}
/**
* @param string $dir
*
* @return void
*/
public function removeDir(string $dir): void {
$items = array_diff(scandir($dir), ['.', '..']);
foreach ($items as $item) {
$itemPath = $dir . DIRECTORY_SEPARATOR . $item;
if (\is_dir($itemPath)) {
$this->removeDir($itemPath);
} else {
\unlink($itemPath);
}
}
\rmdir($dir);
}
/**
* @param string $user
* @param string $resource
* @param string $addressType id|ids|path|paths
*
* @return string
*
* @throws Exception
*/
private function getArchiverQueryString(
string $user,
string $resource,
string $addressType
): string {
switch ($addressType) {
case 'id':
case 'ids':
return 'id=' . $this->featureContext->getFileIdForPath($user, $resource);
case 'remoteItemIds':
return 'id=' . $this->spacesContext->getSharesRemoteItemId($user, $resource);
case 'path':
case 'paths':
return 'path=' . $resource;
default:
throw new Exception(
'"' . $addressType .
'" is not a legal value for $addressType, must be id|ids|remoteItemIds|path|paths'
);
}
}
/**
* @When /^user "([^"]*)" downloads the (zip|tar) archive of "([^"]*)" using the resource (id|ids|path|paths) and setting these headers:$/
*
* @param string $user
* @param string $archiveType
* @param string $resource
* @param string $addressType id|path
* @param TableNode $headersTable
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function userDownloadsTheZipOrTarArchiveOfResourceUsingResourceIdOrPathAndSettingTheseHeaders(
string $user,
string $archiveType,
string $resource,
string $addressType,
TableNode $headersTable
): void {
$this->featureContext->verifyTableNodeColumns(
$headersTable,
['header', 'value']
);
$headers = [];
foreach ($headersTable as $row) {
$headers[$row['header']] = $row ['value'];
}
$this->featureContext->setResponse(
$this->downloadArchive($user, $resource, $addressType, $archiveType, null, $headers)
);
}
/**
* @When user :downloader downloads the archive of :item of user :owner using the resource :addressType
*
* @param string $downloader Who sends the request
* @param string $resource
* @param string $owner Who is the real owner of the file
* @param string $addressType id|path
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function userDownloadsTheArchiveOfItemOfUser(
string $downloader,
string $resource,
string $owner,
string $addressType
): void {
$this->featureContext->setResponse($this->downloadArchive($downloader, $resource, $addressType, null, $owner));
}
/**
* @param string $downloader
* @param string $resource
* @param string $addressType
* @param string|null $archiveType
* @param string|null $owner
* @param array|null $headers
*
* @return ResponseInterface
* @throws GuzzleException
* @throws JsonException
*/
public function downloadArchive(
string $downloader,
string $resource,
string $addressType,
?string $archiveType = null,
?string $owner = null,
?array $headers = null
): ResponseInterface {
$owner = $owner ?? $downloader;
$downloader = $this->featureContext->getActualUsername($downloader);
$queryString = $this->getArchiverQueryString($owner, $resource, $addressType);
if ($archiveType !== null) {
$queryString .= '&output-format=' . $archiveType;
}
return HttpRequestHelper::get(
$this->getArchiverUrl($queryString),
$this->featureContext->getStepLineRef(),
$downloader,
$this->featureContext->getPasswordForUser($downloader),
$headers
);
}
/**
* @When user :user downloads the archive of these items using the resource :addressType
*
* @param string $user
* @param string $addressType ids|paths
* @param TableNode $items
*
* @return void
*
* @throws GuzzleException|Exception
*/
public function userDownloadsTheArchiveOfTheseItems(
string $user,
string $addressType,
TableNode $items
): void {
$user = $this->featureContext->getActualUsername($user);
$queryString = [];
foreach ($items->getRows() as $item) {
$queryString[] = $this->getArchiverQueryString($user, $item[0], $addressType);
}
$queryString = \join('&', $queryString);
$this->featureContext->setResponse(
HttpRequestHelper::get(
$this->getArchiverUrl($queryString),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
)
);
}
/**
* @Then the downloaded :type archive should contain these files:
*
* @param string $type
* @param TableNode $expectedFiles
*
* @return void
*
* @throws Exception
*/
public function theDownloadedArchiveShouldContainTheseFiles(string $type, TableNode $expectedFiles): void {
$this->featureContext->verifyTableNodeColumns($expectedFiles, ['name', 'content']);
$contents = $this->featureContext->getResponse()->getBody()->getContents();
$tempFile = \tempnam(\sys_get_temp_dir(), 'OcAcceptanceTests_');
$tempExtractFolder = $tempFile;
\unlink($tempFile); // we only need the name
$tempFile = $tempFile . '.' . $type; // it needs the extension
\file_put_contents($tempFile, $contents);
// open the archive
$tar = $this->getArchiveClass($type);
$tar->open($tempFile);
$archiveData = $tar->contents();
// extract the archive
$tar->open($tempFile);
$tar->extract($tempExtractFolder);
$tar->close();
foreach ($expectedFiles->getHash() as $expectedItem) {
$expectedPath = trim($expectedItem['name'], "/");
$found = false;
foreach ($archiveData as $info) {
// get only the parent folder path for the given item
$actualPath = $info->getPath();
if ($expectedPath === $actualPath) {
if (!$info->getIsdir()) {
$fileFullPath = "$tempExtractFolder/$actualPath";
$fileMimeType = \mime_content_type($fileFullPath);
if ($fileMimeType === "text/plain") {
$fileContent = \file_get_contents($fileFullPath);
Assert::assertEquals(
$expectedItem['content'],
$fileContent,
__METHOD__ . " content of '" . $expectedPath . "' not as expected"
);
} else {
Assert::assertFileExists(
$fileFullPath,
__METHOD__ . " File '" . $expectedPath . "' is not in the downloaded archive."
);
}
}
$found = true;
break;
}
}
if (!$found) {
Assert::fail("Resource '" . $expectedPath . "' is not in the downloaded archive.");
}
}
\unlink($tempFile);
$this->removeDir($tempExtractFolder);
}
}
@@ -0,0 +1,155 @@
<?php declare(strict_types=1);
/**
* @author Niraj Acharya <niraj@jankaritech.com>
* @copyright Copyright (c) 2024 Niraj Acharya niraj@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 <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use TestHelpers\BehatHelper;
use TestHelpers\AuthAppHelper;
require_once 'bootstrap.php';
/**
* AuthApp context
*/
class AuthAppContext implements Context {
private FeatureContext $featureContext;
/**
* @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');
}
/**
* @When user :user creates app token with expiration time :expiration using the auth-app API
*
* @param string $user
* @param string $expiration
*
* @return void
*/
public function userCreatesAppTokenWithExpirationTimeUsingTheAuthAppApi(string $user, string $expiration): void {
$this->featureContext->setResponse(
AuthAppHelper::createAppAuthToken(
$this->featureContext->getBaseUrl(),
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
["expiry" => $expiration],
)
);
}
/**
* @Given user :user has created app token with expiration time :expiration using the auth-app API
*
* @param string $user
* @param string $expiration
*
* @return void
*/
public function userHasCreatedAppTokenWithExpirationTime(string $user, string $expiration): void {
$response = AuthAppHelper::createAppAuthToken(
$this->featureContext->getBaseUrl(),
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
["expiry" => $expiration]
);
$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
}
/**
* @When user :user lists all created tokens using the auth-app API
*
* @param string $user
*
* @return void
*/
public function userListsAllCreatedTokensUsingTheAuthAppApi(string $user): void {
$this->featureContext->setResponse(
AuthAppHelper::listAllAppAuthTokensForUser(
$this->featureContext->getBaseUrl(),
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
)
);
}
/**
* @Given the administrator has created app token for user :impersonatedUser with expiration time :expiration using the auth-app API
*
* @param string $impersonatedUser
* @param string $expiration
*
* @return void
*/
public function theAdministratorHasCreatedAppTokenWithExpirationTimeImpersonatingUserUsingTheAuthAppApi(
string $impersonatedUser,
string $expiration,
): void {
$response = AuthAppHelper::createAppAuthToken(
$this->featureContext->getBaseUrl(),
$this->featureContext->getAdminUsername(),
$this->featureContext->getAdminPassword(),
[
"expiry" => $expiration,
"userName" => $this->featureContext->getActualUsername($impersonatedUser)
],
);
$this->featureContext->theHTTPStatusCodeShouldBe(
200,
"Failed creating auth-app token\n"
. "HTTP status code 200 is not the expected value " . $response->getStatusCode(),
$response
);
}
/**
* @When the administrator creates app token for user :impersonatedUser with expiration time :expiration using the auth-app API
*
* @param string $impersonatedUser
* @param string $expiration
*
* @return void
*/
public function theAdministratorCreatesAppTokenForUserWithExpirationTimeViaAuthAppApi(
string $impersonatedUser,
string $expiration,
): void {
$this->featureContext->setResponse(
AuthAppHelper::createAppAuthToken(
$this->featureContext->getBaseUrl(),
$this->featureContext->getAdminUsername(),
$this->featureContext->getAdminPassword(),
[
"expiry" => $expiration,
"userName" => $this->featureContext->getActualUsername($impersonatedUser)
],
)
);
}
}
@@ -0,0 +1,741 @@
<?php declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@owncloud.com>
*
* @copyright Copyright (c) 2018, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use Behat\Behat\Context\Context;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\HttpRequestHelper;
use TestHelpers\BehatHelper;
use TestHelpers\TokenHelper;
use TestHelpers\WebDavHelper;
/**
* Authentication functions
*/
class AuthContext implements Context {
private FeatureContext $featureContext;
/**
* @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');
}
/**
* @param string $user
* @param string $password
*
* @return array
*/
public function createBasicAuthHeader(string $user, string $password): array {
$header = [];
$authString = \base64_encode("$user:$password");
$header["Authorization"] = "basic $authString";
return $header;
}
/**
* @param string $url
* @param string $method
* @param string|null $body
* @param array|null $headers
*
* @return ResponseInterface
*/
public function sendRequest(
string $url,
string $method,
?string $body = null,
?array $headers = []
): ResponseInterface {
// NOTE: preserving '/' for tests with special cases
// E.g: coreApiAuth/webDavSpecialURLs.feature
$url = \substr($url, 1);
$trimmedUrl = \ltrim($url, '/');
$slashCount = \strlen($url) - \strlen($trimmedUrl);
if (WebdavHelper::isDAVRequest($url)) {
$url = WebdavHelper::prefixRemotePhp($trimmedUrl);
}
$url = \str_repeat("/", $slashCount) . $url;
$fullUrl = $this->featureContext->getBaseUrl() . "/$url";
return HttpRequestHelper::sendRequest(
$fullUrl,
$this->featureContext->getStepLineRef(),
$method,
null,
null,
$headers,
$body,
);
}
/**
* @param string $user
* @param string $url
* @param string $method
* @param string|null $body
* @param array|null $headers
* @param string|null $property
*
* @return ResponseInterface
*/
public function requestUrlWithBasicAuth(
string $user,
string $url,
string $method,
?string $body = null,
?array $headers = null,
?string $property = null
): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$url = $this->featureContext->substituteInLineCodes(
$url,
$user
);
$authHeader = $this->createBasicAuthHeader($user, $this->featureContext->getPasswordForUser($user));
$headers = \array_merge($headers ?? [], $authHeader);
if ($property !== null) {
$body = $this->featureContext->getBodyForOCSRequest($method, $property);
}
return $this->sendRequest(
$url,
$method,
$body,
$headers
);
}
/**
* @When a user requests :url with :method and no authentication
*
* @param string $url
* @param string $method
*
* @return void
*/
public function userRequestsURLWithNoAuth(string $url, string $method): void {
$this->featureContext->setResponse($this->sendRequest($url, $method));
}
/**
* @When a user requests these endpoints with :method with body :body and no authentication about user :user
*
* @param string $method
* @param string $body
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws JsonException
*/
public function userRequestsEndpointsWithBodyAndNoAuthThenStatusCodeAboutUser(
string $method,
string $body,
string $ofUser,
TableNode $table
): void {
$ofUser = \strtolower($this->featureContext->getActualUsername($ofUser));
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->sendRequest($row['endpoint'], $method, $body);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When a user requests these endpoints with :method with no authentication about user :user
*
* @param string $method
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsEndpointsWithoutBodyAndNoAuthAboutUser(
string $method,
string $ofUser,
TableNode $table
): void {
$ofUser = \strtolower($this->featureContext->getActualUsername($ofUser));
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->sendRequest($row['endpoint'], $method);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When a user requests these endpoints with :method and no authentication
*
* @param string $method
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsEndpointsWithNoAuthentication(string $method, TableNode $table): void {
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
foreach ($table->getHash() as $row) {
$this->featureContext->setResponse(
$this->sendRequest(
$this->featureContext->substituteInLineCodes($row['endpoint']),
$method
)
);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When a user requests these URLs with :method and no authentication
*
* @param $method
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function aUserRequestsTheseUrlsWithAndNoAuthentication($method, TableNode $table): void {
$this->featureContext->verifyTableNodeColumns($table, ['endpoint'], ['service']);
foreach ($table->getHash() as $row) {
$this->featureContext->setResponse(
HttpRequestHelper::sendRequest(
$this->featureContext->substituteInLineCodes($row['endpoint']),
$this->featureContext->getStepLineRef(),
$method
)
);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When the user :user requests these endpoints with :method with basic auth
*
* @param string $user
* @param string $method
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsEndpointsWithBasicAuth(string $user, string $method, TableNode $table): void {
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
foreach ($table->getHash() as $row) {
$response = $this->requestUrlWithBasicAuth($user, $row['endpoint'], $method);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When /^user "([^"]*)" requests these endpoints with "([^"]*)" to (?:get|set) property "([^"]*)" about user "([^"]*)"$/
*
* @param string $user
* @param string $method
* @param string $property
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theUserRequestsTheseEndpointsToGetOrSetPropertyAboutUser(
string $user,
string $method,
string $property,
string $ofUser,
TableNode $table
): void {
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->requestUrlWithBasicAuth($user, $row['endpoint'], $method, null, null, $property);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When the administrator requests these endpoints with :method
*
* @param string $method
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theAdminRequestsTheseEndpointsWithMethod(string $method, TableNode $table): void {
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
foreach ($table->getHash() as $row) {
$response = $this->requestUrlWithBasicAuth(
$this->featureContext->getAdminUsername(),
$row['endpoint'],
$method
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :user requests :url with :method using basic auth
*
* @param string $user
* @param string $url
* @param string $method
*
* @return void
*/
public function userRequestsURLUsingBasicAuth(
string $user,
string $url,
string $method
): void {
$response = $this->requestUrlWithBasicAuth($user, $url, $method);
$this->featureContext->setResponse($response);
}
/**
* @When user :user requests :url with :method using basic auth and with headers
*
* @param string $user
* @param string $url
* @param string $method
* @param TableNode $headersTable
*
* @return void
* @throws Exception
*/
public function userRequestsURLWithUsingBasicAuthAndDepthHeader(
string $user,
string $url,
string $method,
TableNode $headersTable
): void {
$user = $this->featureContext->getActualUsername($user);
$url = $this->featureContext->substituteInLineCodes(
$url,
$user
);
$this->featureContext->verifyTableNodeColumns(
$headersTable,
['header', 'value']
);
$headers = [];
foreach ($headersTable as $row) {
$headers[$row['header']] = $row ['value'];
}
$this->featureContext->setResponse(
$this->requestUrlWithBasicAuth(
$user,
$url,
$method,
null,
$headers
)
);
}
/**
* @When user :user requests these endpoints with :method using password :password
*
* @param string $user
* @param string $method
* @param string $password
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsTheseEndpointsWithPassword(
string $user,
string $method,
string $password,
TableNode $table
): void {
$user = $this->featureContext->getActualUsername($user);
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
$authHeader = $this->createBasicAuthHeader($user, $this->featureContext->getActualPassword($password));
foreach ($table->getHash() as $row) {
$response = $this->sendRequest($row['endpoint'], $method, null, $authHeader);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :user requests these endpoints with :method using password :password about user :ofUser
*
* @param string $user
* @param string $method
* @param string $password
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsTheseEndpointsUsingPasswordAboutUser(
string $user,
string $method,
string $password,
string $ofUser,
TableNode $table
): void {
$user = $this->featureContext->getActualUsername($user);
$ofUser = $this->featureContext->getActualUsername($ofUser);
$this->featureContext->verifyTableNodeColumns($table, ['endpoint'], ['destination']);
$headers = $this->createBasicAuthHeader($user, $this->featureContext->getActualPassword($password));
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
if (isset($row['destination'])) {
$destination = $this->featureContext->substituteInLineCodes(
$row['destination'],
$ofUser
);
$headers['Destination'] = $this->featureContext->getBaseUrl()
. "/" . WebdavHelper::prefixRemotePhp(\ltrim($destination, "/"));
}
$response = $this->sendRequest(
$row['endpoint'],
$method,
null,
$headers
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :user requests these endpoints with :method including body :body using password :password about user :ofUser
*
* @param string $user
* @param string $method
* @param string $body
* @param string $password
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsTheseEndpointsWithBodyUsingPasswordAboutUser(
string $user,
string $method,
string $body,
string $password,
string $ofUser,
TableNode $table
): void {
$user = $this->featureContext->getActualUsername($user);
$ofUser = $this->featureContext->getActualUsername($ofUser);
$this->featureContext->verifyTableNodeColumns($table, ['endpoint'], ['destination']);
$headers = $this->createBasicAuthHeader($user, $this->featureContext->getActualPassword($password));
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
if (isset($row['destination'])) {
$destination = $this->featureContext->substituteInLineCodes(
$row['destination'],
$ofUser
);
$headers['Destination'] = $this->featureContext->getBaseUrl()
. "/" . WebdavHelper::prefixRemotePhp(\ltrim($destination, "/"));
}
$response = $this->sendRequest(
$row['endpoint'],
$method,
$body,
$headers
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :user requests these endpoints with :method including body :body about user :ofUser
*
* @param string $user
* @param string $method
* @param string $body
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsTheseEndpointsIncludingBodyAboutUser(
string $user,
string $method,
string $body,
string $ofUser,
TableNode $table
): void {
$user = $this->featureContext->getActualUsername($user);
$ofUser = $this->featureContext->getActualUsername($ofUser);
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
$headers = [];
if ($method === 'MOVE' || $method === 'COPY') {
$headers['Destination'] = '/path/to/destination';
}
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->requestUrlWithBasicAuth(
$user,
$row['endpoint'],
$method,
$body,
$headers
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :asUser requests these endpoints with :method using the password of user :ofUser
*
* @param string $asUser
* @param string $method
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsTheseEndpointsWithoutBodyUsingThePasswordOfUser(
string $asUser,
string $method,
string $ofUser,
TableNode $table
): void {
$asUser = $this->featureContext->getActualUsername($asUser);
$ofUser = $this->featureContext->getActualUsername($ofUser);
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
// do request as $asUser using password of $ofUser
$authHeader = $this->createBasicAuthHeader($asUser, $this->featureContext->getPasswordForUser($ofUser));
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->sendRequest(
$row['endpoint'],
$method,
null,
$authHeader
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :asUser requests these endpoints with :method including body :body using the password of user :user
*
* @param string $asUser
* @param string $method
* @param string|null $body
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws JsonException
*/
public function userRequestsTheseEndpointsIncludingBodyUsingPasswordOfUser(
string $asUser,
string $method,
?string $body,
string $ofUser,
TableNode $table
): void {
$asUser = $this->featureContext->getActualUsername($asUser);
$ofUser = $this->featureContext->getActualUsername($ofUser);
$this->featureContext->verifyTableNodeColumns($table, ['endpoint']);
// do request as $asUser using password of $ofUser
$authHeader = $this->createBasicAuthHeader($asUser, $this->featureContext->getPasswordForUser($ofUser));
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->sendRequest(
$row['endpoint'],
$method,
$body,
$authHeader
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :user requests these endpoints with :method about user :ofUser
*
* @param string $user
* @param string $method
* @param string $ofUser
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRequestsTheseEndpointsAboutUser(
string $user,
string $method,
string $ofUser,
TableNode $table
): void {
$headers = [];
if ($method === 'MOVE' || $method === 'COPY') {
$baseUrl = $this->featureContext->getBaseUrl();
$suffix = $user;
if ($this->featureContext->getDavPathVersion() === WebDavHelper::DAV_VERSION_SPACES) {
$suffix = $this->featureContext->getPersonalSpaceIdForUser($user);
}
$davPath = WebDavHelper::getDavPath($this->featureContext->getDavPathVersion(), $suffix);
$headers['Destination'] = "$baseUrl/$davPath/moved";
}
foreach ($table->getHash() as $row) {
$row['endpoint'] = $this->featureContext->substituteInLineCodes(
$row['endpoint'],
$ofUser
);
$response = $this->requestUrlWithBasicAuth(
$user,
$row['endpoint'],
$method,
null,
$headers
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
}
}
/**
* @When user :user requests :endpoint with :method without retrying
*
* @param string $user
* @param string $endpoint
* @param string $method
*
* @return void
*/
public function userRequestsURLWithoutRetry(
string $user,
string $endpoint,
string $method
): void {
$username = $this->featureContext->getActualUsername($user);
$endpoint = $this->featureContext->substituteInLineCodes(
$endpoint,
$username
);
$endpoint = \ltrim($endpoint, '/');
if (WebdavHelper::isDAVRequest($endpoint)) {
$endpoint = WebdavHelper::prefixRemotePhp($endpoint);
}
$fullUrl = $this->featureContext->getBaseUrl() . "/$endpoint";
$response = HttpRequestHelper::sendRequestOnce(
$fullUrl,
$this->featureContext->getStepLineRef(),
$method,
$username,
$this->featureContext->getPasswordForUser($user)
);
$this->featureContext->setResponse($response);
}
/**
* @When user :user should not be able to log in with wrong password :password
*
* @param string $user
* @param string $password
*
* @return void
*/
public function userShouldNotBeAbleToLogInWithWrongPassword(
string $user,
string $password
): void {
TokenHelper::clearUserTokens($user, $this->featureContext->getBaseUrl());
$response = TokenHelper::makeLoginRequest(
$user,
$password,
$this->featureContext->getBaseUrl(),
new \GuzzleHttp\Cookie\CookieJar()
);
// why is not 401 returned?
$this->featureContext->theHTTPStatusCodeShouldBe(204, 'should not be able to log in', $response);
}
}
@@ -0,0 +1,322 @@
<?php declare(strict_types=1);
/**
* @author Joas Schilling <coding@schilljs.com>
* @author Sergio Bertolin <sbertolin@owncloud.com>
* @author Phillip Davis <phil@jankaritech.com>
* @copyright Copyright (c) 2018, ownCloud GmbH
*
* 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 <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\PyStringNode;
use Psr\Http\Message\ResponseInterface;
use PHPUnit\Framework\Assert;
use TestHelpers\OcsApiHelper;
use TestHelpers\BehatHelper;
use TestHelpers\HttpRequestHelper;
require_once 'bootstrap.php';
/**
* Capabilities context.
*/
class CapabilitiesContext implements Context {
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
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
}
/**
*
* @param string $username
* @param boolean $formatJson // if true then formats the response in json
*
* @return ResponseInterface
* @throws GuzzleException
* @throws JsonException
*/
public function userGetsCapabilities(string $username, ?bool $formatJson = false): ResponseInterface {
$user = $this->featureContext->getActualUsername($username);
$password = $this->featureContext->getPasswordForUser($user);
return OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$user,
$password,
'GET',
'/cloud/capabilities' . ($formatJson ? '?format=json' : ''),
$this->featureContext->getStepLineRef(),
[],
$this->featureContext->getOcsApiVersion()
);
}
/**
* @return string
* @throws Exception|GuzzleException
*/
public function getAdminUsernameForCapabilitiesCheck(): string {
if (\TestHelpers\OcHelper::isTestingOnReva()) {
// When testing on reva we don't have a user called "admin" to use
// to access the capabilities. So create an ordinary user on-the-fly
// with a default password. That user should be able to get a
// capabilities response that the test can process.
$adminUsername = "PseudoAdminForRevaTest";
$createdUsers = $this->featureContext->getCreatedUsers();
if (!\array_key_exists($adminUsername, $createdUsers)) {
$this->featureContext->userHasBeenCreated(["userName" => $adminUsername]);
}
} else {
$adminUsername = $this->featureContext->getAdminUsername();
}
return $adminUsername;
}
/**
* @param SimpleXMLElement $xml of the capabilities
* @param string $capabilitiesApp the "app" name in the capabilities response
* @param string $capabilitiesPath the path to the element
*
* @return string
*/
public function getParameterValueFromXml(
SimpleXMLElement $xml,
string $capabilitiesApp,
string $capabilitiesPath
): string {
$path_to_element = \explode('@@@', $capabilitiesPath);
$answeredValue = $xml->{$capabilitiesApp};
foreach ($path_to_element as $element) {
$nameIndexParts = \explode('[', $element);
if (isset($nameIndexParts[1])) {
// This part of the path should be something like "some_element[1]"
// Separately extract the name and the index
$name = $nameIndexParts[0];
$index = (int) \explode(']', $nameIndexParts[1])[0];
// and use those to construct the reference into the next XML level
$answeredValue = $answeredValue->{$name}[$index];
} else {
if ($element !== "") {
$answeredValue = $answeredValue->{$element};
}
}
}
return (string) $answeredValue;
}
/**
* @When user :username retrieves the capabilities using the capabilities API
*
* @param string $username
*
* @return void
* @throws GuzzleException
* @throws JsonException
*/
public function userRetrievesCapabilities(string $username): void {
$user = $this->featureContext->getActualUsername($username);
$this->featureContext->setResponse($this->userGetsCapabilities($user, true));
}
/**
* @When the administrator retrieves the capabilities using the capabilities API
*
* @return void
*/
public function theAdministratorGetsCapabilities(): void {
$user = $this->getAdminUsernameForCapabilitiesCheck();
$this->featureContext->setResponse($this->userGetsCapabilities($user, true));
}
/**
* @Then the major-minor-micro version data in the response should match the version string
*
* @return void
* @throws Exception
*/
public function checkVersionMajorMinorMicroResponse(): void {
$jsonResponse = $this->featureContext->getJsonDecodedResponseBodyContent();
$versionData = $jsonResponse->ocs->data->version;
$versionString = (string) $versionData->string;
// We expect that versionString will be in a format like "10.9.2 beta" or "10.9.2-alpha" or "10.9.2"
$result = \preg_match('/^[0-9]+\.[0-9]+\.[0-9]+/', $versionString, $matches);
Assert::assertSame(
1,
$result,
__METHOD__ . " version string '$versionString' does not start with a semver version"
);
// semVerParts should have an array with the 3 semver components of the version, e.g. "1", "9" and "2".
$semVerParts = \explode('.', $matches[0]);
$expectedMajor = $semVerParts[0];
$expectedMinor = $semVerParts[1];
$expectedMicro = $semVerParts[2];
$actualMajor = (string) $versionData->major;
$actualMinor = (string) $versionData->minor;
$actualMicro = (string) $versionData->micro;
Assert::assertSame(
$expectedMajor,
$actualMajor,
__METHOD__ . "'major' data item does not match with major version in string '$versionString'"
);
Assert::assertSame(
$expectedMinor,
$actualMinor,
__METHOD__ . "'minor' data item does not match with minor version in string '$versionString'"
);
Assert::assertSame(
$expectedMicro,
$actualMicro,
__METHOD__ . "'micro' data item does not match with micro (patch) version in string '$versionString'"
);
}
/**
* @Then the status.php response should include
*
* @param PyStringNode $jsonExpected
*
* @return void
* @throws Exception
*/
public function statusPhpRespondedShouldMatch(PyStringNode $jsonExpected): void {
$jsonExpectedDecoded = \json_decode($jsonExpected->getRaw(), true);
$jsonRespondedDecoded = $this->featureContext->getJsonDecodedResponse();
$response = $this->userGetsCapabilities($this->getAdminUsernameForCapabilitiesCheck());
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
$responseXmlObject = HttpRequestHelper::getResponseXml($response, __METHOD__)->data->capabilities;
$edition = $this->getParameterValueFromXml(
$responseXmlObject,
'core',
'status@@@edition'
);
$product = $this->getParameterValueFromXml(
$responseXmlObject,
'core',
'status@@@product'
);
if (!\strlen($product)) {
Assert::fail(
"Cannot get product from core capabilities"
);
}
$productName = $this->getParameterValueFromXml(
$responseXmlObject,
'core',
'status@@@productname'
);
if (!\strlen($productName)) {
Assert::fail(
"Cannot get productname from core capabilities"
);
}
$jsonExpectedDecoded['edition'] = $edition;
$jsonExpectedDecoded['product'] = $product;
$jsonExpectedDecoded['productname'] = $productName;
// We are on КуСфера or reva or some other implementation. We cannot do "occ status".
// So get the expected version values by looking in the capabilities response.
$version = $this->getParameterValueFromXml(
$responseXmlObject,
'core',
'status@@@version'
);
if (!\strlen($version)) {
Assert::fail(
"Cannot get version from core capabilities"
);
}
$versionString = $this->getParameterValueFromXml(
$responseXmlObject,
'core',
'status@@@versionstring'
);
if (!\strlen($versionString)) {
Assert::fail(
"Cannot get versionstring from core capabilities"
);
}
$jsonExpectedDecoded['version'] = $version;
$jsonExpectedDecoded['versionstring'] = $versionString;
$errorMessage = "";
$errorFound = false;
foreach ($jsonExpectedDecoded as $key => $expectedValue) {
if (\array_key_exists($key, $jsonRespondedDecoded)) {
$actualValue = $jsonRespondedDecoded[$key];
if ($actualValue !== $expectedValue) {
$errorMessage .= "$key expected value was $expectedValue but actual value was $actualValue\n";
$errorFound = true;
}
} else {
$errorMessage .= "$key was not found in the status response\n";
$errorFound = true;
}
}
Assert::assertFalse($errorFound, $errorMessage);
// We have checked that the status.php response has data that matches up with
// data found in the capabilities response and/or the "occ status" command output.
// But the output might be reported wrongly in all of these in the same way.
// So check that the values also seem "reasonable".
$version = $jsonExpectedDecoded['version'];
$versionString = $jsonExpectedDecoded['versionstring'];
Assert::assertMatchesRegularExpression(
"/^\d+\.\d+\.\d+\.\d+$/",
$version,
"version should be in a form like 10.9.8.1 but is $version"
);
if (\preg_match("/^(\d+\.\d+\.\d+)\.\d+(-[0-9A-Za-z-]+)?(\+[0-9A-Za-z-]+)?$/", $version, $matches)) {
// We should have matched something like 10.9.8 - the first 3 numbers in the version.
// Ignore pre-releases and meta information
Assert::assertArrayHasKey(
1,
$matches,
"version $version could not match the pattern Major.Minor.Patch"
);
$majorMinorPatchVersion = $matches[1];
} else {
Assert::fail("version '$version' does not start in a form like 10.9.8");
}
Assert::assertStringStartsWith(
$majorMinorPatchVersion,
$versionString,
"version string should start with $majorMinorPatchVersion but is $versionString"
);
}
}
@@ -0,0 +1,517 @@
<?php declare(strict_types=1);
/**
* @author Roeland Jago Douma <rullzer@owncloud.com>
*
* @copyright Copyright (c) 2018, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
use TestHelpers\UploadHelper;
require_once 'bootstrap.php';
/**
* Checksum functions
*/
class ChecksumContext implements Context {
private FeatureContext $featureContext;
/**
* @param string $user
* @param string $source
* @param string $destination
* @param string $checksum
*
* @return ResponseInterface
*/
public function uploadFileToWithChecksumUsingTheAPI(
string $user,
string $source,
string $destination,
string $checksum
): ResponseInterface {
$file = \file_get_contents(
UploadHelper::getAcceptanceTestsDir() . $source
);
return $this->featureContext->makeDavRequest(
$user,
'PUT',
$destination,
['OC-Checksum' => $checksum],
$file
);
}
/**
* @When user :user uploads file :source to :destination with checksum :checksum using the WebDAV API
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $checksum
*
* @return void
*/
public function userUploadsFileToWithChecksumUsingTheAPI(
string $user,
string $source,
string $destination,
string $checksum
): void {
$this->featureContext->setResponse(
$this->uploadFileToWithChecksumUsingTheAPI($user, $source, $destination, $checksum)
);
}
/**
* @Given user :user has uploaded file :source to :destination with checksum :checksum
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $checksum
*
* @return void
*/
public function userHasUploadedFileToWithChecksumUsingTheAPI(
string $user,
string $source,
string $destination,
string $checksum
): void {
$user = $this->featureContext->getActualUsername($user);
$response = $this->uploadFileToWithChecksumUsingTheAPI(
$user,
$source,
$destination,
$checksum
);
$this->featureContext->theHTTPStatusCodeShouldBe([201,204], '', $response);
}
/**
* @param string $user
* @param string $content
* @param string $checksum
* @param string $destination
*
* @return ResponseInterface
*/
public function uploadFileWithContentAndChecksumToUsingTheAPI(
string $user,
string $content,
string $checksum,
string $destination
): ResponseInterface {
return $this->featureContext->makeDavRequest(
$user,
'PUT',
$destination,
['OC-Checksum' => $checksum],
$content
);
}
/**
* @Given user :user has uploaded file with content :content and checksum :checksum to :destination
*
* @param string $user
* @param string $content
* @param string $checksum
* @param string $destination
*
* @return void
*/
public function userHasUploadedFileWithContentAndChecksumToUsingTheAPI(
string $user,
string $content,
string $checksum,
string $destination
): void {
$user = $this->featureContext->getActualUsername($user);
$response = $this->uploadFileWithContentAndChecksumToUsingTheAPI($user, $content, $checksum, $destination);
$this->featureContext->theHTTPStatusCodeShouldBe(201, '', $response);
}
/**
* @When user :user requests the checksum of :path via propfind
*
* @param string $user
* @param string $path
*
* @return void
*/
public function userRequestsTheChecksumOfViaPropfind(string $user, string $path): void {
$this->featureContext->setResponse($this->propfindResourceChecksum($user, $path));
}
/**
* @param string $user
* @param string $path
* @param string|null $spaceId
*
* @return ResponseInterface
*/
public function propfindResourceChecksum(string $user, string $path, ?string $spaceId = null): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$body = '<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop>
<oc:checksums />
</d:prop>
</d:propfind>';
$password = $this->featureContext->getPasswordForUser($user);
return WebDavHelper::makeDavRequest(
$this->featureContext->getBaseUrl(),
$user,
$password,
'PROPFIND',
$path,
null,
$spaceId,
$this->featureContext->getStepLineRef(),
$body,
$this->featureContext->getDavPathVersion()
);
}
/**
* @Then the webdav checksum should match :expectedChecksum
*
* @param string $expectedChecksum
*
* @return void
* @throws Exception
*/
public function theWebdavChecksumShouldMatch(string $expectedChecksum): void {
$bodyContents = $this->featureContext->getResponse()->getBody()->getContents();
$this->validateChecksum($bodyContents, $expectedChecksum);
}
/**
* @Then as user :user the webdav checksum of :path via propfind should match :expectedChecksum
*
* @param string $user
* @param string $path
* @param string $expectedChecksum
*
* @return void
* @throws Exception
*/
public function asUserTheWebdavChecksumOfPathViaPropfindShouldMatch(
string $user,
string $path,
string $expectedChecksum
): void {
$user = $this->featureContext->getActualUsername($user);
$resource = $this->propfindResourceChecksum($user, $path);
$bodyContents = $resource->getBody()->getContents();
$this->validateChecksum($bodyContents, $expectedChecksum);
}
/**
* @param string $bodyContents
* @param string $expectedChecksum
*
* @return void
*/
public function validateChecksum(string $bodyContents, string $expectedChecksum): void {
$service = new Sabre\Xml\Service();
$parsed = $service->parse($bodyContents);
/*
* Fetch the checksum array
* The checksums are way down in the array:
* $checksums = $parsed[0]['value'][1]['value'][0]['value'][0];
* And inside is the actual checksum string:
* $checksums['value'][0]['value']
* The Asserts below check the existence of the expected key at every level
* of the nested array. This helps to see what happened if a test fails
* because the response structure is not as expected.
*/
Assert::assertIsArray(
$parsed,
__METHOD__
. " could not parse response as XML. Expected parsed XML to be an array but found " . $bodyContents
);
Assert::assertArrayHasKey(
0,
$parsed,
__METHOD__ . " parsed XML does not have key 0"
);
$parsed0 = $parsed[0];
Assert::assertArrayHasKey(
'value',
$parsed0,
__METHOD__ . " parsed XML parsed0 does not have key value"
);
$parsed0Value = $parsed0['value'];
Assert::assertArrayHasKey(
1,
$parsed0Value,
__METHOD__ . " parsed XML parsed0Value does not have key 1"
);
$parsed0Value1 = $parsed0Value[1];
Assert::assertArrayHasKey(
'value',
$parsed0Value1,
__METHOD__ . " parsed XML parsed0Value1 does not have key value after key 1"
);
$parsed0Value1Value = $parsed0Value1['value'];
Assert::assertArrayHasKey(
0,
$parsed0Value1Value,
__METHOD__ . " parsed XML parsed0Value1Value does not have key 0"
);
$parsed0Value1Value0 = $parsed0Value1Value[0];
Assert::assertArrayHasKey(
'value',
$parsed0Value1Value0,
__METHOD__ . " parsed XML parsed0Value1Value0 does not have key value"
);
$parsed0Value1Value0Value = $parsed0Value1Value0['value'];
Assert::assertArrayHasKey(
0,
$parsed0Value1Value0Value,
__METHOD__ . " parsed XML parsed0Value1Value0Value does not have key 0"
);
$checksums = $parsed0Value1Value0Value[0];
Assert::assertArrayHasKey(
'value',
$checksums,
__METHOD__ . " parsed XML checksums does not have key value"
);
$checksumsValue = $checksums['value'];
Assert::assertArrayHasKey(
0,
$checksumsValue,
__METHOD__ . " parsed XML checksumsValue does not have key 0"
);
$checksumsValue0 = $checksumsValue[0];
Assert::assertArrayHasKey(
'value',
$checksumsValue0,
__METHOD__ . " parsed XML checksumsValue0 does not have key value"
);
$actualChecksum = $checksumsValue0['value'];
Assert::assertEquals(
$expectedChecksum,
$actualChecksum,
"Expected: webDav checksum should be $expectedChecksum but got $actualChecksum"
);
}
/**
* @Then the header checksum should match :expectedChecksum
*
* @param string $expectedChecksum
*
* @return void
* @throws Exception
*/
public function theHeaderChecksumShouldMatch(string $expectedChecksum): void {
$headerChecksums
= $this->featureContext->getResponse()->getHeader('OC-Checksum');
Assert::assertIsArray(
$headerChecksums,
__METHOD__ . " getHeader('OC-Checksum') did not return an array"
);
Assert::assertNotEmpty(
$headerChecksums,
__METHOD__ . " getHeader('OC-Checksum') returned an empty array. No checksum header was found."
);
$checksumCount = \count($headerChecksums);
Assert::assertTrue(
$checksumCount === 1,
__METHOD__ . " Expected 1 checksum in the header but found $checksumCount checksums"
);
$headerChecksum
= $headerChecksums[0];
Assert::assertEquals(
$expectedChecksum,
$headerChecksum,
"Expected: header checksum should match $expectedChecksum but got $headerChecksum"
);
}
/**
* @Then the header checksum when user :arg1 downloads file :arg2 using the WebDAV API should match :arg3
*
* @param string $user
* @param string $fileName
* @param string $expectedChecksum
*
* @return void
* @throws Exception
*/
public function theHeaderChecksumWhenUserDownloadsFileUsingTheWebdavApiShouldMatch(
string $user,
string $fileName,
string $expectedChecksum
): void {
$response = $this->featureContext->downloadFileAsUserUsingPassword($user, $fileName);
$headerChecksums = $response->getHeader('OC-Checksum');
Assert::assertIsArray(
$headerChecksums,
__METHOD__ . " getHeader('OC-Checksum') did not return an array"
);
Assert::assertNotEmpty(
$headerChecksums,
__METHOD__ . " getHeader('OC-Checksum') returned an empty array. No checksum header was found."
);
$checksumCount = \count($headerChecksums);
Assert::assertTrue(
$checksumCount === 1,
__METHOD__ . " Expected 1 checksum in the header but found $checksumCount checksums"
);
$headerChecksum
= $headerChecksums[0];
Assert::assertEquals(
$expectedChecksum,
$headerChecksum,
"Expected: header checksum should match $expectedChecksum but got $headerChecksum"
);
}
/**
* @param string $user
* @param int $num
* @param int $total
* @param string $data
* @param string $destination
* @param string $expectedChecksum
*
* @return ResponseInterface
*/
public function uploadChunkFileOfWithToWithChecksum(
string $user,
int $num,
int $total,
string $data,
string $destination,
string $expectedChecksum
): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$num -= 1;
$file = "$destination-chunking-42-$total-$num";
return $this->featureContext->makeDavRequest(
$user,
'PUT',
$file,
['OC-Checksum' => $expectedChecksum, 'OC-Chunked' => '1'],
$data
);
}
/**
* @When user :user uploads chunk file :num of :total with :data to :destination with checksum :expectedChecksum using the WebDAV API
*
* @param string $user
* @param int $num
* @param int $total
* @param string $data
* @param string $destination
* @param string $expectedChecksum
*
* @return void
*/
public function userUploadsChunkFileOfWithToWithChecksum(
string $user,
int $num,
int $total,
string $data,
string $destination,
string $expectedChecksum
): void {
$response = $this->uploadChunkFileOfWithToWithChecksum(
$user,
$num,
$total,
$data,
$destination,
$expectedChecksum
);
$this->featureContext->setResponse($response);
}
/**
* @Given user :user has uploaded chunk file :num of :total with :data to :destination with checksum :expectedChecksum
*
* @param string $user
* @param int $num
* @param int $total
* @param string $data
* @param string $destination
* @param string $expectedChecksum
*
* @return void
*/
public function userHasUploadedChunkFileOfWithToWithChecksum(
string $user,
int $num,
int $total,
string $data,
string $destination,
string $expectedChecksum
): void {
$response = $this->uploadChunkFileOfWithToWithChecksum(
$user,
$num,
$total,
$data,
$destination,
$expectedChecksum
);
$this->featureContext->theHTTPStatusCodeShouldBe(
[201, 206],
'',
$response
);
}
/**
* 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');
}
}
@@ -0,0 +1,915 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2024 Sajan Gurung sajan@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 <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
use TestHelpers\CliHelper;
use TestHelpers\OcConfigHelper;
use TestHelpers\BehatHelper;
use Psr\Http\Message\ResponseInterface;
/**
* CLI context
*/
class CliContext implements Context {
private FeatureContext $featureContext;
private SpacesContext $spacesContext;
/**
* qsfera users storage path
*
* @return string
*/
public static function getUsersStoragePath(): string {
$path = getenv('OC_STORAGE_PATH') ?: '/var/lib/qsfera/storage/users';
// need for CI
$home = getenv('HOME');
$path = preg_replace('#^~/#', $home . '/', $path);
$path = str_replace('$HOME', $home, $path);
return rtrim($path, '/') . '/users';
}
/**
* qsfera project spaces storage path
*
* @return string
*/
public static function getProjectsStoragePath(): string {
$path = getenv('OC_STORAGE_PATH') ?: '/var/lib/qsfera/storage/users';
return $path . '/projects';
}
/**
* @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');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
}
/**
* expects a file to exist at the given path
*
* @param string $path
* @param string|null $sizeGb
* @param int $maxSeconds
*
* @return void
*/
private function waitForPath(string $path, ?string $sizeGb = null, int $maxSeconds = 10): void {
$escapedPath = escapeshellarg($path);
for ($i = 0; $i < $maxSeconds * 5; $i++) {
if ($sizeGb) {
if (!preg_match('/^(\d+)gb$/i', $sizeGb, $matches)) {
throw new \InvalidArgumentException("Invalid size format: $sizeGb. Use formats like 1gb, 5gb.");
}
$targetBytes = (int)$matches[1] * 1024 * 1024 * 1024;
$body = [
"command" => "[ -f $escapedPath ] && stat -c%s $escapedPath || echo 0",
"raw" => true
];
$data = json_decode((string)CliHelper::runCommand($body)->getBody(), true);
if (isset($data['message']) && (int)trim($data['message']) >= $targetBytes) {
return;
}
} else {
$body = [
"command" => "ls $escapedPath >/dev/null 2>&1 && echo exists || echo not_exists",
"raw" => true
];
$response = CliHelper::runCommand($body);
$data = json_decode((string)$response->getBody(), true);
if (isset($data['message']) && trim($data['message']) === 'exists') {
return;
}
}
usleep(200000);
}
throw new \Exception("Timeout waiting for: $path");
}
/**
* @Given the administrator has stopped the server
*
* @return void
*/
public function theAdministratorHasStoppedTheServer(): void {
$response = OcConfigHelper::stopQsfera();
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
}
/**
* @Given /^the administrator (?:starts|has started) the server$/
*
* @return void
*/
public function theAdministratorHasStartedTheServer(): void {
$response = OcConfigHelper::startQsfera();
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
}
/**
* @When /^the administrator resets the password of (non-existing|existing) user "([^"]*)" to "([^"]*)" using the CLI$/
*
* @param string $status
* @param string $user
* @param string $password
*
* @return void
*/
public function theAdministratorResetsThePasswordOfUserUsingTheCLI(
string $status,
string $user,
string $password
): void {
$command = "idm resetpassword -u $user";
$body = [
"command" => $command,
"inputs" => [$password, $password]
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
if ($status === "non-existing") {
return;
}
$this->featureContext->updateUserPassword($user, $password);
}
/**
* @When the administrator deletes the empty trashbin folders using the CLI
*
* @return void
*/
public function theAdministratorDeletesEmptyTrashbinFoldersUsingTheCli(): void {
$path = $this->featureContext->getStorageUsersRoot();
$command = "trash purge-empty-dirs -p $path --dry-run=false";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator checks the backup consistency using the CLI
*
* @return void
*/
public function theAdministratorChecksTheBackupConsistencyUsingTheCli(): void {
$path = $this->featureContext->getStorageUsersRoot();
$command = "backup consistency -p $path";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator creates app token for user :user with expiration time :expirationTime using the auth-app CLI
*
* @param string $user
* @param string $expirationTime
*
* @return void
*/
public function theAdministratorCreatesAppTokenForUserWithExpirationTimeUsingTheAuthAppCLI(
string $user,
string $expirationTime
): void {
$user = $this->featureContext->getActualUserName($user);
$command = "auth-app create --user-name=$user --expiration=$expirationTime";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @Given user :user has created app token with expiration time :expirationTime using the auth-app CLI
*
* @param string $user
* @param string $expirationTime
*
* @return void
*/
public function userHasCreatedAppTokenWithExpirationTimeUsingTheAuthAppCLI(
string $user,
string $expirationTime
): void {
$user = $this->featureContext->getActualUserName($user);
$command = "auth-app create --user-name=$user --expiration=$expirationTime";
$body = [
"command" => $command
];
$response = CliHelper::runCommand($body);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
$jsonResponse = $this->featureContext->getJsonDecodedResponse($response);
Assert::assertSame("OK", $jsonResponse["status"]);
Assert::assertSame(
0,
$jsonResponse["exitCode"],
"Expected exit code to be 0, but got " . $jsonResponse["exitCode"]
);
}
/**
* @When the administrator removes all the file versions using the CLI
*
* @return void
*/
public function theAdministratorRemovesAllVersionsOfResources() {
$path = $this->featureContext->getStorageUsersRoot();
$command = "revisions purge -p $path --dry-run=false";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator removes the versions of file :file of user :user from space :space using the CLI
*
* @param string $file
* @param string $user
* @param string $space
*
* @return void
*/
public function theAdministratorRemovesTheVersionsOfFileUsingFileId($file, $user, $space) {
$path = $this->featureContext->getStorageUsersRoot();
$fileId = $this->spacesContext->getFileId($user, $space, $file);
$command = "revisions purge -p $path -r $fileId --dry-run=false";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When /^the administrator reindexes all spaces using the CLI$/
*
* @return void
*/
public function theAdministratorReindexesAllSpacesUsingTheCli(): void {
$endpoint = $this->featureContext->getBaseUrlHostName();
$command = "search index --all-spaces --endpoint $endpoint:9220 --insecure";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When /^the administrator reindexes a space "([^"]*)" using the CLI$/
*
* @param string $spaceName
*
* @return void
*/
public function theAdministratorReindexesASpaceUsingTheCli(string $spaceName): void {
$spaceId = $this->spacesContext->getSpaceIdByName($this->featureContext->getAdminUsername(), $spaceName);
$endpoint = $this->featureContext->getBaseUrlHostName();
$command = "search index --space $spaceId --endpoint $endpoint:9220 --insecure";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator removes the file versions of space :space using the CLI
*
* @param string $space
*
* @return void
*/
public function theAdministratorRemovesTheVersionsOfFilesInSpaceUsingSpaceId(string $space): void {
$path = $this->featureContext->getStorageUsersRoot();
$adminUsername = $this->featureContext->getAdminUsername();
$spaceId = $this->spacesContext->getSpaceIdByName($adminUsername, $space);
$command = "revisions purge -p $path -r $spaceId --dry-run=false";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @Then the command should be successful
*
* @return void
*/
public function theCommandShouldBeSuccessful(): void {
$response = $this->featureContext->getResponse();
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
$jsonResponse = $this->featureContext->getJsonDecodedResponse($response);
Assert::assertSame("OK", $jsonResponse["status"], $jsonResponse["message"]);
Assert::assertSame(
0,
$jsonResponse["exitCode"],
"Expected exit code to be 0, but got " . $jsonResponse["exitCode"]
. ". Message: " . $jsonResponse["message"]
);
}
/**
* @Then /^the command output (should|should not) contain "([^"]*)"$/
*
* @param string $shouldOrNot
* @param string $output
*
* @return void
*/
public function theCommandOutputShouldContain(string $shouldOrNot, string $output): void {
$response = $this->featureContext->getResponse();
$jsonResponse = $this->featureContext->getJsonDecodedResponse($response);
$output = $this->featureContext->substituteInLineCodes($output);
if ($shouldOrNot === "should") {
Assert::assertStringContainsString($output, $jsonResponse["message"]);
} else {
Assert::assertStringNotContainsString($output, $jsonResponse["message"]);
}
}
/**
* @When the administrator lists all the upload sessions
* @When the administrator lists all the upload sessions with flag :flag
*
* @param string|null $flag
*
* @return void
*/
public function theAdministratorListsAllTheUploadSessions(?string $flag = null): void {
if ($flag) {
$flag = "--$flag";
}
$command = "storage-users uploads sessions --json $flag";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator cleans upload sessions with the following flags:
*
* @param TableNode $table
*
* @return void
*/
public function theAdministratorCleansUploadSessionsWithTheFollowingFlags(TableNode $table): void {
$flag = "";
foreach ($table->getRows() as $row) {
$flag .= "--$row[0] ";
}
$flagString = trim($flag);
$command = "storage-users uploads sessions $flagString --clean --json";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator restarts the upload sessions that are in postprocessing
*
* @return void
*/
public function theAdministratorRestartsTheUploadSessionsThatAreInPostprocessing(): void {
$command = "storage-users uploads sessions --processing --restart --json";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator restarts the upload sessions of file :file
*
* @param string $file
*
* @return void
* @throws JsonException
*/
public function theAdministratorRestartsUploadSessionsOfFile(string $file): void {
$response = CliHelper::runCommand(["command" => "storage-users uploads sessions --json"]);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
$responseArray = $this->getJSONDecodedCliMessage($response);
foreach ($responseArray as $item) {
if ($item->filename === $file) {
$uploadId = $item->id;
}
}
$command = "storage-users uploads sessions --id=$uploadId --restart --json";
$body = [
"command" => $command
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @Then /^the CLI response (should|should not) contain these entries:$/
*
* @param string $shouldOrNot
* @param TableNode $table
*
* @return void
* @throws JsonException
*/
public function theCLIResponseShouldContainTheseEntries(string $shouldOrNot, TableNode $table): void {
$expectedFiles = $table->getColumn(0);
$responseArray = $this->getJSONDecodedCliMessage($this->featureContext->getResponse());
$resourceNames = [];
foreach ($responseArray as $item) {
if (isset($item->filename)) {
$resourceNames[] = $item->filename;
}
}
if ($shouldOrNot === "should not") {
foreach ($expectedFiles as $expectedFile) {
Assert::assertNotTrue(
\in_array($expectedFile, $resourceNames),
"The resource '$expectedFile' was found in the response."
);
}
} else {
foreach ($expectedFiles as $expectedFile) {
Assert::assertTrue(
\in_array($expectedFile, $resourceNames),
"The resource '$expectedFile' was not found in the response."
);
}
}
}
/**
* @param ResponseInterface $response
*
* @return array
* @throws JsonException
*/
public function getJSONDecodedCliMessage(ResponseInterface $response): array {
$responseBody = $this->featureContext->getJsonDecodedResponse($response);
// $responseBody["message"] contains a message info with the array of output json of the upload sessions command
// Example Output: "INFO memory is not limited, skipping package=github.com/KimMachineGun/automemlimit/memlimit [{<output-json>}]"
// So, only extracting the array of output json from the message
\preg_match('/(\[.*\])/', $responseBody["message"], $matches);
return \json_decode($matches[1], null, 512, JSON_THROW_ON_ERROR);
}
/**
* @AfterScenario @cli-uploads-sessions
*
* @return void
*/
public function cleanUploadsSessions(): void {
$command = "storage-users uploads sessions --clean";
$body = [
"command" => $command
];
$response = CliHelper::runCommand($body);
Assert::assertEquals("200", $response->getStatusCode(), "Failed to clean upload sessions");
}
/**
* @When the administrator creates the folder :folder for user :user on the POSIX filesystem
*
* @param string $folder
* @param string $user
*
* @return void
*/
public function theAdministratorCreatesFolder(string $folder, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$fullPath = "$storagePath/$userUuid/$folder";
$body = [
"command" => "mkdir -p $fullPath",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
$this->waitForPath($fullPath);
sleep(1);
}
/**
* @When the administrator lists the content of the POSIX storage folder of user :user
*
* @param string $user
*
* @return void
*/
public function theAdministratorCheckUsersFolder(string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$body = [
"command" => "ls -la $storagePath/$userUuid",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator creates the file :file with content :content for user :user on the POSIX filesystem
*
* @param string $file
* @param string $content
* @param string $user
*
* @return void
*/
public function theAdministratorCreatesFile(string $file, string $content, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$fullPath = "$storagePath/$userUuid/$file";
$safeContent = escapeshellarg($content);
$body = [
"command" => "echo -n $safeContent > $fullPath",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
$this->waitForPath($fullPath);
sleep(1);
}
/**
* @When the administrator has created the file :file with content :content for user :user on the POSIX filesystem
*
* @param string $file
* @param string $content
* @param string $user
*
* @return void
*/
public function theAdministratorHasCreatedFile(string $file, string $content, string $user): void {
$this->theAdministratorCreatesFile($file, $content, $user);
$this->theCommandShouldBeSuccessful();
}
/**
* @When the administrator creates the file :file with size :size for user :user on the POSIX filesystem
*
* @param string $file
* @param string $size Example: "1gb", "5gb"
* @param string $user
*
* @return void
*/
public function theAdministratorCreatesLargeFileWithSize(string $file, string $size, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$size = strtolower($size);
if (!preg_match('/^(\d+)gb$/', $size, $matches)) {
throw new \InvalidArgumentException("Invalid size format: $size. Use formats like 1gb, 5gb.");
}
$count = (int)$matches[1] * 1024; // 1GB = 1024M
$bs = '1M';
$filePath = "$storagePath/$userUuid/$file";
$body = [
"command" => "dd if=/dev/zero of=$filePath bs=$bs count=$count",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
$this->waitForPath($filePath, $size, 15);
sleep(7);
}
/**
* @When the administrator creates :count files sequentially in the directory :dir for user :user on the POSIX filesystem
*
* @param int $count
* @param string $dir
* @param string $user
*
* @return void
*/
public function theAdministratorCreatesFilesSequentially(int $count, string $dir, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath() . "/$userUuid/$dir";
$cmd = '';
for ($i = 1; $i <= $count; $i++) {
$cmd .= "echo -n \"file $i content\" > $storagePath/file_$i.txt; ";
}
$body = [
"command" => $cmd,
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
$this->waitForPath("$storagePath/file_$count.txt");
sleep(3);
}
/**
* @When the administrator creates :count files in parallel in the directory :dir for user :user on the POSIX filesystem
*
* @param int $count
* @param string $dir
* @param string $user
*
* @return void
*/
public function theAdministratorCreatesFilesInParallel(int $count, string $dir, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath() . "/$userUuid/$dir";
$cmd = "mkdir -p $storagePath; ";
for ($i = 1; $i <= $count; $i++) {
$cmd .= "echo -n \"parallel file $i content\" > $storagePath/parallel_$i.txt & ";
}
$cmd .= "wait";
$body = [
"command" => $cmd,
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
$this->waitForPath("$storagePath/parallel_$count.txt");
sleep(1);
}
/**
* @When the administrator puts the content :content into the file :file in the POSIX storage folder of user :user
*
* @param string $content
* @param string $file
* @param string $user
*
* @return void
*/
public function theAdministratorChangesFileContent(string $content, string $file, string $user): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$safeContent = escapeshellarg($content);
$body = [
"command" => "echo -n $safeContent >> $storagePath/$userUuid/$file",
"raw" => true
];
sleep(1);
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator reads the content of the file :file in the POSIX storage folder of user :user
*
* @param string $user
* @param string $file
*
* @return void
*/
public function theAdministratorReadsTheFileContent(string $user, string $file): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$body = [
"command" => "cat $storagePath/$userUuid/$file",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
/**
* @When the administrator copies the file :file to the folder :folder for user :user on the POSIX filesystem
*
* @param string $user
* @param string $file
* @param string $folder
*
* @return void
*/
public function theAdministratorCopiesFileToFolder(string $user, string $file, string $folder): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$source = "$storagePath/$userUuid/$file";
$destination = "$storagePath/$userUuid/$folder";
$body = [
"command" => "cp $source $destination",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator renames the file :file to :newName for user :user on the POSIX filesystem
*
* @param string $user
* @param string $file
* @param string $newName
*
* @return void
*/
public function theAdministratorRenamesFile(string $user, string $file, string $newName): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$source = "$storagePath/$userUuid/$file";
$destination = "$storagePath/$userUuid/$newName";
$body = [
"command" => "mv $source $destination",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator moves the file :file to the folder :folder for user :user on the POSIX filesystem
*
* @param string $user
* @param string $file
* @param string $folder
*
* @return void
*/
public function theAdministratorMovesFileToFolder(string $user, string $file, string $folder): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$source = "$storagePath/$userUuid/$file";
$destination = "$storagePath/$userUuid/$folder";
$body = [
"command" => "mv $source $destination",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator deletes the file :file for user :user on the POSIX filesystem
*
* @param string $file
* @param string $user
*
* @return void
*/
public function theAdministratorDeletesFile(string $file, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$body = [
"command" => "rm $storagePath/$userUuid/$file",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator deletes the folder :folder for user :user on the POSIX filesystem
*
* @param string $folder
* @param string $user
*
* @return void
*/
public function theAdministratorDeletesFolder(string $folder, string $user): void {
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$body = [
"command" => "rm -r $storagePath/$userUuid/$folder",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator copies the file :file to the space :space for user :user on the POSIX filesystem
*
* @param string $user
* @param string $file
* @param string $space
*
* @return void
*/
public function theAdministratorCopiesFileToSpace(string $user, string $file, string $space): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$usersStoragePath = $this->getUsersStoragePath();
$projectsStoragePath = $this->getProjectsStoragePath();
$spaceId = $this->spacesContext->getSpaceIdByName($this->featureContext->getAdminUsername(), $space);
$spaceId = explode('$', $spaceId)[1];
$source = "$usersStoragePath/$userUuid/$file";
$destination = "$projectsStoragePath/$spaceId";
$body = [
"command" => "cp $source $destination",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator deletes the project space :space on the POSIX filesystem
*
* @param string $space
*
* @return void
*/
public function theAdministratorDeletesSpace(string $space): void {
$projectsStoragePath = $this->getProjectsStoragePath();
$spaceId = $this->spacesContext->getSpaceIdByName($this->featureContext->getAdminUsername(), $space);
$spaceId = explode('$', $spaceId)[1];
$body = [
"command" => "rm -r $projectsStoragePath/$spaceId",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
sleep(1);
}
/**
* @When the administrator checks the attribute :attribute of file :file for user :user on the POSIX filesystem
*
* @param string $attribute
* @param string $file
* @param string $user
*
* @return void
*/
public function theAdminChecksTheAttributeOfFileForUser(string $attribute, string $file, string $user): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$body = [
"command" => "getfattr -n " . escapeshellarg($attribute) . " --only-values $storagePath/$userUuid/$file",
"raw" => true
];
$this->featureContext->setResponse(CliHelper::runCommand($body));
}
}
@@ -0,0 +1,467 @@
<?php declare(strict_types=1);
/**
* @author Amrita Shrestha <amrita@jankaritech.com>
* @copyright Copyright (c) 2024 Amrita Shrestha <amrita@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 <http://www.gnu.org/licenses/>
*
*/
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 TestHelpers\HttpRequestHelper;
use TestHelpers\WebDavHelper;
use TestHelpers\CollaborationHelper;
use TestHelpers\BehatHelper;
/**
* steps needed to re-configure КуСфера server
*/
class CollaborationContext implements Context {
private FeatureContext $featureContext;
private SpacesContext $spacesContext;
private string $lastAppOpenData;
/**
* 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 = BehatHelper::getContext($scope, $environment, 'FeatureContext');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
}
/**
* @param string $data
*
* @return void
*/
public function setLastAppOpenData(string $data): void {
$this->lastAppOpenData = $data;
}
/**
* @return string
*/
public function getLastAppOpenData(): string {
return $this->lastAppOpenData;
}
/**
* @When user :user checks the information of file :file of space :space using office :app
* @When user :user checks the information of file :file of space :space using office :app with view mode :view
*
* @param string $user
* @param string $file
* @param string $space
* @param string $app
* @param string|null $viewMode
*
* @return void
*
* @throws GuzzleException
*/
public function userChecksTheInformationOfFileOfSpaceUsingOffice(
string $user,
string $file,
string $space,
string $app,
string $viewMode = null
): void {
$fileId = $this->spacesContext->getFileId($user, $space, $file);
$response = \json_decode(
CollaborationHelper::sendPOSTRequestToAppOpen(
$fileId,
$app,
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$viewMode
)->getBody()->getContents()
);
$accessToken = $response->form_parameters->access_token;
// Extract the WOPISrc from the app_url
$parsedUrl = parse_url($response->app_url);
parse_str($parsedUrl['query'], $queryParams);
$wopiSrc = $queryParams['WOPISrc'];
$this->featureContext->setResponse(
HttpRequestHelper::get(
$wopiSrc . "?access_token=$accessToken",
$this->featureContext->getStepLineRef()
)
);
}
/**
* @When user :user creates a file :file inside folder :folder in space :space using wopi endpoint
* @When user :user tries to create a file :file inside folder :folder in space :space using wopi endpoint
*
* @param string $user
* @param string $file
* @param string $folder
* @param string $space
*
* @return void
* @throws GuzzleException
*/
public function userCreatesFileInsideFolderInSpaceUsingWopiEndpoint(
string $user,
string $file,
string $folder,
string $space
): void {
$parentContainerId = $this->spacesContext->getResourceId($user, $space, $folder);
$this->featureContext->setResponse(
CollaborationHelper::createFile(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$parentContainerId,
$file
)
);
}
/**
* @param string $file
* @param string $password
* @param string $folder
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public function createFile(string $file, string $password, string $folder = ""): void {
$token = $this->featureContext->shareNgGetLastCreatedLinkShareToken();
$baseUrl = $this->featureContext->getBaseUrl();
$davPath = WebDavHelper::getDavPath(WebDavHelper::DAV_VERSION_NEW, $token, "public-files");
$responseXmlObject = HttpRequestHelper::getResponseXml(
HttpRequestHelper::sendRequest(
"$baseUrl/$davPath/$folder",
$this->featureContext->getStepLineRef(),
"PROPFIND",
"public",
$this->featureContext->getActualPassword($password)
)
);
$xmlPart = $responseXmlObject->xpath("//d:prop/oc:fileid");
$parentContainerId = (string) $xmlPart[0];
$headers = [
"Public-Token" => $token
];
$this->featureContext->setResponse(
CollaborationHelper::createFile(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
"public",
$this->featureContext->getActualPassword($password),
$parentContainerId,
$file,
$headers
)
);
}
/**
* @When the public creates a file :file inside the last shared public link folder with password :password using wopi endpoint
* @When the public tries to create a file :file inside the last shared public link folder with password :password using wopi endpoint
*
* @param string $file
* @param string $password
*
* @return void
* @throws GuzzleException
*/
public function thePublicCreatesAFileInsideTheLastSharedPublicLinkFolderWithPasswordUsingWopiEndpoint(
string $file,
string $password
): void {
$this->createFile($file, $password);
}
/**
* @When the public creates a file :file inside folder :folder in the last shared public link space with password :password using wopi endpoint
* @When the public tries to create a file :file inside folder :folder in the last shared public link space with password :password using wopi endpoint
*
* @param string $file
* @param string $folder
* @param string $password
*
* @return void
* @throws GuzzleException
*/
public function thePublicCreatesAFileInsideFolderInTheLastSharedPublicLinkSpaceWithPasswordUsingWopiEndpoint(
string $file,
string $folder,
string $password
): void {
$this->createFile($file, $password, $folder);
}
/**
* @When user :user tries to check the information of file :file of space :space using office :app with invalid file-id
*
* @param string $user
* @param string $file
* @param string $space
* @param string $app
*
* @return void
*
* @throws GuzzleException
* @throws JsonException
*/
public function userTriesToCheckTheInformationOfFileOfSpaceUsingOfficeWithInvalidFileId(
string $user,
string $file,
string $space,
string $app
): void {
$response = \json_decode(
CollaborationHelper::sendPOSTRequestToAppOpen(
$this->spacesContext->getFileId($user, $space, $file),
$app,
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef()
)->getBody()->getContents()
);
$accessToken = $response->form_parameters->access_token;
// Extract the WOPISrc from the app_url
$parsedUrl = parse_url($response->app_url);
parse_str($parsedUrl['query'], $queryParams);
$wopiSrc = $queryParams['WOPISrc'];
$position = strpos($wopiSrc, '/files/') + \strlen('/files/');
// Extract the base URL up to and including '/files/'
$fullUrl = substr($wopiSrc, 0, $position) . WebDavHelper::generateUUIDv4();
$this->featureContext->setResponse(
HttpRequestHelper::get(
$fullUrl . "?access_token=$accessToken",
$this->featureContext->getStepLineRef()
)
);
}
/**
* @When user :user tries to create a file :file inside deleted folder using wopi endpoint
*
* @param string $user
* @param string $file
*
* @return void
* @throws GuzzleException
*/
public function userTriesToCreateAFileInsideDeletedFolderUsingWopiEndpoint(string $user, string $file): void {
$parentContainerId = $this->featureContext->getStoredFileID();
$this->featureContext->setResponse(
CollaborationHelper::createFile(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$parentContainerId,
$file
)
);
}
/**
* @Given user :user has sent the following app-open request:
*
* @param string $user
* @param TableNode $properties
*
* @return void
* @throws GuzzleException
*/
public function userHasSentTheFollowingAppOpenRequest(string $user, TableNode $properties): void {
$rows = $properties->getRowsHash();
$appResponse = CollaborationHelper::sendPOSTRequestToAppOpen(
$this->spacesContext->getFileId($user, $rows['space'], $rows['resource']),
$rows['app'],
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef()
);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $appResponse);
$this->setLastAppOpenData($appResponse->getBody()->getContents());
}
/**
* @When user :user tries to get the information of the last opened file using wopi endpoint
* @When user :user gets the information of the last opened file using wopi endpoint
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userGetsTheInformationOfTheLastOpenedFileUsingWopiEndpoint(string $user): void {
$response = json_decode($this->getLastAppOpenData());
$accessToken = $response->form_parameters->access_token;
// Extract the WOPISrc from the app_url
$parsedUrl = parse_url($response->app_url);
parse_str($parsedUrl['query'], $queryParams);
$wopiSrc = $queryParams['WOPISrc'];
$this->featureContext->setResponse(
HttpRequestHelper::get(
$wopiSrc . "?access_token=$accessToken",
$this->featureContext->getStepLineRef()
)
);
}
/**
* @Then /^the response (should|should not) contain the following MIME types:$/
*
* @param string $shouldOrNot
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theFollowingMimeTypesShouldExistForUser(string $shouldOrNot, TableNode $table): void {
$rows = $table->getRows();
$responseArray = $this->featureContext->getJsonDecodedResponse(
$this->featureContext->getResponse()
)['mime-types'];
$mimeTypes = \array_column($responseArray, 'mime_type');
foreach ($rows as $row) {
if ($shouldOrNot === "should not") {
Assert::assertFalse(
\in_array($row[0], $mimeTypes),
"the response should not contain the mimetype $row[0].\nMime Types found in response:\n"
. print_r($mimeTypes, true)
);
} else {
Assert::assertTrue(
\in_array($row[0], $mimeTypes),
"the response does not contain the mimetype $row[0].\nMime Types found in response:\n"
. print_r($mimeTypes, true)
);
}
}
}
/**
* @Then the app list response should contain the following template information for office :app:
*
* @param string $app
* @param TableNode $table
*
* @return void
*/
public function theAppListResponseShouldContainTheFollowingTemplateInformationForOffice(
string $app,
TableNode $table
): void {
$responseArray = $this->featureContext->getJsonDecodedResponse($this->featureContext->getResponse());
Assert::assertArrayHasKey(
"mime-types",
$responseArray,
"Expected 'mime-types' in the response but not found.\n" . print_r($responseArray, true)
);
$mimeTypes = $responseArray['mime-types'];
$mimeTypeMap = [];
foreach ($mimeTypes as $mimeType) {
$mimeTypeMap[$mimeType['mime_type']] = $mimeType;
}
foreach ($table->getColumnsHash() as $row) {
Assert::assertArrayHasKey(
$row['mime-type'],
$mimeTypeMap,
"Expected mime-type '{$row['mime-type']}' to exist in the response but it doesn't.\n"
. print_r($mimeTypeMap, true)
);
$mimeType = $mimeTypeMap[$row['mime-type']];
$found = false;
foreach ($mimeType['app_providers'] as $provider) {
if ($provider['name'] === $app && isset($row['target-extension'])) {
Assert::assertSame(
$row['target-extension'],
$provider['target_ext'],
"Expected 'target_ext' for $app to be '{$row['target-extension']}'"
. " but found '{$provider['target_ext']}'"
);
$found = true;
break;
}
}
if (!$found) {
Assert::fail(
"Expected response to contain app-provider '$app' with target-extension "
. "'{$row['target-extension']}' for mime-type '{$row['mime-type']}',"
. " but no matching provider was found.\n App Providers Found: "
. print_r($mimeType['app_providers'], true)
);
}
}
}
/**
* @When user :user has created a file :file using wopi endpoint
*
* @param string $user
* @param string $file
*
* @return string
* @throws GuzzleException
*/
public function userHasCreatedAFileInSpaceUsingWopiEndpoint(string $user, string $file): string {
$parentContainerId = $this->featureContext->getFileIdForPath($user, "/");
$response = CollaborationHelper::createFile(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$parentContainerId,
$file
);
$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
$decodedResponse = $this->featureContext->getJsonDecodedResponseBodyContent($response);
return $decodedResponse->file_id;
}
}
@@ -0,0 +1,92 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @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 <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* context containing favorites related API steps
*/
class FavoritesContext implements Context {
private WebDavPropertiesContext $webDavPropertiesContext;
/**
* @Then /^as user "([^"]*)" (?:file|folder|entry) "([^"]*)" should be favorited$/
*
* @param string $user
* @param string $path
* @param integer $expectedValue 0|1
* @param string|null $spaceId
*
* @return void
*/
public function asUserFileOrFolderShouldBeFavorited(
string $user,
string $path,
int $expectedValue = 1,
?string $spaceId = null
): void {
$property = "oc:favorite";
$this->webDavPropertiesContext->checkPropertyOfAFolder(
$user,
$path,
$property,
(string)$expectedValue,
null,
$spaceId,
);
}
/**
* @Then /^as user "([^"]*)" (?:file|folder|entry) "([^"]*)" should not be favorited$/
*
* @param string $user
* @param string $path
*
* @return void
*/
public function asUserFileShouldNotBeFavorited(string $user, string $path): void {
$this->asUserFileOrFolderShouldBeFavorited($user, $path, 0);
}
/**
* 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->webDavPropertiesContext = BehatHelper::getContext(
$scope,
$environment,
'WebDavPropertiesContext'
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,551 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2018, ownCloud GmbH
*
* 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 <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\HttpRequestHelper;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* Steps that relate to files_versions app
*/
class FilesVersionsContext implements Context {
private FeatureContext $featureContext;
private SpacesContext $spacesContext;
/**
* 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');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
}
/**
* @When user :user tries to get versions of file :file from :fileOwner
*
* @param string $user
* @param string $file
* @param string $fileOwner
*
* @return void
* @throws Exception
*/
public function userTriesToGetFileVersions(string $user, string $file, string $fileOwner): void {
$this->featureContext->setResponse($this->getFileVersions($user, $file, $fileOwner));
}
/**
* @When user :user gets the number of versions of file :file
*
* @param string $user
* @param string $file
*
* @return void
* @throws Exception
*/
public function userGetsFileVersions(string $user, string $file): void {
$this->featureContext->setResponse($this->getFileVersions($user, $file));
}
/**
* @When the public tries to get the number of versions of file :file with password :password using file-id :endpoint
*
* @param string $file
* @param string $password
* @param string $fileId
*
* @return void
*/
public function thePublicTriesToGetTheNumberOfVersionsOfFileWithPasswordUsingFileId(
string $file,
string $password,
string $fileId
): void {
$password = $this->featureContext->getActualPassword($password);
$this->featureContext->setResponse(
$this->featureContext->makeDavRequest(
"public",
"PROPFIND",
$fileId,
null,
null,
null,
"versions",
$this->featureContext->getDavPathVersion(),
false,
$password
)
);
}
/**
* @param string $user
* @param string $file
* @param string|null $fileOwner
* @param string|null $spaceId
*
* @return ResponseInterface
* @throws JsonException
* @throws GuzzleException
*/
public function getFileVersions(
string $user,
string $file,
?string $fileOwner = null,
?string $spaceId = null,
): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$fileOwner = $fileOwner ? $this->featureContext->getActualUsername($fileOwner) : $user;
$fileId = $this->featureContext->getFileIdForPath($fileOwner, $file, $spaceId);
Assert::assertNotNull(
$fileId,
__METHOD__
. " fileid of file $file user $fileOwner not found (the file may not exist)"
);
return $this->featureContext->makeDavRequest(
$user,
"PROPFIND",
$fileId,
null,
null,
$spaceId,
"versions"
);
}
/**
* @When user :user gets the number of versions of file :resource using file-id :fileId
* @When user :user tries to get the number of versions of file :resource using file-id :fileId
*
* @param string $user
* @param string $fileId
*
* @return void
*/
public function userGetsTheNumberOfVersionsOfFileOfTheSpace(string $user, string $fileId): void {
$this->featureContext->setResponse(
$this->featureContext->makeDavRequest(
$user,
"PROPFIND",
$fileId,
null,
null,
null,
"versions"
)
);
}
/**
* @param string $user
* @param string $file
*
* @return ResponseInterface
*/
public function getFileVersionMetadata(string $user, string $file): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$fileId = $this->featureContext->getFileIdForPath($user, $file);
Assert::assertNotNull(
$fileId,
__METHOD__
. " fileid of file $file user $user not found (the file may not exist)"
);
$body = '<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop>
<oc:meta-version-edited-by />
<oc:meta-version-edited-by-name />
</d:prop>
</d:propfind>';
return $this->featureContext->makeDavRequest(
$user,
"PROPFIND",
$fileId,
null,
$body,
null,
"versions",
);
}
/**
* @param string $user
* @param int $versionIndex
* @param string $path
*
* @return ResponseInterface
* @throws Exception
*/
public function restoreVersionIndexOfFile(string $user, int $versionIndex, string $path): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$fileId = $this->featureContext->getFileIdForPath($user, $path);
Assert::assertNotNull(
$fileId,
__METHOD__
. " fileid of file $path user $user not found (the file may not exist)"
);
$response = $this->listVersionFolder($user, $fileId, 1);
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
$xmlPart = $responseXmlObject->xpath("//d:response/d:href");
//restoring the version only works with DAV path v2
$destinationUrl = $this->featureContext->getBaseUrl() . "/" .
WebDavHelper::getDavPath(WebDavHelper::DAV_VERSION_NEW, $user) . \trim($path, "/");
$fullUrl = $this->featureContext->getBaseUrlWithoutPath() .
$xmlPart[$versionIndex];
return HttpRequestHelper::sendRequest(
$fullUrl,
$this->featureContext->getStepLineRef(),
'COPY',
$user,
$this->featureContext->getPasswordForUser($user),
['Destination' => $destinationUrl]
);
}
/**
* @Given user :user has restored version index :versionIndex of file :path
*
* @param string $user
* @param int $versionIndex
* @param string $path
*
* @return void
* @throws Exception
*/
public function userHasRestoredVersionIndexOfFile(string $user, int $versionIndex, string $path): void {
$response = $this->restoreVersionIndexOfFile($user, $versionIndex, $path);
$this->featureContext->theHTTPStatusCodeShouldBe(204, "", $response);
}
/**
* @When user :user restores version index :versionIndex of file :path using the WebDAV API
*
* @param string $user
* @param int $versionIndex
* @param string $path
*
* @return void
* @throws Exception
*/
public function userRestoresVersionIndexOfFile(string $user, int $versionIndex, string $path): void {
$response = $this->restoreVersionIndexOfFile($user, $versionIndex, $path);
$this->featureContext->setResponse($response, $user);
}
/**
* assert file versions count
*
* @param string $user
* @param string $fileId
* @param int $expectedCount
*
* @return void
* @throws Exception
*/
public function assertFileVersionsCount(string $user, string $fileId, int $expectedCount): void {
$response = $this->listVersionFolder($user, $fileId, 1);
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
$actualCount = \count($responseXmlObject->xpath("//d:prop/d:getetag")) - 1;
if ($actualCount === -1) {
$actualCount = 0;
}
Assert::assertEquals(
$expectedCount,
$actualCount,
"Expected $expectedCount versions but found $actualCount in \n" . $responseXmlObject->asXML()
);
}
/**
* @Then the version folder of file :path for user :user should contain :count element(s)
*
* @param string $path
* @param string $user
* @param int $count
*
* @return void
* @throws Exception
*/
public function theVersionFolderOfFileShouldContainElements(
string $path,
string $user,
int $count
): void {
$user = $this->featureContext->getActualUsername($user);
$fileId = $this->featureContext->getFileIdForPath($user, $path);
Assert::assertNotNull(
$fileId,
__METHOD__
. ". file '$path' for user '$user' not found (the file may not exist)"
);
$this->assertFileVersionsCount($user, $fileId, $count);
}
/**
* @Then the content length of file :path with version index :index for user :user in versions folder should be :length
*
* @param string $path
* @param int $index
* @param string $user
* @param int $length
*
* @return void
* @throws Exception
*/
public function theContentLengthOfFileForUserInVersionsFolderIs(
string $path,
int $index,
string $user,
int $length
): void {
$user = $this->featureContext->getActualUsername($user);
$fileId = $this->featureContext->getFileIdForPath($user, $path);
Assert::assertNotNull(
$fileId,
__METHOD__
. " fileid of file $path user $user not found (the file may not exist)"
);
$response = $this->listVersionFolder($user, $fileId, 1, ['d:getcontentlength']);
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
$xmlPart = $responseXmlObject->xpath("//d:prop/d:getcontentlength");
Assert::assertEquals(
$length,
(int) $xmlPart[$index],
"The content length of file $path with version $index for user $user was
expected to be $length but the actual content length is $xmlPart[$index]"
);
}
/**
* @param string $user
* @param string $path
* @param string $index
* @param string|NullCpuCoreFinder $spaceId
*
* @return ResponseInterface
* @throws Exception
*/
public function downloadVersion(
string $user,
string $path,
string $index,
?string $spaceId = null
): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$fileId = $this->featureContext->getFileIdForPath($user, $path, $spaceId);
Assert::assertNotNull(
$fileId,
__METHOD__
. " fileid of file $path user $user not found (the file may not exist)"
);
$index = (int)$index;
$response = $this->listVersionFolder($user, $fileId, 1);
if ($response->getStatusCode() === 403) {
return $response;
}
$responseXmlObject = new SimpleXMLElement($response->getBody()->getContents());
$xmlPart = $responseXmlObject->xpath("//d:response/d:href");
if (!isset($xmlPart[$index])) {
Assert::fail(
'could not find version of path "' . $path . '" with index "' . $index . '"'
);
}
// the href already contains the path
$url = WebDavHelper::sanitizeUrl(
$this->featureContext->getBaseUrlWithoutPath() . $xmlPart[$index]
);
return HttpRequestHelper::get(
$url,
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user)
);
}
/**
* @When user :user downloads the version of file :path with the index :index
*
* @param string $user
* @param string $path
* @param string $index
*
* @return void
* @throws Exception
*/
public function userDownloadsVersion(string $user, string $path, string $index): void {
$this->featureContext->setResponse($this->downloadVersion($user, $path, $index), $user);
}
/**
* @Then /^the content of version index "([^"]*)" of file "([^"]*)" for user "([^"]*)" should be "([^"]*)"$/
*
* @param string $index
* @param string $path
* @param string $user
* @param string $content
*
* @return void
* @throws Exception
*/
public function theContentOfVersionIndexOfFileForUserShouldBe(
string $index,
string $path,
string $user,
string $content
): void {
$response = $this->downloadVersion($user, $path, $index);
$this->featureContext->theHTTPStatusCodeShouldBe("200", '', $response);
$this->featureContext->checkDownloadedContentMatches($content, '', $response);
}
/**
* @When /^user "([^"]*)" retrieves the meta information of (file|fileId) "([^"]*)" using the meta API$/
*
* @param string $user
* @param string $fileOrFileId
* @param string $path
*
* @return void
*/
public function userGetMetaInfo(string $user, string $fileOrFileId, string $path): void {
$user = $this->featureContext->getActualUsername($user);
$baseUrl = $this->featureContext->getBaseUrl();
$password = $this->featureContext->getPasswordForUser($user);
if ($fileOrFileId === "file") {
$fileId = $this->featureContext->getFileIdForPath($user, $path);
$metaPath = "/meta/$fileId/";
} else {
$metaPath = "/meta/$path/";
}
$body = '<?xml version="1.0" encoding="utf-8"?>
<a:propfind xmlns:a="DAV:" xmlns:oc="http://owncloud.org/ns">
<a:prop>
<oc:meta-path-for-user />
</a:prop>
</a:propfind>';
$response = WebDavHelper::makeDavRequest(
$baseUrl,
$user,
$password,
"PROPFIND",
$metaPath,
['Content-Type' => 'text/xml','Depth' => '0'],
null,
$this->featureContext->getStepLineRef(),
$body,
$this->featureContext->getDavPathVersion(),
null
);
$this->featureContext->setResponse($response);
}
/**
* returns the result parsed into a SimpleXMLElement
* with a registered namespace with 'd' as prefix and 'DAV:' as namespace
*
* @param string $user
* @param string $fileId
* @param int $folderDepth
* @param string[]|null $properties
*
* @return ResponseInterface
* @throws Exception
*/
public function listVersionFolder(
string $user,
string $fileId,
int $folderDepth,
?array $properties = null
): ResponseInterface {
if (!$properties) {
$properties = [
'd:getetag'
];
}
$user = $this->featureContext->getActualUsername($user);
$password = $this->featureContext->getPasswordForUser($user);
$response = WebDavHelper::propfind(
$this->featureContext->getBaseUrl(),
$user,
$password,
$fileId,
$properties,
$this->featureContext->getStepLineRef(),
(string) $folderDepth,
null,
"versions"
);
return $response;
}
/**
* @When user :user gets the number of versions of file :file from space :space
* @When user :user tries to get the number of versions of file :file from space :space
*
* @param string $user
* @param string $file
* @param string $space
*
* @return void
*/
public function userGetsTheNumberOfVersionsOfFileFromSpace(string $user, string $file, string $space): void {
$fileId = $this->spacesContext->getFileId($user, $space, $file);
$this->featureContext->setResponse(
$this->featureContext->makeDavRequest(
$user,
"PROPFIND",
$fileId,
null,
null,
null,
"versions"
)
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,763 @@
<?php declare(strict_types=1);
/**
* @author Viktor Scharf <vscharf@owncloud.com>
* @copyright Copyright (c) 2023 Viktor Scharf vscharf@owncloud.com
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use Behat\Gherkin\Node\PyStringNode;
use PHPUnit\Framework\Assert;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\EmailHelper;
use TestHelpers\OcsApiHelper;
use TestHelpers\GraphHelper;
use TestHelpers\SettingsHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* Defines application features from the specific context.
*/
class NotificationContext implements Context {
private FeatureContext $featureContext;
private SpacesContext $spacesContext;
private string $notificationEndpointPath = '/apps/notifications/api/v1/notifications?format=json';
private string $globalNotificationEndpointPath = '/apps/notifications/api/v1/notifications/global';
private array $notificationIds;
/**
* @return array[]
*/
public function getNotificationIds(): array {
return $this->notificationIds;
}
/**
* @return array[]
*/
public function getLastNotificationId(): array {
return \end($this->notificationIds);
}
/**
* @AfterScenario
*
* @return void
*/
public function deleteDeprovisioningNotification(): void {
$payload["ids"] = ["deprovision"];
OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$this->featureContext->getAdminUsername(),
$this->featureContext->getAdminPassword(),
'DELETE',
$this->globalNotificationEndpointPath,
$this->featureContext->getStepLineRef(),
json_encode($payload)
);
}
/**
* @var string
*/
private string $userRecipient;
/**
* @param string $userRecipient
*
* @return void
*/
public function setUserRecipient(string $userRecipient): void {
$this->userRecipient = $userRecipient;
}
/**
* @return string
*/
public function getUserRecipient(): string {
return $this->userRecipient;
}
/**
* @BeforeScenario
*
* @param BeforeScenarioScope $scope
*
* @return void
* @throws Exception
*/
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');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
}
/**
* @param string $user
*
* @return ResponseInterface
*/
public function listAllNotifications(string $user): ResponseInterface {
$this->setUserRecipient($user);
$language = SettingsHelper::getLanguageSettingValue(
$this->featureContext->getBaseUrl(),
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getStepLineRef()
);
$headers = ["accept-language" => $language];
return OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
'GET',
$this->notificationEndpointPath,
$this->featureContext->getStepLineRef(),
[],
2,
$headers
);
}
/**
* @When /^user "([^"]*)" lists all notifications$/
*
* @param string $user
*
* @return void
*/
public function userListAllNotifications(string $user): void {
$response = $this->listAllNotifications($user);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastHttpStatusCodesArray();
}
/**
* @param string $user
*
* @return ResponseInterface
* @throws GuzzleException
* @throws JsonException
*/
public function deleteAllNotifications(string $user): ResponseInterface {
$response = $this->listAllNotifications($user);
if (isset($this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data)) {
$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data;
foreach ($responseBody as $value) {
// set notificationId
$this->notificationIds[] = $value->notification_id;
}
}
return $this->userDeletesNotification($user);
}
/**
* @When user :user deletes all notifications
*
* @param string $user
*
* @return void
* @throws GuzzleException
* @throws JsonException
*/
public function userDeletesAllNotifications(string $user): void {
$response = $this->deleteAllNotifications($user);
$this->featureContext->setResponse($response);
}
/**
* @Given user :user has deleted all notifications
*
* @param string $user
*
* @return void
* @throws GuzzleException
* @throws JsonException
*/
public function userHasDeletedAllNotifications(string $user): void {
$response = $this->deleteAllNotifications($user);
$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
}
/**
* @When user :user deletes a notification related to resource :resource with subject :subject
*
* @param string $user
* @param string $resource
* @param string $subject
*
* @return void
* @throws GuzzleException
* @throws JsonException
*/
public function userDeletesNotificationOfResourceAndSubject(string $user, string $resource, string $subject): void {
$response = $this->listAllNotifications($user);
$this->filterNotificationsBySubjectAndResource($subject, $resource, $response);
$this->featureContext->setResponse($this->userDeletesNotification($user));
}
/**
* deletes notification
*
* @param string $user
*
* @return ResponseInterface
* @throws GuzzleException
* @throws JsonException
*/
public function userDeletesNotification(string $user): ResponseInterface {
$this->setUserRecipient($user);
$payload["ids"] = $this->getNotificationIds();
return OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
'DELETE',
$this->notificationEndpointPath,
$this->featureContext->getStepLineRef(),
\json_encode($payload),
2
);
}
/**
* @Then the notifications should be empty
*
* @return void
* @throws Exception
*/
public function theNotificationsShouldBeEmpty(): void {
$statusCode = $this->featureContext->getResponse()->getStatusCode();
if ($statusCode !== 200) {
$response = $this->featureContext->getResponse()->getBody()->getContents();
throw new \Exception(
__METHOD__
. " Failed to get user notification list" . $response
);
}
$notifications = $this->featureContext->getJsonDecodedResponseBodyContent()->ocs->data;
Assert::assertNull($notifications, "response should not contain any notification");
}
/**
* @Then user :user should not have any notification
*
* @param $user
*
* @return void
* @throws Exception
*/
public function userShouldNotHaveAnyNotification($user): void {
$response = $this->listAllNotifications($user);
$notifications = $this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data;
Assert::assertNull($notifications, "response should not contain any notification");
}
/**
* @Then /^there should be "([^"]*)" notifications$/
*
* @param int $numberOfNotification
*
* @return void
* @throws Exception
*/
public function userShouldHaveNotifications(int $numberOfNotification): void {
if (!isset($this->featureContext->getJsonDecodedResponseBodyContent()->ocs->data)) {
throw new Exception("Notification is empty");
}
$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent()->ocs->data;
$actualNumber = \count($responseBody);
Assert::assertEquals(
$numberOfNotification,
$actualNumber,
"Expected number of notifications was '$numberOfNotification', but got '$actualNumber'"
);
}
/**
* @Then /^the JSON response should contain a notification message with the subject "([^"]*)" and the message-details should match$/
*
* @param string $subject
* @param PyStringNode $schemaString
*
* @return void
* @throws Exception
*/
public function theJsonDataFromLastResponseShouldMatch(
string $subject,
PyStringNode $schemaString
): void {
$responseBody = $this->filterResponseAccordingToNotificationSubject($subject);
// substitute the value here
$schemaString = $schemaString->getRaw();
$schemaString = $this->featureContext->substituteInLineCodes(
$schemaString,
$this->featureContext->getCurrentUser(),
[],
[],
null,
$this->getUserRecipient(),
);
$this->featureContext->assertJsonDocumentMatchesSchema(
$responseBody,
$this->featureContext->getJSONSchema($schemaString)
);
}
/**
* filter notification according to subject
*
* @param string $subject
* @param ResponseInterface|null $response
*
* @return object
*/
public function filterResponseAccordingToNotificationSubject(
string $subject,
?ResponseInterface $response = null
): object {
$response = $response ?? $this->featureContext->getResponse();
if (isset($this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data)) {
$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data;
foreach ($responseBody as $value) {
if (isset($value->subject) && $value->subject === $subject) {
$responseBody = $value;
// set notificationId
$this->notificationIds[] = $value->notification_id;
break;
}
}
} else {
$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent($response);
}
return $responseBody;
}
/**
* filter notification according to subject and resource
*
* @param string $subject
* @param string $resource
* @param ResponseInterface|null $response
*
* @return array
*/
public function filterNotificationsBySubjectAndResource(
string $subject,
string $resource,
?ResponseInterface $response = null
): array {
$filteredNotifications = [];
$response = $response ?? $this->featureContext->getResponse();
$responseObject = $this->featureContext->getJsonDecodedResponseBodyContent($response);
if (!isset($responseObject->ocs->data)) {
Assert::fail("Response doesn't contain notification: " . print_r($responseObject, true));
}
$notifications = $responseObject->ocs->data;
foreach ($notifications as $notification) {
if (isset($notification->subject) && $notification->subject === $subject
&& isset($notification->messageRichParameters->resource->name)
&& $notification->messageRichParameters->resource->name === $resource
) {
$this->notificationIds[] = $notification->notification_id;
$filteredNotifications[] = $notification;
}
}
return $filteredNotifications;
}
/**
* @Then /^user "([^"]*)" should get a notification with subject "([^"]*)" and message:$/
*
* @param string $user
* @param string $subject
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userShouldGetANotificationWithMessage(string $user, string $subject, TableNode $table): void {
$count = 0;
// Sometimes the test might try to get the notifications before the server has created the notification.
// To prevent the test from failing because of that, try to list the notifications again
do {
if ($count > 0) {
\sleep(1);
}
$this->featureContext->setResponse(null);
$response = $this->listAllNotifications($user);
$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
++$count;
} while (!isset($this->filterResponseAccordingToNotificationSubject($subject, $response)->message)
&& $count <= 10
);
if (isset($this->filterResponseAccordingToNotificationSubject($subject, $response)->message)) {
$actualMessage = str_replace(
["\r", "\n"],
" ",
$this->filterResponseAccordingToNotificationSubject($subject, $response)->message
);
} else {
throw new \Exception("Notification was not found even after retrying for 5 seconds.");
}
$expectedMessage = $table->getColumnsHash()[0]['message'];
Assert::assertStringStartsWith(
$expectedMessage,
$actualMessage,
__METHOD__ . "expected message to start with '$expectedMessage' but found'$actualMessage'"
);
}
/**
* @Then user :user should get a notification for resource :resource with subject :subject and message:
*
* @param string $user
* @param string $resource
* @param string $subject
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userShouldGetNotificationForResourceWithMessage(
string $user,
string $resource,
string $subject,
TableNode $table
): void {
$response = $this->listAllNotifications($user);
$notification = $this->filterNotificationsBySubjectAndResource($subject, $resource, $response);
if (\count($notification) === 1) {
$actualMessage = str_replace(["\r", "\r"], " ", $notification[0]->message);
$expectedMessage = $table->getColumnsHash()[0]['message'];
Assert::assertStringStartsWith(
$expectedMessage,
$actualMessage,
__METHOD__ . "expected message to start with '$expectedMessage' but found'$actualMessage'"
);
$response = $this->userDeletesNotification($user);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
} elseif (\count($notification) === 0) {
throw new \Exception(
"Response doesn't contain any notification with resource '$resource' and subject '$subject'.\n"
. print_r($notification, true)
);
} else {
throw new \Exception(
"Response contains more than one notification with resource '$resource' and subject '$subject'.\n"
. print_r($notification, true)
);
}
}
/**
* @Then user :user should not get a notification related to resource :resource with subject :subject
*
* @param string $user
* @param string $resource
* @param string $subject
*
* @return void
*/
public function userShouldNotHaveANotificationRelatedToResourceWithSubject(
string $user,
string $resource,
string $subject
): void {
$response = $this->listAllNotifications($user);
$filteredResponse = $this->filterNotificationsBySubjectAndResource($subject, $resource, $response);
Assert::assertCount(
0,
$filteredResponse,
"Response should not contain notification related to resource '$resource' with subject '$subject' but found"
. print_r($filteredResponse, true)
);
}
/**
* @Then user :user should have received the following email from user :sender about the share of project space :spaceName
*
* @param string $user
* @param string $sender
* @param string $spaceName
* @param PyStringNode $content
*
* @return void
* @throws Exception
*/
public function userShouldHaveReceivedTheFollowingEmailFromUserAboutTheShareOfProjectSpace(
string $user,
string $sender,
string $spaceName,
PyStringNode $content
): void {
$rawExpectedEmailBodyContent = \str_replace("\r\n", "\n", $content->getRaw());
$this->featureContext->setResponse(
GraphHelper::getMySpaces(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
'',
$this->featureContext->getStepLineRef()
)
);
$expectedEmailBodyContent = $this->featureContext->substituteInLineCodes(
$rawExpectedEmailBodyContent,
$sender,
[],
[
[
"code" => "%space_id%",
"function" =>
[$this->spacesContext, "getSpaceIdByName"],
"parameter" => [$sender, $spaceName]
],
]
);
$this->assertEmailContains($user, $expectedEmailBodyContent);
}
/**
* @Then user :user should have received the following email from user :sender
*
* @param string $user
* @param string $sender
* @param PyStringNode $content
*
* @return void
* @throws Exception
*/
public function userShouldHaveReceivedTheFollowingEmailFromUser(
string $user,
string $sender,
PyStringNode $content
): void {
$rawExpectedEmailBodyContent = \str_replace("\r\n", "\n", $content->getRaw());
$expectedEmailBodyContent = $this->featureContext->substituteInLineCodes(
$rawExpectedEmailBodyContent,
$sender
);
$this->assertEmailContains($user, $expectedEmailBodyContent);
}
/**
* @Then user :user should have received the following email from user :sender ignoring whitespaces
*
* @param string $user
* @param string $sender
* @param PyStringNode $content
*
* @return void
* @throws Exception
*/
public function userShouldHaveReceivedTheFollowingEmailFromUserIgnoringWhitespaces(
string $user,
string $sender,
PyStringNode $content
): void {
$rawExpectedEmailBodyContent = \str_replace("\r\n", "\n", $content->getRaw());
$expectedEmailBodyContent = $this->featureContext->substituteInLineCodes(
$rawExpectedEmailBodyContent,
$sender
);
$this->assertEmailContains($user, $expectedEmailBodyContent, true);
}
/***
* @param string $user
* @param string $expectedEmailBodyContent
* @param bool $ignoreWhiteSpace
*
* @return void
* @throws GuzzleException
*/
public function assertEmailContains(
string $user,
string $expectedEmailBodyContent,
$ignoreWhiteSpace = false
): void {
$address = $this->featureContext->getEmailAddressForUser($user);
$this->featureContext->pushEmailRecipientAsMailBox($address);
// assert with retries as email delivery might be delayed
$retried = 0;
do {
$actualEmailBodyContent = EmailHelper::getBodyOfLastEmail(
$address,
$this->featureContext->getStepLineRef()
);
if ($ignoreWhiteSpace) {
$expectedEmailBodyContent = preg_replace('/\s+/', '', $expectedEmailBodyContent);
$actualEmailBodyContent = preg_replace('/\s+/', '', $actualEmailBodyContent);
}
$tryAgain = !\str_contains($actualEmailBodyContent, $expectedEmailBodyContent)
&& $retried <= STANDARD_RETRY_COUNT;
$retried++;
if ($tryAgain) {
$mailBox = EmailHelper::getMailBoxFromEmail($address);
echo "[INFO] Checking last email content for '$mailBox'. (Retry $retried)\n";
// wait for 1 second before trying again
sleep(1);
}
} while ($tryAgain);
Assert::assertStringContainsString(
$expectedEmailBodyContent,
$actualEmailBodyContent,
"The email address '$address' should have received an"
. " email with the body containing '$expectedEmailBodyContent'"
. " but the received email is '$actualEmailBodyContent'"
);
}
/**
* Delete all emails from the mailboxes
*
* @AfterScenario @email
*
* @return void
*/
public function clearMailboxes(): void {
$users = \array_keys($this->featureContext->getCreatedUsers());
try {
if (!empty($users)) {
foreach ($users as $emailRecipient) {
$retried = 0;
do {
$res = EmailHelper::deleteAllEmails(
EmailHelper::getLocalEmailUrl(),
$emailRecipient,
$this->featureContext->getStepLineRef(),
);
$deleteStatus = $res->getStatusCode();
$mailBox = EmailHelper::getMailboxInformation($emailRecipient);
$tryAgain = ($deleteStatus !== 200 || !empty($mailBox)) && $retried <= STANDARD_RETRY_COUNT;
$retried++;
if ($tryAgain) {
echo "[INFO] Clearing mailbox '$emailRecipient'."
. " Status: $deleteStatus. Emails: " . \count($mailBox) . "."
. " (Retry $retried)\n";
// wait for 1 second before trying again
sleep(1);
}
} while ($tryAgain);
}
}
} catch (Exception $e) {
echo __METHOD__ .
" could not delete inbucket messages, is inbucket set up?\n" .
$e->getMessage();
}
}
/**
*
* @param string|null $user
* @param string|null $deprovision_date
* @param string|null $deprovision_date_format
*
* @return ResponseInterface
*
* @throws GuzzleException
*
* @throws JsonException
*/
public function userCreatesDeprovisioningNotification(
?string $user = null,
?string $deprovision_date = "2043-07-04T11:23:12Z",
?string $deprovision_date_format= "2006-01-02T15:04:05Z07:00"
): ResponseInterface {
$payload["type"] = "deprovision";
$payload["data"] = [
"deprovision_date" => $deprovision_date, "deprovision_date_format" => $deprovision_date_format];
return OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$user ? $this->featureContext->getActualUsername($user) : $this->featureContext->getAdminUsername(),
$user ? $this->featureContext->getPasswordForUser($user) : $this->featureContext->getAdminPassword(),
'POST',
$this->globalNotificationEndpointPath,
$this->featureContext->getStepLineRef(),
json_encode($payload)
);
}
/**
* @When the administrator creates a deprovisioning notification
* @When user :user tries to create a deprovisioning notification
*
* @param string|null $user
*
* @return void
*
* @throws GuzzleException
* @throws JsonException
*/
public function theAdministratorCreatesADeprovisioningNotification(?string $user = null) {
$response = $this->userCreatesDeprovisioningNotification($user);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastHttpStatusCodesArray();
}
/**
* @When the administrator creates a deprovisioning notification for date :deprovision_date of format :deprovision_date_format
*
* @param $deprovision_date
* @param $deprovision_date_format
*
* @return void
*
* @throws GuzzleException
* @throws JsonException
*/
public function theAdministratorCreatesADeprovisioningNotificationUsingDateFormat(
$deprovision_date,
$deprovision_date_format
) {
$response = $this->userCreatesDeprovisioningNotification(null, $deprovision_date, $deprovision_date_format);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastHttpStatusCodesArray();
}
/**
* @Given the administrator has created a deprovisioning notification
*
* @return void
*/
public function userHasCreatedDeprovisioningNotification(): void {
$response = $this->userCreatesDeprovisioningNotification();
$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
}
/**
* @When the administrator deletes the deprovisioning notification
* @When user :user tries to delete the deprovisioning notification
*
* @param string|null $user
*
* @return void
*/
public function userDeletesDeprovisioningNotification(?string $user = null): void {
$payload["ids"] = ["deprovision"];
$response = OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$user ? $this->featureContext->getActualUsername($user) : $this->featureContext->getAdminUsername(),
$user ? $this->featureContext->getPasswordForUser($user) : $this->featureContext->getAdminPassword(),
'DELETE',
$this->globalNotificationEndpointPath,
$this->featureContext->getStepLineRef(),
json_encode($payload)
);
$this->featureContext->setResponse($response);
}
}
@@ -0,0 +1,438 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2019, ownCloud GmbH
*
* 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 <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Psr\Http\Message\ResponseInterface;
use PHPUnit\Framework\Assert;
use TestHelpers\HttpRequestHelper;
use TestHelpers\OcsApiHelper;
use TestHelpers\TranslationHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* steps needed to send requests to the OCS API
*/
class OCSContext implements Context {
private FeatureContext $featureContext;
/**
* @When /^the user sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)"$/
*
* @param string $verb
* @param string $url
*
* @return void
*/
public function theUserSendsToOcsApiEndpoint(string $verb, string $url): void {
$response = $this->theUserSendsToOcsApiEndpointWithBody($verb, $url);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)"$/
* @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" using password "([^"]*)"$/
*
* @param string $user
* @param string $verb
* @param string $url
* @param string|null $password
*
* @return void
*/
public function userSendsToOcsApiEndpoint(string $user, string $verb, string $url, ?string $password = null): void {
$response = $this->sendRequestToOcsEndpoint(
$user,
$verb,
$url,
null,
$password
);
$this->featureContext->setResponse($response);
}
/**
* @param string $user
* @param string $verb
* @param string $url
* @param TableNode|null $body
* @param string|null $password
* @param array|null $headers
*
* @return ResponseInterface
*/
public function sendRequestToOcsEndpoint(
string $user,
string $verb,
string $url,
?TableNode $body = null,
?string $password = null,
?array $headers = null
): ResponseInterface {
/**
* array of the data to be sent in the body.
* contains $body data converted to an array
*/
$bodyArray = [];
if ($body instanceof TableNode) {
$bodyArray = $body->getRowsHash();
}
if ($user !== 'UNAUTHORIZED_USER') {
if ($password === null) {
$password = $this->featureContext->getPasswordForUser($user);
}
$user = $this->featureContext->getActualUsername($user);
} else {
$user = null;
$password = null;
}
return OcsApiHelper::sendRequest(
$this->featureContext->getBaseUrl(),
$user,
$password,
$verb,
$url,
$this->featureContext->getStepLineRef(),
$bodyArray,
$this->featureContext->getOcsApiVersion(),
$headers
);
}
/**
* @param string $verb
* @param string $url
* @param TableNode|null $body
*
* @return ResponseInterface
*/
public function adminSendsHttpMethodToOcsApiEndpointWithBody(
string $verb,
string $url,
?TableNode $body
): ResponseInterface {
$admin = $this->featureContext->getAdminUsername();
return $this->sendRequestToOcsEndpoint(
$admin,
$verb,
$url,
$body
);
}
/**
* @param string $verb
* @param string $url
* @param TableNode|null $body
*
* @return ResponseInterface
*/
public function theUserSendsToOcsApiEndpointWithBody(
string $verb,
string $url,
?TableNode $body = null
): ResponseInterface {
return $this->sendRequestToOcsEndpoint(
$this->featureContext->getCurrentUser(),
$verb,
$url,
$body
);
}
/**
* @When /^user "([^"]*)" sends HTTP method "([^"]*)" to OCS API endpoint "([^"]*)" with headers$/
*
* @param string $user
* @param string $verb
* @param string $url
* @param TableNode $headersTable
*
* @return void
* @throws Exception
*/
public function userSendsToOcsApiEndpointWithHeaders(
string $user,
string $verb,
string $url,
TableNode $headersTable
): void {
$user = $this->featureContext->getActualUsername($user);
$password = $this->featureContext->getPasswordForUser($user);
$this->featureContext->setResponse(
$this->sendRequestToOcsEndpoint(
$user,
$verb,
$url,
null,
$password,
$headersTable->getRowsHash()
)
);
}
/**
* @Then /^the OCS status code should be "([^"]*)"$/
*
* @param string $statusCode
* @param string $message
* @param ResponseInterface|null $response
*
* @return void
* @throws Exception
*/
public function theOCSStatusCodeShouldBe(
string $statusCode,
string $message = "",
?ResponseInterface $response = null
): void {
$statusCodes = explode(",", $statusCode);
$response = $response ?? $this->featureContext->getResponse();
$responseStatusCode = $this->getOCSResponseStatusCode(
$response
);
if (\is_array($statusCodes)) {
if ($message === "") {
$message = "OCS status code is not any of the expected values "
. \implode(",", $statusCodes) . " got " . $responseStatusCode;
}
Assert::assertContainsEquals(
$responseStatusCode,
$statusCodes,
$message
);
$this->featureContext->emptyLastOCSStatusCodesArray();
} else {
if ($message === "") {
$message = "OCS status code is not the expected value " . $statusCodes . " got " . $responseStatusCode;
}
Assert::assertEquals(
$statusCodes,
$responseStatusCode,
$message
);
}
}
/**
* @Then /^the OCS status code should be "([^"]*)" or "([^"]*)"$/
*
* @param string $statusCode1
* @param string $statusCode2
*
* @return void
* @throws Exception
*/
public function theOcsStatusCodeShouldBeOr(string $statusCode1, string $statusCode2): void {
$statusCodes = [$statusCode1,$statusCode1];
$response = $this->featureContext->getResponse();
$responseStatusCode = $this->getOCSResponseStatusCode(
$response
);
Assert::assertContainsEquals(
$responseStatusCode,
$statusCodes,
"OCS status code is not any of the expected values "
. \implode(",", $statusCodes) . " got " . $responseStatusCode
);
$this->featureContext->emptyLastOCSStatusCodesArray();
}
/**
* Check the text in an OCS status message
*
* @Then /^the OCS status message should be "([^"]*)"$/
* @Then /^the OCS status message should be "([^"]*)" in language "([^"]*)"$/
*
* @param string $statusMessage
* @param string|null $language
*
* @return void
*/
public function theOCSStatusMessageShouldBe(string $statusMessage, ?string $language = null): void {
$language = TranslationHelper::getLanguage($language);
$statusMessage = $this->getActualStatusMessage($statusMessage, $language);
Assert::assertEquals(
$statusMessage,
$this->getOCSResponseStatusMessage(
$this->featureContext->getResponse()
),
'Unexpected OCS status message :"' . $this->getOCSResponseStatusMessage(
$this->featureContext->getResponse()
) . '" in response'
);
}
/**
* Check the text in an OCS status message.
* Use this step form if the expected text contains double quotes,
* single quotes and other content that theOCSStatusMessageShouldBe()
* cannot handle.
*
* After the step, write the expected text in PyString form like:
*
* """
* File "abc.txt" can't be shared due to reason "xyz"
* """
*
* @Then /^the OCS status message should be:$/
*
* @param PyStringNode $statusMessage
*
* @return void
*/
public function theOCSStatusMessageShouldBePyString(
PyStringNode $statusMessage
): void {
Assert::assertEquals(
$statusMessage->getRaw(),
$this->getOCSResponseStatusMessage(
$this->featureContext->getResponse()
),
'Unexpected OCS status message: "' . $this->getOCSResponseStatusMessage(
$this->featureContext->getResponse()
) . '" in response'
);
}
/**
* Parses the xml answer to get ocs response which doesn't match with
* http one in v1 of the api.
*
* @param ResponseInterface $response
*
* @return string
* @throws Exception
*/
public function getOCSResponseStatusCode(ResponseInterface $response): string {
try {
$jsonResponse = $this->featureContext->getJsonDecodedResponseBodyContent($response);
} catch (JsonException $e) {
$jsonResponse = null;
}
if (\is_object($jsonResponse) && $jsonResponse->ocs->meta->statuscode) {
return (string) $jsonResponse->ocs->meta->statuscode;
}
// go to xml response when json response is null (it means not formatted and get status code)
$responseXmlObject = HttpRequestHelper::getResponseXml($response, __METHOD__);
if (isset($responseXmlObject->meta[0], $responseXmlObject->meta[0]->statuscode)) {
return (string) $responseXmlObject->meta[0]->statuscode;
}
Assert::fail("No OCS status code found in response");
}
/**
* Parses the xml answer to return data items from ocs response
*
* @param ResponseInterface $response
*
* @return SimpleXMLElement
* @throws Exception
*/
public function getOCSResponseData(ResponseInterface $response): SimpleXMLElement {
$responseXmlObject = HttpRequestHelper::getResponseXml($response, __METHOD__);
if (isset($responseXmlObject->data)) {
return $responseXmlObject->data;
}
Assert::fail("No OCS data items found in response");
}
/**
* Parses the xml answer to get ocs response message which doesn't match with
* http one in v1 of the api.
*
* @param ResponseInterface $response
*
* @return string
*/
public function getOCSResponseStatusMessage(ResponseInterface $response): string {
return (string) HttpRequestHelper::getResponseXml($response, __METHOD__)->meta[0]->message;
}
/**
* convert status message in the desired language
*
* @param string $statusMessage
* @param string|null $language
*
* @return string
*/
public function getActualStatusMessage(string $statusMessage, ?string $language = null): string {
if ($language !== null) {
$multiLingualMessage = \json_decode(
\file_get_contents(__DIR__ . "/../fixtures/multiLanguageErrors.json"),
true
);
if (isset($multiLingualMessage[$statusMessage][$language])) {
$statusMessage = $multiLingualMessage[$statusMessage][$language];
}
}
return $statusMessage;
}
/**
* check if the HTTP status code and the OCS status code indicate that the request was successful
* this function is aware of the currently used OCS version
*
* @param string|null $message
* @param ResponseInterface|null $response
*
* @return void
* @throws Exception
*/
public function assertOCSResponseIndicatesSuccess(
?string $message = "",
?ResponseInterface $response = null
): void {
$response = $response ?? $this->featureContext->getResponse();
$this->featureContext->theHTTPStatusCodeShouldBe('200', $message, $response);
if ($this->featureContext->getOcsApiVersion() === 1) {
$this->theOCSStatusCodeShouldBe('100', $message, $response);
} else {
$this->theOCSStatusCodeShouldBe('200', $message, $response);
}
}
/**
* 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');
}
}
@@ -0,0 +1,218 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2023 Sajan Gurung sajan@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 <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Exception\GuzzleException;
use TestHelpers\OcConfigHelper;
use TestHelpers\GraphHelper;
use PHPUnit\Framework\Assert;
/**
* steps needed to re-configure КуСфера server
*/
class OcConfigContext implements Context {
private array $enabledPermissionsRoles = [];
/**
* @return array
*/
public function getEnabledPermissionsRoles(): array {
return $this->enabledPermissionsRoles;
}
/**
* @param array $enabledPermissionsRoles
*
* @return void
*/
public function setEnabledPermissionsRoles(array $enabledPermissionsRoles): void {
$this->enabledPermissionsRoles = $enabledPermissionsRoles;
}
/**
* @Given async upload has been enabled with post-processing delayed to :delayTime seconds
*
* @param string $delayTime
*
* @return void
* @throws GuzzleException
*/
public function asyncUploadHasBeenEnabledWithDelayedPostProcessing(string $delayTime): void {
$envs = [
"OC_ASYNC_UPLOADS" => true,
"OC_EVENTS_ENABLE_TLS" => false,
"POSTPROCESSING_DELAY" => $delayTime . "s",
];
$response = OcConfigHelper::reConfigureOc($envs);
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to set async upload with delayed post processing"
);
OcConfigHelper::setPostProcessingDelay($delayTime);
}
/**
* @Given the config :configVariable has been set to :configValue
*
* @param string $configVariable
* @param string $configValue
*
* @return void
* @throws GuzzleException
*/
public function theConfigHasBeenSetTo(string $configVariable, string $configValue): void {
$envs = [
$configVariable => $configValue,
];
$response = OcConfigHelper::reConfigureOc($envs);
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to set config $configVariable=$configValue"
);
if ($configVariable === "POSTPROCESSING_DELAY") {
OcConfigHelper::setPostProcessingDelay($configValue);
}
}
/**
* @Given the administrator has enabled the permissions role :role
*
* @param string $role
*
* @return void
*/
public function theAdministratorHasEnabledTheRole(string $role): void {
$roleId = GraphHelper::getPermissionsRoleIdByName($role);
$defaultRoles = array_values(GraphHelper::DEFAULT_PERMISSIONS_ROLES);
if (!\in_array($role, $defaultRoles)) {
$defaultRoles[] = $roleId;
}
$envs = [
"GRAPH_AVAILABLE_ROLES" => implode(',', $defaultRoles),
];
$response = OcConfigHelper::reConfigureOc($envs);
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to enable role $role"
);
$this->setEnabledPermissionsRoles($defaultRoles);
}
/**
* @Given the administrator has disabled the permissions role :role
*
* @param string $role
*
* @return void
*/
public function theAdministratorHasDisabledThePermissionsRole(string $role): void {
$roleId = GraphHelper::getPermissionsRoleIdByName($role);
$availableRoles = $this->getEnabledPermissionsRoles();
if ($key = array_search($roleId, $availableRoles)) {
unset($availableRoles[$key]);
}
$envs = [
"GRAPH_AVAILABLE_ROLES" => implode(',', $availableRoles),
];
$response = OcConfigHelper::reConfigureOc($envs);
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to disable role $role"
);
$this->setEnabledPermissionsRoles($availableRoles);
}
/**
* @Given the config :configVariable has been set to path :path
*
* @param string $configVariable
* @param string $path
*
* @return void
* @throws GuzzleException
*/
public function theConfigHasBeenSetPathTo(string $configVariable, string $path): void {
if (\getenv('TEST_ROOT_PATH')) {
$path = \getenv('TEST_ROOT_PATH') . "/" . $path;
} else {
$path = \realpath(\dirname(__FILE__) . "/../../" . $path);
}
$response = OcConfigHelper::reConfigureOc(
[
$configVariable => $path
]
);
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to set config $configVariable=$path"
);
}
/**
* @Given the following configs have been set:
*
* @param TableNode $table
*
* @return void
* @throws GuzzleException
*/
public function theConfigHasBeenSetToValue(TableNode $table): void {
$envs = [];
foreach ($table->getHash() as $row) {
$envs[$row['config']] = $row['value'];
if ($row['config'] === "POSTPROCESSING_DELAY") {
OcConfigHelper::setPostProcessingDelay($row['value']);
}
}
$response = OcConfigHelper::reConfigureOc($envs);
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to set config"
);
}
/**
* @AfterScenario @env-config
*
* @return void
*/
public function rollbackOc(): void {
OcConfigHelper::setPostProcessingDelay('0');
$response = OcConfigHelper::rollbackOc();
Assert::assertEquals(
200,
$response->getStatusCode(),
"Failed to rollback КуСфера server. Check if КуСфера is started with ocwrapper."
);
}
}
@@ -0,0 +1,314 @@
<?php declare(strict_types=1);
/**
* @author Viktor Scharf <scharf.vi@gmail.com>
*
* @copyright Copyright (c) 2024, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\OcHelper;
use TestHelpers\OcmHelper;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
/**
* Acceptance test steps related to testing federation share(ocm) features
*/
class OcmContext implements Context {
private FeatureContext $featureContext;
private string $invitationToken;
/**
* 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 = BehatHelper::getContext($scope, $environment, 'FeatureContext');
}
/**
* @return string
* @throws Exception
*/
public function getLastFederatedInvitationToken(): string {
if (empty($this->invitationToken)) {
throw new Exception(__METHOD__ . " token not found");
}
return $this->invitationToken;
}
/**
* @param string $user
* @param string $email
* @param string $description
*
* @return ResponseInterface
* @throws GuzzleException
*/
public function createInvitation(string $user, $email = null, $description = null): ResponseInterface {
$response = OcmHelper::createInvitation(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$email,
$description
);
$responseData = \json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
if (isset($responseData["token"])) {
$this->invitationToken = $responseData["token"];
}
return $response;
}
/**
* @When :user creates the federation share invitation
* @When :user creates the federation share invitation with email :email and description :description
*
* @param string $user
* @param string $email
* @param string $description
*
* @return void
* @throws GuzzleException
*/
public function userCreatesTheFederationShareInvitation(string $user, $email = null, $description = null): void {
$this->featureContext->setResponse($this->createInvitation($user, $email, $description));
}
/**
* @Given :user has created the federation share invitation
* @Given :user has created the federation share invitation with email :email and description :description
*
* @param string $user
* @param string $email
* @param string $description
*
* @return void
* @throws GuzzleException
*/
public function userHasCreatedTheFederationShareInvitation(string $user, $email = null, $description = null): void {
$response = $this->createInvitation($user, $email, $description);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
}
/**
* @param string $user
* @param string $token
*
* @return ResponseInterface
* @throws GuzzleException
*/
public function acceptInvitation(string $user, ?string $token = null): ResponseInterface {
$providerDomain = $this->featureContext->getLocalBaseUrlWithoutScheme();
if ($this->featureContext->getCurrentServer() === "LOCAL") {
$providerDomain = $this->featureContext->getRemoteBaseUrlWithoutScheme();
}
return OcmHelper::acceptInvitation(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$token ? $token : $this->getLastFederatedInvitationToken(),
$providerDomain
);
}
/**
* @When :user accepts the last federation share invitation
* @When :user tries to accept the last federation share invitation
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userAcceptsTheLastFederationShareInvitation(string $user): void {
$this->featureContext->setResponse($this->acceptInvitation($user));
}
/**
* @When :user tries to accept the invitation with invalid token
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userTriesToAcceptInvitationWithInvalidToken(string $user): void {
$this->featureContext->setResponse($this->acceptInvitation($user, WebDavHelper::generateUUIDv4()));
}
/**
* @Given :user has accepted invitation
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userHasAcceptedTheLastFederationShareInvitation(string $user): void {
$response = $this->acceptInvitation($user);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
}
/**
* @param string $user
*
* @return ResponseInterface
* @throws GuzzleException
*/
public function findAcceptedUsers(string $user): ResponseInterface {
return OcmHelper::findAcceptedUsers(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user)
);
}
/**
* @When :user searches for accepted users
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userFindsAcceptedUsers(string $user): void {
$this->featureContext->setResponse($this->findAcceptedUsers($user));
}
/**
*
* @param string $user
* @param string $ocmUserName
*
* @return array
* @throws GuzzleException
*/
public function getAcceptedUserByName(string $user, string $ocmUserName): array {
$users = ($this->featureContext->getJsonDecodedResponse($this->findAcceptedUsers($user)));
foreach ($users as $user) {
if (strpos($user["display_name"], $ocmUserName) !== false) {
return $user;
}
}
throw new \Exception("Could not find user with name '{$ocmUserName}' in the accepted users list.");
}
/**
* @param string $user
*
* @return ResponseInterface
* @throws GuzzleException
*/
public function listInvitations(string $user): ResponseInterface {
return OcmHelper::listInvite(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user)
);
}
/**
* @When :user lists the created invitations
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userListsCreatedInvitations(string $user): void {
$this->featureContext->setResponse($this->listInvitations($user));
}
/**
* @When the user waits :number seconds for the invitation token to expire
*
* @param int $number
*
* @return void
* @throws GuzzleException
*/
public function theUserWaitsForTokenToExpire(int $number): void {
\sleep($number);
}
/**
* @When user :user deletes federated connection with user :ocmUser using the Graph API
*
* @param string $user
* @param string $ocmUser
*
* @return void
* @throws GuzzleException
*/
public function userDeletesFederatedConnectionWithUserUsingTheGraphApi(string $user, string $ocmUser): void {
$this->featureContext->setResponse($this->deleteConnection($user, $ocmUser));
}
/**
* @When user :user has deleted federated connection with user :ocmUser
*
* @param string $user
* @param string $ocmUser
*
* @return void
* @throws GuzzleException
*/
public function userHasDeletedFederatedConnectionWithUser(string $user, string $ocmUser): void {
$response = $this->deleteConnection($user, $ocmUser);
$this->featureContext->theHTTPStatusCodeShouldBe(
200,
"failed while deleting connection with user $ocmUser",
$response
);
}
/**
* @param string $user
* @param string $ocmUser
*
* @return ResponseInterface
* @throws GuzzleException
*/
public function deleteConnection(string $user, string $ocmUser): ResponseInterface {
$ocmUser = $this->getAcceptedUserByName($user, $ocmUser);
return OcmHelper::deleteConnection(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$ocmUser['user_id'],
$ocmUser['idp']
);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,281 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @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 <http://www.gnu.org/licenses/>
*
*/
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\WebDavHelper;
use TestHelpers\HttpRequestHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* context containing search related API steps
*/
class SearchContext implements Context {
private FeatureContext $featureContext;
/**
* @param string $user
* @param string $pattern
* @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,
?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
= "<?xml version='1.0' encoding='utf-8' ?>\n" .
" <oc:search-files xmlns:a='DAV:' xmlns:oc='http://owncloud.org/ns' >\n" .
" <oc:search>\n";
if ($scope !== null) {
if ($scopeType === "space") {
$spaceId = $this->featureContext->spacesContext->getSpaceIdByName($user, $scope);
$pattern .= " scope:$spaceId";
} else {
$resourceID = $this->featureContext->spacesContext->getResourceId(
$user,
$spaceName ?? "Personal",
$scope
);
$pattern .= " scope:$resourceID";
}
}
$body .= "<oc:pattern>$pattern</oc:pattern>\n";
if ($limit !== null) {
$body .= " <oc:limit>$limit</oc:limit>\n";
}
$body .= " </oc:search>\n";
if ($properties !== null) {
$propertiesRows = $properties->getRows();
$body .= " <a:prop>";
foreach ($propertiesRows as $property) {
$body .= "<$property[0]/>";
}
$body .= " </a:prop>";
}
$body .= " </oc:search-files>";
$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");
return HttpRequestHelper::sendRequest(
$fullUrl,
$this->featureContext->getStepLineRef(),
'REPORT',
$user,
$password,
null,
$body
);
}
/**
* @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 {
// NOTE: because indexing of newly uploaded files or directories with КуСфера is decoupled and occurs asynchronously
// short wait is necessary before searching
sleep(10);
$response = $this->searchFiles($user, $pattern, $limit, null, null, null, $properties);
$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
);
if (\is_object($fileResult)) {
$fileResultProperty = $fileResult->xpath("d:propstat//" . $property['name']);
} else {
throw new Exception("Expected fileResult to be an object, but found " . \gettype($fileResult));
}
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 <mark> tags
preg_match('/<mark>(.*?)<\/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) "([^"]*)" using the WebDAV API$/
* @When /^user "([^"]*)" searches for "([^"]*)" inside (folder) "([^"]*)" in space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $pattern
* @param string $scopeType
* @param string $scope
* @param string|null $spaceName
*
* @return void
* @throws Exception|GuzzleException
*/
public function userSearchesInsideFolderOrSpaceUsingWebDavAPI(
string $user,
string $pattern,
string $scopeType,
string $scope,
?string $spaceName = null,
): void {
// NOTE: since indexing of newly uploaded files or directories with КуСфера is decoupled and occurs asynchronously,
// a short wait is necessary before searching
sleep(5);
$response = $this-> searchFiles($user, $pattern, null, $scopeType, $scope, $spaceName);
$this->featureContext->setResponse($response);
}
}
@@ -0,0 +1,553 @@
<?php
declare(strict_types=1);
/**
* @author Viktor Scharf <v.scharf@owncloud.com>
* @copyright Copyright (c) 2022 Viktor Scharf v.scharf@owncloud.com
*/
use Behat\Behat\Context\Context;
use GuzzleHttp\Exception\GuzzleException;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\HttpRequestHelper;
use TestHelpers\SettingsHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* Context for the TUS-specific steps using the Graph API
*/
class SettingsContext implements Context {
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 = BehatHelper::getContext($scope, $environment, 'FeatureContext');
}
/**
* @param string $user
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function getRoles(string $user): ResponseInterface {
return SettingsHelper::getRolesList(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getStepLineRef()
);
}
/**
* @When /^user "([^"]*)" tries to get all existing roles using the settings API$/
*
* @param string $user
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function getAllExistingRoles(string $user): void {
$response = $this->getRoles($user);
$this->featureContext->setResponse($response);
}
/**
* @param string $user
* @param string $userId
* @param string $roleId
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function assignRoleToUser(string $user, string $userId, string $roleId): ResponseInterface {
return SettingsHelper::assignRoleToUser(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$userId,
$roleId,
$this->featureContext->getStepLineRef(),
);
}
/**
* @param string $user
* @param string $userId
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function getAssignmentsList(string $user, string $userId): ResponseInterface {
return SettingsHelper::getAssignmentsList(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$userId,
$this->featureContext->getStepLineRef(),
);
}
/**
* @Given /^the administrator has given "([^"]*)" the role "([^"]*)" using the settings api$/
*
* @param string $user
* @param string $role
*
* @return void
*
* @throws Exception
*/
public function theAdministratorHasGivenUserTheRole(string $user, string $role): void {
$admin = $this->featureContext->getAdminUserName();
$roleId = $this->getRoleIdByRoleName($admin, $role);
$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id') ?: $user;
$response = $this->assignRoleToUser($admin, $userId, $roleId);
$this->featureContext->theHTTPStatusCodeShouldBe(
201,
"Expected response status code should be 201",
$response
);
}
/**
* @When user :assigner assigns the role :role to user :assignee using the settings API
*
* @param string $assigner
* @param string $role
* @param string $assignee
*
* @return void
*
* @throws Exception
*/
public function userAssignsTheRoleToUserUsingTheSettingsApi(
string $assigner,
string $role,
string $assignee
): void {
$response = $this->assignRoleToUser(
$assigner,
$this->featureContext->getAttributeOfCreatedUser($assignee, 'id'),
$this->getRoleIdByRoleName($assigner, $role)
);
$this->featureContext->setResponse($response);
}
/**
* @param string $user
* @param string $role
*
* @return string
*/
public function getRoleIdByRoleName(string $user, string $role): string {
// Sometimes the response body is not complete and results invalid json.
// So we try again until we get a valid json.
$retried = 0;
do {
$response = $this->getRoles($user);
$this->featureContext->theHTTPStatusCodeShouldBe(
201,
"Expected response status code should be 201",
$response
);
$rawBody = $response->getBody()->getContents();
try {
$decodedBody = \json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
$tryAgain = false;
} catch (Exception $e) {
$tryAgain = $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
if (!$tryAgain) {
throw $e;
}
}
if ($tryAgain) {
$retried += 1;
echo "Invalid json body, retrying ($retried)...\n";
// wait 500ms and try again
\usleep(500 * 1000);
}
} while ($tryAgain);
Assert::assertArrayHasKey(
'bundles',
$decodedBody,
__METHOD__ . " could not find bundles in body"
);
$bundles = $decodedBody["bundles"];
$roleToAssign = "";
foreach ($bundles as $value) {
// find the selected role
if ($value["displayName"] === $role) {
$roleToAssign = $value;
break;
}
}
Assert::assertNotEmpty($roleToAssign, "The selected role $role could not be found");
return $roleToAssign["id"];
}
/**
* @When /^user "([^"]*)" changes his own role to "([^"]*)"$/
*
* @param string $user
* @param string $role
*
* @return void
* @throws GuzzleException
*/
public function userChangeOwnRole(string $user, string $role): void {
// we assume that the user knows uuid role.
$roleId = $this->getRoleIdByRoleName($this->featureContext->getAdminUserName(), $role);
$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$response = $this->assignRoleToUser($user, $userId, $roleId);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" changes the role "([^"]*)" for user "([^"]*)"$/
*
* @param string $user
* @param string $role
* @param string $assignedUser
*
* @return void
* @throws GuzzleException
*/
public function userChangeRoleAnotherUser(string $user, string $role, string $assignedUser): void {
// we assume that the user knows uuid role.
$roleId = $this->getRoleIdByRoleName($this->featureContext->getAdminUserName(), $role);
$userId = $this->featureContext->getAttributeOfCreatedUser($assignedUser, 'id');
$response = $this->assignRoleToUser($user, $userId, $roleId);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" tries to get list of assignment using the settings API$/
*
* @param string $user
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function userGetAssignmentsList(string $user): void {
$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$this->featureContext->setResponse($this->getAssignmentsList($user, $userId));
}
/**
* @Then /^user "([^"]*)" should have the role "([^"]*)"$/
*
* @param string $user
* @param string $role
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function userShouldHaveRole(string $user, string $role): void {
$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$response = $this->getAssignmentsList($this->featureContext->getAdminUserName(), $userId);
$assignmentResponse = $this->featureContext->getJsonDecodedResponseBodyContent($response);
if (isset($assignmentResponse->assignments[0]->roleId)) {
$actualRoleId = $assignmentResponse->assignments[0]->roleId;
Assert::assertEquals(
$this->getRoleIdByRoleName($this->featureContext->getAdminUserName(), $role),
$actualRoleId,
"user $user has no role $role"
);
} else {
Assert::fail("Response should contain user role but not found.\n" . json_encode($assignmentResponse));
}
}
/**
* @Then /^the setting API response should have the role "([^"]*)"$/
*
* @param string $role
*
* @return void
*
* @throws Exception
*/
public function theSettingApiResponseShouldHaveTheRole(string $role): void {
$assignmentRoleId = $this->featureContext->getJsonDecodedResponse(
$this->featureContext->getResponse()
)["assignments"][0]["roleId"];
Assert::assertEquals(
$this->getRoleIdByRoleName($this->featureContext->getAdminUserName(), $role),
$assignmentRoleId,
"user has no role $role"
);
}
/**
* @param string $user
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function sendRequestGetBundlesList(string $user): ResponseInterface {
return SettingsHelper::getBundlesList(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getStepLineRef(),
);
}
/**
* @param string $user
* @param string $bundleName
*
* @return array
*
* @throws GuzzleException
* @throws Exception
*/
public function getBundleByName(string $user, string $bundleName): array {
return SettingsHelper::getBundleByName(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$bundleName,
$this->featureContext->getStepLineRef()
);
}
/**
* @param string $user
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function sendRequestGetSettingsValuesList(string $user, array $headers = null): ResponseInterface {
return SettingsHelper::getValuesList(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getStepLineRef(),
$headers
);
}
/**
* @When /^user "([^"]*)" lists values-list with headers using the Settings API$/
*
* @param string $user
* @param TableNode $headersTable
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function theUserListsAllValuesListWithHeadersUsingSettingsApi(string $user, TableNode $headersTable): void {
$this->featureContext->verifyTableNodeColumns(
$headersTable,
['header', 'value']
);
$headers = [];
foreach ($headersTable as $row) {
$headers[$row['header']] = $row ['value'];
}
$this->featureContext->setResponse($this->sendRequestGetSettingsValuesList($user, $headers));
}
/**
* @param string $user
* @param string $language
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function sendRequestToSwitchSystemLanguage(string $user, string $language): ResponseInterface {
$profileBundlesList = $this->getBundleByName($user, "Profile");
Assert::assertNotEmpty($profileBundlesList, "bundles list is empty");
$settingId = '';
foreach ($profileBundlesList["settings"] as $value) {
if ($value["name"] === "language") {
$settingId = $value["id"];
break;
}
}
Assert::assertNotEmpty($settingId, "settingId is empty");
$userId = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$body = json_encode(
[
"value" => [
"account_uuid" => "me",
"bundleId" => $profileBundlesList["id"],
"id" => $userId,
"listValue" => [
"values" => [
[
"stringValue" => $language
]
]
],
"resource" => [
"type" => "TYPE_USER"
],
"settingId" => $settingId
]
],
JSON_THROW_ON_ERROR
);
return SettingsHelper::updateSettings(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$body,
$this->featureContext->getStepLineRef()
);
}
/**
* @Given /^user "([^"]*)" has switched the system language to "([^"]*)" using the settings API$/
*
* @param string $user
* @param string $language
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function theUserHasSwitchedSystemLanguage(string $user, string $language): void {
$response = $this->sendRequestToSwitchSystemLanguage($user, $language);
$this->featureContext->theHTTPStatusCodeShouldBe(
201,
"Expected response status code should be 201",
$response
);
}
/**
* @When user :user switches the system language to :language using the settings API
*
* @param string $user
* @param string $language
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userSwitchesTheSystemLanguageUsingTheSettingsApi(string $user, string $language): void {
$response = $this->sendRequestToSwitchSystemLanguage($user, $language);
$this->featureContext->setResponse($response);
}
/**
* @param string $user
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public function sendRequestToDisableAutoAccepting(string $user): ResponseInterface {
$body = json_encode(
[
"value" => [
"account_uuid" => "me",
"bundleId" => "2a506de7-99bd-4f0d-994e-c38e72c28fd9",
"settingId" => "ec3ed4a3-3946-4efc-8f9f-76d38b12d3a9",
"resource" => [
"type" => "TYPE_USER"
],
"boolValue" => false
]
],
JSON_THROW_ON_ERROR
);
return SettingsHelper::updateSettings(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$body,
$this->featureContext->getStepLineRef()
);
}
/**
* @Given user :user has disabled auto-accepting
* @Given user :user has disabled the auto-sync share
*
* @param string $user
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function theUserHasDisabledAutoAccepting(string $user): void {
$response = $this->sendRequestToDisableAutoAccepting($user);
$this->featureContext->theHTTPStatusCodeShouldBe(
201,
"Expected response status code should be 201",
$response
);
$this->featureContext->rememberUserAutoSyncSetting($user, false);
}
/**
* @When user :user disables the auto-sync share using the settings API
*
* @param string $user
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userDisablesAutoAcceptingUsingSettingsApi(string $user): void {
$response = $this->sendRequestToDisableAutoAccepting($user);
$this->featureContext->setResponse($response);
$this->featureContext->rememberUserAutoSyncSetting($user, false);
}
}
@@ -0,0 +1,221 @@
<?php declare(strict_types=1);
/**
* @author Joas Schilling <coding@schilljs.com>
* @author Sergio Bertolin <sbertolin@owncloud.com>
* @author Phillip Davis <phil@jankaritech.com>
* @copyright Copyright (c) 2018, ownCloud GmbH
*
* 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 <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use Psr\Http\Message\ResponseInterface;
use PHPUnit\Framework\Assert;
use TestHelpers\BehatHelper;
use TestHelpers\HttpRequestHelper;
require_once 'bootstrap.php';
/**
* Sharees context.
*/
class ShareesContext implements Context {
private FeatureContext $featureContext;
private OCSContext $ocsContext;
/**
* @When /^user "([^"]*)" gets the sharees using the sharing API with parameters$/
*
* @param string $user
* @param TableNode $body
*
* @return void
*/
public function userGetsTheShareesWithParameters(string $user, TableNode $body): void {
$this->featureContext->setResponse(
$this->getShareesWithParameters(
$user,
$body
)
);
}
/**
* @Then /^the "([^"]*)" sharees returned should be$/
*
* @param string $shareeType
* @param TableNode $shareesList
*
* @return void
* @throws Exception
*/
public function theShareesReturnedShouldBe(string $shareeType, TableNode $shareesList): void {
$this->featureContext->verifyTableNodeColumnsCount($shareesList, 4);
$sharees = $shareesList->getRows();
$respondedArray = $this->getArrayOfShareesResponded(
$this->featureContext->getResponse(),
$shareeType
);
Assert::assertEquals(
$sharees,
$respondedArray,
"Returned sharees do not match the expected ones. See the differences below."
);
}
/**
* @Then /^the "([^"]*)" sharees returned should include$/
*
* @param string $shareeType
* @param TableNode $shareesList
*
* @return void
* @throws Exception
*/
public function theShareesReturnedShouldInclude(string $shareeType, TableNode $shareesList): void {
$this->featureContext->verifyTableNodeColumnsCount($shareesList, 3);
$sharees = $shareesList->getRows();
$respondedArray = $this->getArrayOfShareesResponded(
$this->featureContext->getResponse(),
$shareeType
);
foreach ($sharees as $sharee) {
Assert::assertContains(
$sharee,
$respondedArray,
"Returned sharees do not match the expected ones. See the differences below."
);
}
}
/**
* @Then /^the "([^"]*)" sharees returned should be empty$/
*
* @param string $shareeType
*
* @return void
*/
public function theShareesReturnedShouldBeEmpty(string $shareeType): void {
$respondedArray = $this->getArrayOfShareesResponded(
$this->featureContext->getResponse(),
$shareeType
);
if (isset($respondedArray[0])) {
// [0] is display name and [2] is user or group id
$firstEntry = $respondedArray[0][0] . " (" . $respondedArray[0][2] . ")";
} else {
$firstEntry = "";
}
Assert::assertEmpty(
$respondedArray,
"'$shareeType' array should be empty, but it starts with $firstEntry"
);
}
/**
* @param ResponseInterface $response
* @param string $shareeType
*
* @return array
* @throws Exception
*/
public function getArrayOfShareesResponded(
ResponseInterface $response,
string $shareeType
): array {
$elements = HttpRequestHelper::getResponseXml($response, __METHOD__)->data;
$elements = \json_decode(\json_encode($elements), true);
if (\strpos($shareeType, 'exact ') === 0) {
$elements = $elements['exact'];
$shareeType = \substr($shareeType, 6);
}
Assert::assertArrayHasKey(
$shareeType,
$elements,
__METHOD__ . " The sharees response does not have key '$shareeType'"
);
$sharees = [];
foreach ($elements[$shareeType] as $element) {
if (\is_int(\key($element))) {
// this is a list of elements instead of just one item,
// so return the list
foreach ($element as $innerItem) {
$sharees[] = [
$innerItem['label'],
$innerItem['value']['shareType'],
$innerItem['value']['shareWith'],
$innerItem['value']['shareWithAdditionalInfo']
];
}
} else {
$sharees[] = [
$element['label'],
$element['value']['shareType'],
$element['value']['shareWith'],
$element['value']['shareWithAdditionalInfo']
];
}
}
return $sharees;
}
/**
* @param string $user
* @param TableNode $body
*
* @return ResponseInterface
*/
public function getShareesWithParameters(string $user, TableNode $body): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$url = '/apps/files_sharing/api/v1/sharees';
$this->featureContext->verifyTableNodeColumnsCount($body, 2);
$parameters = [];
foreach ($body->getRowsHash() as $key => $value) {
$parameters[] = "$key=$value";
}
if (!empty($parameters)) {
$url .= '?' . \implode('&', $parameters);
}
return $this->ocsContext->sendRequestToOcsEndpoint(
$user,
'GET',
$url
);
}
/**
* 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');
$this->ocsContext = BehatHelper::getContext($scope, $environment, 'OCSContext');
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,460 @@
<?php
declare(strict_types=1);
/**
* @author Viktor Scharf <v.scharf@owncloud.com>
* @copyright Copyright (c) 2022 Viktor Scharf v.scharf@owncloud.com
*/
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 TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* Context for the TUS-specific steps using the Graph API
*/
class SpacesTUSContext implements Context {
private FeatureContext $featureContext;
private TUSContext $tusContext;
private SpacesContext $spacesContext;
/**
* 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 = BehatHelper::getContext($scope, $environment, 'FeatureContext');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
$this->tusContext = BehatHelper::getContext($scope, $environment, 'TUSContext');
}
/**
* @Given /^user "([^"]*)" has uploaded a file from "([^"]*)" to "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $spaceName
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userHasUploadedFileViaTusInSpace(
string $user,
string $source,
string $destination,
string $spaceName
): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$this->tusContext->uploadFileUsingTus($user, $source, $destination, $spaceId);
$this->featureContext->setLastUploadDeleteTime(\time());
}
/**
* @When /^user "([^"]*)" uploads a file from "([^"]*)" to "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $spaceName
*
* @return void
* @throws Exception
* @throws GuzzleException
*/
public function userUploadsAFileViaTusInsideOfTheSpaceUsingTheWebdavApi(
string $user,
string $source,
string $destination,
string $spaceName
): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$this->tusContext->uploadFileUsingTus($user, $source, $destination, $spaceId);
$this->featureContext->setLastUploadDeleteTime(\time());
}
/**
* @Given user :user has created a new TUS resource in the space :spaceName with the following headers:
*
* @param string $user
* @param string $spaceName
* @param TableNode $headers
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userHasCreatedANewTusResourceForTheSpaceUsingTheWebdavApiWithTheseHeaders(
string $user,
string $spaceName,
TableNode $headers
): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$response = $this->tusContext->createNewTUSResourceWithHeaders($user, $headers, '', $spaceId);
$this->featureContext->theHTTPStatusCodeShouldBe(201, "Expected response status code should be 201", $response);
}
/**
* @When user :user creates a new TUS resource for the space :spaceName with content :content using the WebDAV API with these headers:
*
* @param string $user
* @param string $spaceName
* @param string $content
* @param TableNode $headers
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userCreatesANewTusResourceForTheSpaceUsingTheWebdavApiWithTheseHeaders(
string $user,
string $spaceName,
string $content,
TableNode $headers
): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$response = $this->tusContext->createNewTUSResourceWithHeaders($user, $headers, $content, $spaceId);
$this->featureContext->setResponse($response);
}
/**
* Uploads a file with content to the specified space using the TUS protocol via the WebDAV API.
*
* @param string $user
* @param string $content
* @param string $resource
* @param string $spaceName
*
* @return void
* @throws Exception|GuzzleException
*/
private function uploadFileViaTus(string $user, string $content, string $resource, string $spaceName): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$tmpFile = $this->tusContext->writeDataToTempFile($content);
try {
$this->tusContext->uploadFileUsingTus(
$user,
\basename($tmpFile),
$resource,
$spaceId
);
$this->featureContext->setLastUploadDeleteTime(\time());
} catch (Exception $e) {
Assert::assertStringContainsString('Unable to create resource', (string)$e);
}
\unlink($tmpFile);
}
/**
* @When /^user "([^"]*)" uploads a file with content "([^"]*)" to "([^"]*)" inside federated share "([^"]*)" via TUS using the WebDAV API$/
*
* @param string $user
* @param string $content
* @param string $file
* @param string $destination
*
* @return void
* @throws Exception|GuzzleException
*/
public function userUploadsAFileWithContentToInsideFederatedShareViaTusUsingTheWebdavApi(
string $user,
string $content,
string $file,
string $destination
): void {
$remoteItemId = $this->spacesContext->getSharesRemoteItemId($user, $destination);
$remoteItemId = \rawurlencode($remoteItemId);
$tmpFile = $this->tusContext->writeDataToTempFile($content);
$this->tusContext->uploadFileUsingTus(
$user,
\basename($tmpFile),
$file,
$remoteItemId
);
$this->featureContext->setLastUploadDeleteTime(\time());
\unlink($tmpFile);
}
/**
* @When /^user "([^"]*)" uploads a file with content "([^"]*)" to "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $content
* @param string $resource
* @param string $spaceName
*
* @return void
* @throws Exception|GuzzleException
*/
public function userUploadsAFileWithContentToViaTusInsideOfTheSpaceUsingTheWebdavApi(
string $user,
string $content,
string $resource,
string $spaceName
): void {
$this->uploadFileViaTus($user, $content, $resource, $spaceName);
}
/**
* @Given /^user "([^"]*)" has uploaded a file with content "([^"]*)" to "([^"]*)" via TUS inside of the space "([^"]*)"$/
*
* @param string $user
* @param string $content
* @param string $resource
* @param string $spaceName
*
* @return void
* @throws Exception|GuzzleException
*/
public function userHasUploadedAFileWithContentToViaTusInsideOfTheSpace(
string $user,
string $content,
string $resource,
string $spaceName
): void {
$this->uploadFileViaTus($user, $content, $resource, $spaceName);
}
/**
* @When /^user "([^"]*)" uploads a file "([^"]*)" to "([^"]*)" with mtime "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $mtime Time in human-readable format is taken as input which is converted into milliseconds that is used by API
* @param string $spaceName
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userUploadsAFileToWithMtimeViaTusInsideOfTheSpaceUsingTheWebdavApi(
string $user,
string $source,
string $destination,
string $mtime,
string $spaceName
): void {
switch ($mtime) {
case "today":
$mtime = date('Y-m-d', strtotime('today'));
break;
case "yesterday":
$mtime = date('Y-m-d', strtotime('yesterday'));
break;
case "lastWeek":
$mtime = date('Y-m-d', strtotime('-7 days'));
break;
case "lastMonth":
$mtime = date('Y-m-d', strtotime('first day of previous month'));
break;
case "lastYear":
$mtime = date('Y-m' . '-01', strtotime('-1 year'));
break;
default:
}
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$mtime = new DateTime($mtime);
$mtime = $mtime->format('U');
$user = $this->featureContext->getActualUsername($user);
$this->tusContext->uploadFileUsingTus(
$user,
$source,
$destination,
$spaceId,
['mtime' => $mtime]
);
$this->featureContext->setLastUploadDeleteTime(\time());
}
/**
* @Given /^user "([^"]*)" has uploaded file with checksum "([^"]*)" to the last created TUS Location with offset "([^"]*)" and content "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $checksum
* @param string $offset
* @param string $content
* @param string $spaceName
*
* @return void
* @throws Exception|GuzzleException
* @codingStandardsIgnoreStart
*/
public function userHasUploadedFileWithChecksumToTheLastCreatedTusLocationWithOffsetAndContentViaTusInsideOfTheSpaceUsingTheWebdavApi(
// @codingStandardsIgnoreEnd
string $user,
string $checksum,
string $offset,
string $content,
string $spaceName
): void {
$resourceLocation = $this->tusContext->getLastTusResourceLocation();
$response = $this->tusContext->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $content, $checksum);
$this->featureContext->theHTTPStatusCodeShouldBe(204, "", $response);
}
/**
* @When /^user "([^"]*)" uploads file with checksum "([^"]*)" to the last created TUS Location with offset "([^"]*)" and content "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $checksum
* @param string $offset
* @param string $content
* @param string $spaceName
*
* @return void
* @throws Exception|GuzzleException
* @codingStandardsIgnoreStart
*/
public function userUploadsFileWithChecksumToTheLastCreatedTusLocationWithOffsetAndContentViaTusInsideOfTheSpaceUsingTheWebdavApi(
// @codingStandardsIgnoreEnd
string $user,
string $checksum,
string $offset,
string $content,
string $spaceName
): void {
$resourceLocation = $this->tusContext->getLastTusResourceLocation();
$response = $this->tusContext->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $content, $checksum);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" sends a chunk to the last created TUS Location with offset "([^"]*)" and data "([^"]*)" with checksum "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API$/
*
* @param string $user
* @param string $offset
* @param string $data
* @param string $checksum
* @param string $spaceName
*
* @return void
* @throws Exception|GuzzleException
* @codingStandardsIgnoreStart
*/
public function userSendsAChunkToTheLastCreatedTusLocationWithOffsetAndDataWithChecksumViaTusInsideOfTheSpaceUsingTheWebdavApi(
// @codingStandardsIgnoreEnd
string $user,
string $offset,
string $data,
string $checksum,
string $spaceName
): void {
$resourceLocation = $this->tusContext->getLastTusResourceLocation();
$response = $this->tusContext->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data, $checksum);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" sends a chunk to the last created TUS Location with data "([^"]*)" with the following headers:$/
*
* @param string $user
* @param string $data
* @param TableNode $headers
*
* @return void
* @throws Exception|GuzzleException
*/
public function userSendsAChunkToTheLastCreatedTusLocationWithDataInsideOfTheSpaceWithHeaders(
string $user,
string $data,
TableNode $headers
): void {
$rows = $headers->getRowsHash();
$resourceLocation = $this->tusContext->getLastTusResourceLocation();
$response = $this->tusContext->uploadChunkToTUSLocation(
$user,
$resourceLocation,
$rows['Upload-Offset'],
$data,
$rows['Upload-Checksum'],
['Origin' => $rows['Origin']]
);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" overwrites recently shared file with offset "([^"]*)" and data "([^"]*)" with checksum "([^"]*)" via TUS inside of the space "([^"]*)" using the WebDAV API with these headers:$/
*
* @param string $user
* @param string $offset
* @param string $data
* @param string $checksum
* @param string $spaceName
* @param TableNode $headers
*
* @return void
* @throws GuzzleException
* @codingStandardsIgnoreStart
*/
public function userOverwritesRecentlySharedFileWithOffsetAndDataWithChecksumViaTusInsideOfTheSpaceUsingTheWebdavApiWithTheseHeaders(
// @codingStandardsIgnoreEnd
string $user,
string $offset,
string $data,
string $checksum,
string $spaceName,
TableNode $headers
): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$createResponse = $this->tusContext->createNewTUSResource($user, $headers, $spaceId);
$this->featureContext->theHTTPStatusCodeShouldBe(201, "", $createResponse);
$resourceLocation = $this->tusContext->getLastTusResourceLocation();
$response = $this->tusContext->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data, $checksum);
$this->featureContext->setResponse($response);
}
/**
* @Then /^as "([^"]*)" the mtime of the file "([^"]*)" in space "([^"]*)" should be "([^"]*)"$/
*
* @param string $user
* @param string $resource
* @param string $spaceName
* @param string $mtime
*
* @return void
* @throws Exception|GuzzleException
*/
public function theMtimeOfTheFileInSpaceShouldBe(
string $user,
string $resource,
string $spaceName,
string $mtime
): void {
$spaceId = $this->spacesContext->getSpaceIdByName($user, $spaceName);
$mtime = new DateTime($mtime);
Assert::assertEquals(
$mtime->format('U'),
WebDavHelper::getMtimeOfResource(
$this->featureContext->getActualUsername($user),
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getBaseUrl(),
$resource,
$this->featureContext->getStepLineRef(),
$this->featureContext->getDavPathVersion(),
$spaceId,
)
);
}
}
@@ -0,0 +1,746 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
*
* @copyright Copyright (c) 2020, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Exception\GuzzleException;
use TusPhp\Exception\ConnectionException;
use TusPhp\Exception\TusException;
use TusPhp\Tus\Client;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\HttpRequestHelper;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
use TestHelpers\UploadHelper;
require_once 'bootstrap.php';
/**
* TUS related test steps
*/
class TUSContext implements Context {
private FeatureContext $featureContext;
private array $tusResourceLocations = [];
/**
* @param string $filenameHash
* @param string $location
*
* @return void
*/
public function saveTusResourceLocation(string $filenameHash, string $location): void {
$this->tusResourceLocations[$filenameHash][] = $location;
}
/**
* @param string $filenameHash
* @param int|null $index
*
* @return string
*/
public function getTusResourceLocation(string $filenameHash, ?int $index = null): string {
if ($index === null) {
// get the last one
$index = \count($this->tusResourceLocations[$filenameHash]) - 1;
}
return $this->tusResourceLocations[$filenameHash][$index];
}
/**
* @return string
*/
public function getLastTusResourceLocation(): string {
$lastKey = \array_key_last($this->tusResourceLocations);
$index = \count($this->tusResourceLocations[$lastKey]) - 1;
return $this->tusResourceLocations[$lastKey][$index];
}
/**
* @param string $uploadMetadata
*
* @return string
*/
public function parseFilenameHash(string $uploadMetadata): string {
$filenameHash = \explode("filename ", $uploadMetadata)[1] ?? '';
return \explode(" ", $filenameHash, 2)[0];
}
/**
* @param string $user
* @param TableNode $headersTable
* @param string $content
* @param string|null $spaceId
*
* @return ResponseInterface
*
* @throws Exception
* @throws GuzzleException
*/
public function createNewTUSResourceWithHeaders(
string $user,
TableNode $headersTable,
string $content = '',
?string $spaceId = null
): ResponseInterface {
$this->featureContext->verifyTableNodeColumnsCount($headersTable, 2);
$user = $this->featureContext->getActualUsername($user);
$password = $this->featureContext->getUserPassword($user);
$headers = $headersTable->getRowsHash();
$response = $this->featureContext->makeDavRequest(
$user,
"POST",
null,
$headers,
$content,
$spaceId,
"files",
null,
false,
$password
);
$locationHeader = $response->getHeader('Location');
if (\sizeof($locationHeader) > 0) {
$filenameHash = $this->parseFilenameHash($headers['Upload-Metadata']);
$this->saveTusResourceLocation($filenameHash, $locationHeader[0]);
}
return $response;
}
/**
* @When user :user creates a new TUS resource on the WebDAV API with these headers:
*
* @param string $user
* @param TableNode $headers
* @param string $content
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userCreateNewTUSResourceWithHeaders(string $user, TableNode $headers, string $content = ''): void {
$response = $this->createNewTUSResourceWithHeaders($user, $headers, $content);
$this->featureContext->setResponse($response);
}
/**
* @Given user :user has created a new TUS resource on the WebDAV API with these headers:
*
* @param string $user
* @param TableNode $headers Tus-Resumable: 1.0.0 header is added automatically
*
* @return void
*
* @throws Exception
* @throws GuzzleException
*/
public function userHasCreatedNewTUSResourceWithHeaders(string $user, TableNode $headers): void {
$response = $this->createNewTUSResource($user, $headers);
$this->featureContext->theHTTPStatusCodeShouldBe(201, "", $response);
}
/**
* @param string $user
* @param TableNode $headers
* @param string|null $spaceId
*
* @return ResponseInterface
*/
public function createNewTUSResource(string $user, TableNode $headers, ?string $spaceId = null): ResponseInterface {
$rows = $headers->getRows();
$rows[] = ['Tus-Resumable', '1.0.0'];
return $this->createNewTUSResourceWithHeaders($user, new TableNode($rows), '', $spaceId);
}
/**
* @param string $user
* @param string $resourceLocation
* @param string $offset
* @param string $data
* @param string $checksum
* @param array|null $extraHeaders
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws JsonException
*/
public function uploadChunkToTUSLocation(
string $user,
string $resourceLocation,
string $offset,
string $data,
string $checksum = '',
?array $extraHeaders = null
): ResponseInterface {
$user = $this->featureContext->getActualUsername($user);
$password = $this->featureContext->getUserPassword($user);
$headers = [
'Content-Type' => 'application/offset+octet-stream',
'Tus-Resumable' => '1.0.0',
'Upload-Checksum' => $checksum,
'Upload-Offset' => $offset
];
$headers = empty($extraHeaders) ? $headers : array_merge($headers, $extraHeaders);
return HttpRequestHelper::sendRequest(
$resourceLocation,
$this->featureContext->getStepLineRef(),
'PATCH',
$user,
$password,
$headers,
$data
);
}
/**
* @When user :user sends a chunk to the last created TUS Location with offset :offset and data :data with retry on offset mismatch using the WebDAV API
*
* @param string $user
* @param string $offset
* @param string $data
*
* @return void
*
* @throws GuzzleException
* @throws JsonException
*/
public function userSendsAChunkToTUSLocationWithOffsetAndDataWithRetryOnOffsetMismatch(
string $user,
string $offset,
string $data,
): void {
$resourceLocation = $this->getLastTusResourceLocation();
$retried = 0;
do {
$tryAgain = false;
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data);
// retry on 409 Conflict (Offset mismatch during TUS upload)
if ($response->getStatusCode() === 409) {
$tryAgain = true;
}
$tryAgain = $tryAgain && $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
if ($tryAgain) {
$retried += 1;
echo "Offset mismatch during TUS upload, retrying ($retried)...\n";
// wait 1s and try again
\sleep(1);
}
} while ($tryAgain);
$this->featureContext->setResponse($response);
}
/**
* @When user :user sends a chunk to the last created TUS Location with offset :offset and data :data using the WebDAV API
*
* @param string $user
* @param string $offset
* @param string $data
*
* @return void
*
* @throws GuzzleException
* @throws JsonException
*/
public function userSendsAChunkToTUSLocationWithOffsetAndData(string $user, string $offset, string $data): void {
$resourceLocation = $this->getLastTusResourceLocation();
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data);
$this->featureContext->setResponse($response);
}
/**
* @When user :user uploads file :source to :destination using the TUS protocol on the WebDAV API
*
* @param string|null $user
* @param string $source
* @param string $destination
* @param array $uploadMetadata array of metadata to be placed in the
* `Upload-Metadata` header.
* see https://tus.io/protocols/resumable-upload.html#upload-metadata
* Don't Base64 encode the value.
* @param int $noOfChunks
* @param int|null $bytes
* @param string $checksum
*
* @return void
* @throws ConnectionException
* @throws GuzzleException
* @throws JsonException
* @throws ReflectionException
* @throws TusException
*/
public function userUploadsUsingTusAFileTo(
?string $user,
string $source,
string $destination,
array $uploadMetadata = [],
int $noOfChunks = 1,
?int $bytes = null,
string $checksum = ''
): void {
$this->uploadFileUsingTus($user, $source, $destination, null, $uploadMetadata, $noOfChunks, $bytes, $checksum);
$this->featureContext->setLastUploadDeleteTime(\time());
}
/**
* @param string $user
* @param string $source
* @param string $destination
* @param string|null $spaceId
* @param array $uploadMetadata
* @param integer $noOfChunks
* @param integer $bytes
* @param string $checksum
*
* @return void
*/
public function uploadFileUsingTus(
?string $user,
string $source,
string $destination,
?string $spaceId = null,
array $uploadMetadata = [],
int $noOfChunks = 1,
?int $bytes = null,
string $checksum = ''
) {
$user = $this->featureContext->getActualUsername($user);
$password = $this->featureContext->getUserPassword($user);
$headers = [
'Authorization' => 'Basic ' . \base64_encode($user . ':' . $password)
];
if ($bytes !== null) {
$creationWithUploadHeader = [
'Content-Type' => 'application/offset+octet-stream',
'Tus-Resumable' => '1.0.0'
];
$headers = \array_merge($headers, $creationWithUploadHeader);
}
if ($checksum != '') {
$checksumHeader = [
'Upload-Checksum' => $checksum
];
$headers = \array_merge($headers, $checksumHeader);
}
$client = new Client(
$this->featureContext->getBaseUrl(),
[
'verify' => false,
'headers' => $headers
]
);
$davPathVersion = $this->featureContext->getDavPathVersion();
$suffixPath = $user;
if ($davPathVersion === WebDavHelper::DAV_VERSION_SPACES) {
$suffixPath = $spaceId ?: $this->featureContext->getPersonalSpaceIdForUser($user);
}
$client->setChecksumAlgorithm('sha1');
$client->setApiPath(WebDavHelper::getDavPath($davPathVersion, $suffixPath));
$client->setMetadata($uploadMetadata);
$sourceFile = UploadHelper::getAcceptanceTestsDir() . $source;
$client->setKey((string)rand())->file($sourceFile, $destination);
$this->featureContext->pauseUploadDelete();
if ($bytes !== null) {
$client->file($sourceFile, $destination)->createWithUpload($client->getKey(), $bytes);
} elseif (\filesize($sourceFile) === 0) {
$client->file($sourceFile, $destination)->createWithUpload($client->getKey(), 0);
} elseif ($noOfChunks === 1) {
$client->file($sourceFile, $destination)->upload();
} else {
$bytesPerChunk = (int)\ceil(\filesize($sourceFile) / $noOfChunks);
for ($i = 0; $i < $noOfChunks; $i++) {
$client->upload($bytesPerChunk);
}
}
}
/**
* @When user :user uploads file with content :content to :destination using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $content
* @param string $destination
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public function userUploadsAFileWithContentToUsingTus(
string $user,
string $content,
string $destination
): void {
$temporaryFileName = $this->writeDataToTempFile($content);
try {
$this->uploadFileUsingTus(
$user,
\basename($temporaryFileName),
$destination
);
$this->featureContext->setLastUploadDeleteTime(\time());
} catch (Exception $e) {
Assert::assertStringContainsString('TusPhp\Exception\FileException: Unable to create resource', (string)$e);
}
\unlink($temporaryFileName);
}
/**
* @When user :user uploads file with content :content in :noOfChunks chunks to :destination using the TUS protocol on the WebDAV API
*
* @param string|null $user
* @param string $content
* @param int|null $noOfChunks
* @param string $destination
*
* @return void
* @throws ConnectionException
* @throws GuzzleException
* @throws JsonException
* @throws ReflectionException
* @throws TusException
* @throws Exception
* @throws GuzzleException
*/
public function userUploadsAFileWithContentInChunksUsingTus(
?string $user,
string $content,
?int $noOfChunks,
string $destination
): void {
$temporaryFileName = $this->writeDataToTempFile($content);
$this->uploadFileUsingTus(
$user,
\basename($temporaryFileName),
$destination,
null,
[],
$noOfChunks
);
$this->featureContext->setLastUploadDeleteTime(\time());
\unlink($temporaryFileName);
}
/**
* @Given user :user has uploaded file :source to :destination with mtime :mtime using the TUS protocol
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $mtime Time in human-readable format is taken as input which is converted into milliseconds that is used by API
*
* @return void
* @throws Exception
* @throws GuzzleException
*/
public function userHasUploadedFileWithMtimeUsingTUS(
string $user,
string $source,
string $destination,
string $mtime
): void {
$mtime = new DateTime($mtime);
$mtime = $mtime->format('U');
$user = $this->featureContext->getActualUsername($user);
$this->uploadFileUsingTus(
$user,
$source,
$destination,
null,
['mtime' => $mtime]
);
$this->featureContext->setLastUploadDeleteTime(\time());
}
/**
* @When user :user uploads file :source to :destination with mtime :mtime using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $source
* @param string $destination
* @param string $mtime Time in human-readable format is taken as input which is converted into milliseconds that is used by API
*
* @return void
* @throws Exception
* @throws GuzzleException
*/
public function userUploadsFileWithContentToWithMtimeUsingTUS(
string $user,
string $source,
string $destination,
string $mtime
): void {
$mtime = new DateTime($mtime);
$mtime = $mtime->format('U');
$user = $this->featureContext->getActualUsername($user);
$this->uploadFileUsingTus(
$user,
$source,
$destination,
null,
['mtime' => $mtime]
);
$this->featureContext->setLastUploadDeleteTime(\time());
}
/**
* @param string $content
*
* @return string the file name
* @throws Exception
*/
public function writeDataToTempFile(string $content): string {
$temporaryFileName = \tempnam(
UploadHelper::getAcceptanceTestsDir(),
"tus-upload-test-"
);
if ($temporaryFileName === false) {
throw new \Exception("could not create a temporary filename");
}
$temporaryFile = \fopen($temporaryFileName, "w");
if ($temporaryFile === false) {
throw new \Exception("could not open " . $temporaryFileName . " for write");
}
\fwrite($temporaryFile, $content);
\fclose($temporaryFile);
return $temporaryFileName;
}
/**
* @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');
// clear TUS locations cache
$this->tusResourceLocations = [];
}
/**
* @When user :user creates a new TUS resource with content :content on the WebDAV API with these headers:
*
* @param string $user
* @param string $content
* @param TableNode $headers
*
* @return void
* @throws Exception
* @throws GuzzleException
*/
public function userCreatesWithUpload(
string $user,
string $content,
TableNode $headers
): void {
$response = $this->createNewTUSResourceWithHeaders($user, $headers, $content);
$this->featureContext->setResponse($response);
}
/**
* @When user :user creates file :source and uploads content :content in the same request using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $source
* @param string $content
*
* @return void
* @throws Exception
*/
public function userUploadsWithCreatesWithUpload(
string $user,
string $source,
string $content
): void {
$temporaryFileName = $this->writeDataToTempFile($content);
$this->uploadFileUsingTus(
$user,
\basename($temporaryFileName),
$source,
null,
[],
1,
-1
);
$this->featureContext->setLastUploadDeleteTime(\time());
\unlink($temporaryFileName);
}
/**
* @When user :user uploads file with checksum :checksum to the last created TUS Location with offset :offset and content :content using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $checksum
* @param string $offset
* @param string $content
*
* @return void
* @throws Exception
*/
public function userUploadsFileWithChecksum(
string $user,
string $checksum,
string $offset,
string $content
): void {
$resourceLocation = $this->getLastTusResourceLocation();
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $content, $checksum);
$this->featureContext->setResponse($response);
}
/**
* @When user :user uploads content :content with checksum :checksum and offset :offset to the index :locationIndex location of file :filename using the TUS protocol
* @When user :user tries to upload content :content with checksum :checksum and offset :offset to the index :locationIndex location of file :filename using the TUS protocol
*
* @param string $user
* @param string $content
* @param string $checksum
* @param string $offset
* @param string $locationIndex
* @param string $filename
*
* @return void
* @throws Exception
*/
public function userUploadsContentWithChecksumAndOffsetToIndexLocationUsingTUSProtocol(
string $user,
string $content,
string $checksum,
string $offset,
string $locationIndex,
string $filename
): void {
$filenameHash = \base64_encode($filename);
$resourceLocation = $this->getTusResourceLocation($filenameHash, (int)$locationIndex);
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $content, $checksum);
$this->featureContext->setResponse($response);
}
/**
* @Given user :user has uploaded file with checksum :checksum to the last created TUS Location with offset :offset and content :content using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $checksum
* @param string $offset
* @param string $content
*
* @return void
* @throws Exception
*/
public function userHasUploadedFileWithChecksum(
string $user,
string $checksum,
string $offset,
string $content
): void {
$resourceLocation = $this->getLastTusResourceLocation();
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $content, $checksum);
$this->featureContext->theHTTPStatusCodeShouldBe(204, "", $response);
}
/**
* @When user :user sends a chunk to the last created TUS Location with offset :offset and data :data with checksum :checksum using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $offset
* @param string $data
* @param string $checksum
*
* @return void
* @throws Exception
*/
public function userUploadsChunkFileWithChecksum(
string $user,
string $offset,
string $data,
string $checksum
): void {
$resourceLocation = $this->getLastTusResourceLocation();
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data, $checksum);
$this->featureContext->setResponse($response);
}
/**
* @Given user :user has uploaded a chunk to the last created TUS Location with offset :offset and data :data with checksum :checksum using the TUS protocol on the WebDAV API
*
* @param string $user
* @param string $offset
* @param string $data
* @param string $checksum
*
* @return void
* @throws Exception
*/
public function userHasUploadedChunkFileWithChecksum(
string $user,
string $offset,
string $data,
string $checksum
): void {
$resourceLocation = $this->getLastTusResourceLocation();
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data, $checksum);
$this->featureContext->theHTTPStatusCodeShouldBe(204, "", $response);
}
/**
* @When user :user overwrites recently shared file with offset :offset and data :data with checksum :checksum using the TUS protocol on the WebDAV API with these headers:
* @When user :user overwrites existing file with offset :offset and data :data with checksum :checksum using the TUS protocol on the WebDAV API with these headers:
*
* @param string $user
* @param string $offset
* @param string $data
* @param string $checksum
* @param TableNode $headers Tus-Resumable: 1.0.0 header is added automatically
*
* @return void
*
* @throws GuzzleException
* @throws Exception
*/
public function userOverwritesFileWithChecksum(
string $user,
string $offset,
string $data,
string $checksum,
TableNode $headers
): void {
$createResponse = $this->createNewTUSResource($user, $headers);
$this->featureContext->theHTTPStatusCodeShouldBe(201, "", $createResponse);
$resourceLocation = $this->getLastTusResourceLocation();
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data, $checksum);
$this->featureContext->setResponse($response);
}
}
@@ -0,0 +1,308 @@
<?php declare(strict_types=1);
/**
* @author Viktor Scharf <scharf.vi@gmail.com>
*
* @copyright Copyright (c) 2022, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\GraphHelper;
use TestHelpers\BehatHelper;
require_once 'bootstrap.php';
/**
* Acceptance test steps related to testing tags features
*/
class TagContext implements Context {
private FeatureContext $featureContext;
private SpacesContext $spacesContext;
/**
* 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 = BehatHelper::getContext($scope, $environment, 'FeatureContext');
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
}
/**
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param TableNode $table
*
* @return ResponseInterface
* @throws Exception
*/
public function createTags(
string $user,
string $fileOrFolder,
string $resource,
string $space,
TableNode $table
): ResponseInterface {
$tagNameArray = [];
foreach ($table->getRows() as $value) {
$tagNameArray[] = $value[0];
}
if ($fileOrFolder === 'folder' || $fileOrFolder === 'folders') {
$resourceId = $this->spacesContext->getResourceId($user, $space, $resource);
} else {
$resourceId = $this->spacesContext->getFileId($user, $space, $resource);
}
return GraphHelper::createTags(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$resourceId,
$tagNameArray
);
}
/**
* @When /^user "([^"]*)" creates the following tags for (folder|file) "([^"]*)" of space "([^"]*)":$/
*
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theUserCreatesFollowingTags(
string $user,
string $fileOrFolder,
string $resource,
string $space,
TableNode $table
): void {
$response = $this->createTags($user, $fileOrFolder, $resource, $space, $table);
$this->featureContext->setResponse($response);
}
/**
* @Given /^user "([^"]*)" has created the following tags for (folder|file)\s?"([^"]*)" of the space "([^"]*)":$/
*
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theUserHasCreatedFollowingTags(
string $user,
string $fileOrFolder,
string $resource,
string $space,
TableNode $table
): void {
$response = $this->createTags($user, $fileOrFolder, $resource, $space, $table);
$this->featureContext->theHttpStatusCodeShouldBe(200, "", $response);
}
/**
* @Given /^user "([^"]*)" has tagged the following (folders|files) of the space "([^"]*)":$/
*
* @param string $user
* @param string $filesOrFolders (files|folders)
* @param string $space
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userHasCreatedTheFollowingTagsForFilesOfTheSpace(
string $user,
string $filesOrFolders,
string $space,
TableNode $table
): void {
$this->featureContext->verifyTableNodeColumns($table, ["path", "tagName"]);
$rows = $table->getHash();
foreach ($rows as $row) {
$tags = explode(',', $row['tagName']);
$response = $this->createTags($user, $filesOrFolders, $row['path'], $space, new TableNode([$tags]));
$this->featureContext->theHttpStatusCodeShouldBe(200, "", $response);
}
}
/**
* @When user :user lists all available tag(s) via the Graph API
*
* @param string $user
*
* @return void
* @throws Exception
*/
public function theUserGetsAllAvailableTags(string $user): void {
// Note: after creating or deleting tags, in some cases tags do not appear or disappear immediately,
// So wait is necessary before listing tags
sleep(5);
$this->featureContext->setResponse(
GraphHelper::getTags(
$this->featureContext->getBaseUrl(),
$user,
$this->featureContext->getPasswordForUser($user),
$this->featureContext->getStepLineRef()
)
);
}
/**
* @Then /^the response should (not|)\s?contain following tag(s):$/
*
* @param string $shouldOrNot (not|)
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theFollowingTagsShouldExistForUser(string $shouldOrNot, TableNode $table): void {
$rows = $table->getRows();
foreach ($rows as $row) {
$responseArray = $this->featureContext->getJsonDecodedResponse(
$this->featureContext->getResponse()
)['value'];
if ($shouldOrNot === "not") {
Assert::assertFalse(
\in_array($row[0], $responseArray),
"the response should not contain the tag $row[0].\nResponse\n"
. print_r($responseArray, true)
);
} else {
Assert::assertTrue(
\in_array($row[0], $responseArray),
"the response does not contain the tag $row[0].\nResponse\n"
. print_r($responseArray, true)
);
}
}
}
/**
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param TableNode $table
*
* @return ResponseInterface
* @throws Exception
*/
public function removeTagsFromResourceOfTheSpace(
string $user,
string $fileOrFolder,
string $resource,
string $space,
TableNode $table
): ResponseInterface {
$tagNameArray = [];
foreach ($table->getRows() as $value) {
$tagNameArray[] = $value[0];
}
if ($fileOrFolder === 'folder') {
$resourceId = $this->spacesContext->getResourceId($user, $space, $resource);
} else {
$resourceId = $this->spacesContext->getFileId($user, $space, $resource);
}
return GraphHelper::deleteTags(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$resourceId,
$tagNameArray
);
}
/**
* @When /^user "([^"]*)" removes the following tags for (folder|file) "([^"]*)" of space "([^"]*)":$/
*
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userRemovesTagsFromResourceOfTheSpace(
string $user,
string $fileOrFolder,
string $resource,
string $space,
TableNode $table
): void {
$response = $this->removeTagsFromResourceOfTheSpace(
$user,
$fileOrFolder,
$resource,
$space,
$table
);
$this->featureContext->setResponse($response);
}
/**
* @Given /^user "([^"]*)" has removed the following tags for (folder|file) "([^"]*)" of space "([^"]*)":$/
*
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function userHAsRemovedTheFollowingTagsForFileOfSpace(
string $user,
string $fileOrFolder,
string $resource,
string $space,
TableNode $table
): void {
$response = $this->removeTagsFromResourceOfTheSpace($user, $fileOrFolder, $resource, $space, $table);
$this->featureContext->theHttpStatusCodeShouldBe(200, "", $response);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
<?php declare(strict_types=1);
/**
* @author Phil Davis <phil@jankaritech.com>
* @copyright Copyright (c) 2020 Phil Davis phil@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 <http://www.gnu.org/licenses/>
*
*/
use Composer\Autoload\ClassLoader;
$classLoader = new ClassLoader();
$classLoader->addPsr4("TestHelpers\\", __DIR__ . "/../TestHelpers", true);
$classLoader->register();
// Default number of times to retry where retries are useful
if (!\defined('STANDARD_RETRY_COUNT')) {
\define('STANDARD_RETRY_COUNT', 10);
}
// Minimum number of times to retry where retries are useful
if (!\defined('MINIMUM_RETRY_COUNT')) {
\define('MINIMUM_RETRY_COUNT', 2);
}
// Minimum number of times to retry where retries are useful
if (!\defined('HTTP_REQUEST_TIMEOUT')) {
\define('HTTP_REQUEST_TIMEOUT', 60);
}
// The remote server-under-test might or might not happen to have this directory.
// If it does not exist, then the tests may end up creating it.
if (!\defined('ACCEPTANCE_TEST_DIR_ON_REMOTE_SERVER')) {
\define('ACCEPTANCE_TEST_DIR_ON_REMOTE_SERVER', 'tests/acceptance');
}
// The following directory should NOT already exist on the remote server-under-test.
// Acceptance tests are free to do anything needed in this directory, and to
// delete it during or at the end of testing.
if (!\defined('TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER')) {
\define('TEMPORARY_STORAGE_DIR_ON_REMOTE_SERVER', ACCEPTANCE_TEST_DIR_ON_REMOTE_SERVER . '/server_tmp');
}