mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-10-30 10:08:39 -04:00
tests: add acceptance tests for sharing with guests via email (#3751)
Share resources with an email address via the sharingNg API and access them with the guest link from the email.
This commit is contained in:
1 parent
6bb1f2e859
commit
8517f84ea3
11 files changed
+1306
-3
No files matched your search
@@ -250,6 +250,7 @@ config = {
|
||||
"notification": {
|
||||
"suites": [
|
||||
"apiNotification",
|
||||
"apiGuestLink",
|
||||
],
|
||||
"skip": False,
|
||||
"withRemotePhp": False,
|
||||
@@ -265,6 +266,7 @@ config = {
|
||||
"NOTIFICATIONS_SMTP_INSECURE": True,
|
||||
"NOTIFICATIONS_SMTP_SENDER": "ownCloud <noreply@example.com>",
|
||||
"NOTIFICATIONS_DEBUG_ADDR": "0.0.0.0:9174",
|
||||
"OC_ENABLE_GUEST_LINKS": True,
|
||||
},
|
||||
},
|
||||
"antivirus": {
|
||||
|
||||
@@ -1765,6 +1765,11 @@ class GraphHelper {
|
||||
|
||||
foreach ($shareeIds as $index => $shareeId) {
|
||||
$shareType = $shareTypes[$index];
|
||||
if ($shareType === "guest") {
|
||||
// guest invitation via email address
|
||||
$body['recipients'][] = ["email" => $shareeId];
|
||||
continue;
|
||||
}
|
||||
$body['recipients'][] = [
|
||||
"@libre.graph.recipient.type" => $shareType,
|
||||
"objectId" => $shareeId
|
||||
@@ -1780,7 +1785,8 @@ class GraphHelper {
|
||||
$body['@libre.graph.permissions.actions'] = ['libre.graph/driveItem/' . $permissionsAction];
|
||||
}
|
||||
|
||||
if ($expirationDateTime !== null) {
|
||||
if ($expirationDateTime !== null || \in_array("guest", $shareTypes, true)) {
|
||||
// guest invitations always send expirationDateTime, like the web client does
|
||||
$body['expirationDateTime'] = $expirationDateTime;
|
||||
}
|
||||
return $body;
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* @author Viktor Scharf OpenCloud GmbH <v.scharf@opencloud.eu>
|
||||
* @copyright Copyright (c) 2026 OpenCloud GmbH <v.scharf@opencloud.eu>
|
||||
*
|
||||
* 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/>
|
||||
*
|
||||
*/
|
||||
|
||||
namespace TestHelpers;
|
||||
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Helper for the unauthenticated guest links endpoints of the auth-guest service
|
||||
*/
|
||||
class GuestLinkHelper {
|
||||
public const SESSION_COOKIE_NAME = '__Host-oc_guest_session';
|
||||
|
||||
/**
|
||||
* matches the guest link token in a guest link email, e.g. "/g/v1.<share id hash>.<secret>"
|
||||
*/
|
||||
public const TOKEN_IN_EMAIL_REGEX = '/\/g\/(v1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/';
|
||||
|
||||
/**
|
||||
* @param string $baseUrl
|
||||
* @param string $action verify/token|verify/pin|renew
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getUrl(string $baseUrl, string $action): string {
|
||||
return GraphHelper::getBetaFullUrl($baseUrl, "extensions/org.libregraph/guestLinks/$action");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $cookies cookie name => value
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getHeaders(array $cookies = []): array {
|
||||
$headers = ['Content-Type' => 'application/json'];
|
||||
if ($cookies !== []) {
|
||||
$headers['Cookie'] = \implode(
|
||||
'; ',
|
||||
\array_map(fn ($name, $value) => "$name=$value", \array_keys($cookies), $cookies)
|
||||
);
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $baseUrl
|
||||
* @param string $xRequestId
|
||||
* @param string $token
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public static function verifyToken(string $baseUrl, string $xRequestId, string $token): ResponseInterface {
|
||||
return HttpRequestHelper::post(
|
||||
self::getUrl($baseUrl, 'verify/token'),
|
||||
$xRequestId,
|
||||
null,
|
||||
null,
|
||||
self::getHeaders(),
|
||||
\json_encode(['token' => $token])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Set-Cookie headers of the response, keyed by the cookie name
|
||||
*
|
||||
* @param ResponseInterface $response
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function getSetCookieHeaders(ResponseInterface $response): array {
|
||||
$setCookies = [];
|
||||
foreach ($response->getHeader('Set-Cookie') as $setCookie) {
|
||||
$name = \trim(\explode('=', \explode(';', $setCookie, 2)[0], 2)[0]);
|
||||
$setCookies[$name] = $setCookie;
|
||||
}
|
||||
return $setCookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of a cookie set by the response, or null when it is not set or is cleared
|
||||
*
|
||||
* @param string $setCookie
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public static function getCookieValue(string $setCookie): ?string {
|
||||
$value = \explode('=', \explode(';', $setCookie, 2)[0], 2)[1] ?? '';
|
||||
$attributes = \strtolower($setCookie);
|
||||
if ($value === '' || \str_contains($attributes, 'max-age=0') || \preg_match('/max-age=-\d/', $attributes)) {
|
||||
return null;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw Set-Cookie header of the guest session cookie, or null
|
||||
*
|
||||
* @param ResponseInterface $response
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public static function getSessionSetCookieHeader(ResponseInterface $response): ?string {
|
||||
return self::getSetCookieHeaders($response)[self::SESSION_COOKIE_NAME] ?? null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* @author Viktor Scharf OpenCloud GmbH <v.scharf@opencloud.eu>
|
||||
* @copyright Copyright (c) 2026 OpenCloud GmbH <v.scharf@opencloud.eu>
|
||||
*
|
||||
* 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\Step\Given;
|
||||
use Behat\Step\Then;
|
||||
use Behat\Step\When;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TestHelpers\BehatHelper;
|
||||
use TestHelpers\EmailHelper;
|
||||
use TestHelpers\GuestLinkHelper;
|
||||
use TestHelpers\HttpRequestHelper;
|
||||
use TestHelpers\WaitHelper;
|
||||
use TestHelpers\WebDavHelper;
|
||||
|
||||
require_once 'bootstrap.php';
|
||||
|
||||
/**
|
||||
* Steps for guest links: shares with an email address that the guest opens
|
||||
* through a link (or PIN) received by email, without an OpenCloud account.
|
||||
*
|
||||
* The guest is treated like a browser: it keeps the session cookie set by verifying
|
||||
* a link and sends it back.
|
||||
*/
|
||||
class GuestLinkContext implements Context {
|
||||
private FeatureContext $featureContext;
|
||||
private SpacesContext $spacesContext;
|
||||
|
||||
/**
|
||||
* guest link tokens received by each guest, in the order they were read from the emails
|
||||
*
|
||||
* @var array<string, string[]>
|
||||
*/
|
||||
private array $guestLinkTokens = [];
|
||||
|
||||
/**
|
||||
* guest session cookie of each guest
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $guestSessions = [];
|
||||
|
||||
/**
|
||||
* number of emails of each guest that the scenario has already read
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
private array $readEmailCount = [];
|
||||
|
||||
/**
|
||||
* @BeforeScenario
|
||||
*
|
||||
* @param BeforeScenarioScope $scope
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function before(BeforeScenarioScope $scope): void {
|
||||
$environment = $scope->getEnvironment();
|
||||
$this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext');
|
||||
$this->spacesContext = BehatHelper::getContext($scope, $environment, 'SpacesContext');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the emails of the guests that were invited in the scenario,
|
||||
* so that the next scenario does not read a stale guest link.
|
||||
*
|
||||
* @AfterScenario
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearGuestMailboxes(): void {
|
||||
foreach ($this->featureContext->emailRecipients as $mailBox) {
|
||||
try {
|
||||
EmailHelper::deleteAllEmails(
|
||||
EmailHelper::getLocalEmailUrl(),
|
||||
$mailBox,
|
||||
$this->featureContext->getStepLineRef(),
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
echo __METHOD__ . " could not delete emails of '$mailBox', is inbucket set up?\n" . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
* @param int $count
|
||||
* @param int $timeoutSeconds
|
||||
*
|
||||
* @return array the emails of the guest, after at least $count arrived or the timeout passed
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
private function waitForEmails(string $guest, int $count, int $timeoutSeconds = 10): array {
|
||||
$this->featureContext->pushEmailRecipientAsMailBox($guest);
|
||||
$mailBox = EmailHelper::getMailBoxFromEmail($guest);
|
||||
return WaitHelper::waitUntil(
|
||||
fn () => EmailHelper::getMailBoxInformation($mailBox, $this->featureContext->getStepLineRef()),
|
||||
fn ($emails) => \count($emails) >= $count,
|
||||
500,
|
||||
$timeoutSeconds
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers the guest link contained in an email
|
||||
*
|
||||
* @param string $guest
|
||||
* @param string $body
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function rememberCredentialsFromEmail(string $guest, string $body): void {
|
||||
if (\preg_match(GuestLinkHelper::TOKEN_IN_EMAIL_REGEX, $body, $matches)) {
|
||||
$knownTokens = $this->guestLinkTokens[$guest] ?? [];
|
||||
if (\end($knownTokens) !== $matches[1]) {
|
||||
$this->guestLinkTokens[$guest][] = $matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for an email that the scenario has not read yet and returns its body
|
||||
*
|
||||
* @param string $guest
|
||||
*
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
private function readNewEmail(string $guest): string {
|
||||
$expectedCount = ($this->readEmailCount[$guest] ?? 0) + 1;
|
||||
$emails = $this->waitForEmails($guest, $expectedCount);
|
||||
Assert::assertGreaterThanOrEqual(
|
||||
$expectedCount,
|
||||
\count($emails),
|
||||
"Guest '$guest' was expected to have received a new email"
|
||||
);
|
||||
$this->readEmailCount[$guest] = \count($emails);
|
||||
$body = EmailHelper::getBodyOfLastEmail($guest, $this->featureContext->getStepLineRef());
|
||||
$this->rememberCredentialsFromEmail($guest, $body);
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the body of the last email, waiting for the first one if none was read yet
|
||||
*
|
||||
* @param string $guest
|
||||
*
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
private function readLastEmail(string $guest): string {
|
||||
if (($this->readEmailCount[$guest] ?? 0) === 0) {
|
||||
return $this->readNewEmail($guest);
|
||||
}
|
||||
$body = EmailHelper::getBodyOfLastEmail($guest, $this->featureContext->getStepLineRef());
|
||||
$this->rememberCredentialsFromEmail($guest, $body);
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
*
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
private function getGuestLinkTokenFromLastEmail(string $guest): string {
|
||||
$body = $this->readLastEmail($guest);
|
||||
Assert::assertMatchesRegularExpression(
|
||||
GuestLinkHelper::TOKEN_IN_EMAIL_REGEX,
|
||||
$body,
|
||||
"The last email of guest '$guest' does not contain a guest link"
|
||||
);
|
||||
\preg_match(GuestLinkHelper::TOKEN_IN_EMAIL_REGEX, $body, $matches);
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getGuestSession(string $guest): string {
|
||||
Assert::assertArrayHasKey(
|
||||
$guest,
|
||||
$this->guestSessions,
|
||||
"Guest '$guest' has no guest session, verify a guest link or PIN first"
|
||||
);
|
||||
return $this->guestSessions[$guest];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getSessionHeaders(string $guest): array {
|
||||
return GuestLinkHelper::getHeaders([GuestLinkHelper::SESSION_COOKIE_NAME => $this->getGuestSession($guest)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the cookies that a guest endpoint sets, like a browser does
|
||||
*
|
||||
* @param string $guest
|
||||
* @param ResponseInterface $response
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function rememberCookies(string $guest, ResponseInterface $response): void {
|
||||
$sessionSetCookie = GuestLinkHelper::getSessionSetCookieHeader($response);
|
||||
if ($sessionSetCookie !== null && GuestLinkHelper::getCookieValue($sessionSetCookie) !== null) {
|
||||
$this->guestSessions[$guest] = GuestLinkHelper::getCookieValue($sessionSetCookie);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
* @param string $token
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function verifyGuestLinkToken(string $guest, string $token): ResponseInterface {
|
||||
$response = GuestLinkHelper::verifyToken(
|
||||
$this->featureContext->getBaseUrl(),
|
||||
$this->featureContext->getStepLineRef(),
|
||||
$token
|
||||
);
|
||||
$this->rememberCookies($guest, $response);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
*
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
#[When('guest :guest verifies the guest link from the last email using the Graph API')]
|
||||
public function guestVerifiesTheGuestLinkFromTheLastEmail(string $guest): void {
|
||||
$token = $this->getGuestLinkTokenFromLastEmail($guest);
|
||||
$this->featureContext->setResponse($this->verifyGuestLinkToken($guest, $token));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
*
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
#[Given('guest :guest has verified the guest link from the last email')]
|
||||
public function guestHasVerifiedTheGuestLinkFromTheLastEmail(string $guest): void {
|
||||
$token = $this->getGuestLinkTokenFromLastEmail($guest);
|
||||
$response = $this->verifyGuestLinkToken($guest, $token);
|
||||
$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the next email of the guest and remembers its link without using it
|
||||
*
|
||||
* @param string $guest
|
||||
*
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
#[Given('guest :guest has received a new guest link')]
|
||||
public function guestHasReceivedANewGuestLink(string $guest): void {
|
||||
$body = $this->readNewEmail($guest);
|
||||
Assert::assertMatchesRegularExpression(
|
||||
GuestLinkHelper::TOKEN_IN_EMAIL_REGEX,
|
||||
$body,
|
||||
"The new email of guest '$guest' does not contain a guest link"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
*
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
#[When('guest :guest verifies the previous guest link using the Graph API')]
|
||||
public function guestVerifiesThePreviousGuestLink(string $guest): void {
|
||||
$this->readLastEmail($guest);
|
||||
$tokens = $this->guestLinkTokens[$guest] ?? [];
|
||||
Assert::assertGreaterThanOrEqual(2, \count($tokens), "Guest '$guest' has not received a previous guest link");
|
||||
$this->featureContext->setResponse($this->verifyGuestLinkToken($guest, $tokens[\count($tokens) - 2]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
* @param string $token
|
||||
*
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
#[When('guest :guest verifies the guest link token :token using the Graph API')]
|
||||
public function guestVerifiesTheGuestLinkToken(string $guest, string $token): void {
|
||||
$this->featureContext->setResponse($this->verifyGuestLinkToken($guest, $token));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
*
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
#[When('guest :guest lists the shares shared with them using the Graph API')]
|
||||
public function guestListsTheSharesSharedWithThem(string $guest): void {
|
||||
$this->featureContext->setResponse(
|
||||
HttpRequestHelper::get(
|
||||
$this->featureContext->getBaseUrl() . '/graph/v1beta1/me/drive/sharedWithMe',
|
||||
$this->featureContext->getStepLineRef(),
|
||||
null,
|
||||
null,
|
||||
$this->getSessionHeaders($guest),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
* @param string $resource
|
||||
* @param string $space
|
||||
* @param string $owner
|
||||
*
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
#[When('guest :guest downloads file :resource shared from space :space of user :owner using the WebDAV API')]
|
||||
public function guestDownloadsSharedFile(string $guest, string $resource, string $space, string $owner): void {
|
||||
$fileId = $this->spacesContext->getResourceId($owner, $space, $resource);
|
||||
$this->featureContext->setResponse(
|
||||
HttpRequestHelper::get(
|
||||
$this->featureContext->getBaseUrl() . "/" . WebDavHelper::prefixRemotePhp("dav/spaces/$fileId"),
|
||||
$this->featureContext->getStepLineRef(),
|
||||
null,
|
||||
null,
|
||||
$this->getSessionHeaders($guest),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
* @param string $content
|
||||
* @param string $resource
|
||||
* @param string $space
|
||||
* @param string $owner
|
||||
*
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
#[When('guest :guest uploads content :content to file :resource shared from space :space of user :owner using the WebDAV API')]
|
||||
public function guestUploadsContentToSharedFile(
|
||||
string $guest,
|
||||
string $content,
|
||||
string $resource,
|
||||
string $space,
|
||||
string $owner
|
||||
): void {
|
||||
$fileId = $this->spacesContext->getResourceId($owner, $space, $resource);
|
||||
$this->featureContext->setResponse(
|
||||
HttpRequestHelper::sendRequest(
|
||||
$this->featureContext->getBaseUrl() . "/" . WebDavHelper::prefixRemotePhp("dav/spaces/$fileId"),
|
||||
$this->featureContext->getStepLineRef(),
|
||||
'PUT',
|
||||
null,
|
||||
null,
|
||||
$this->getSessionHeaders($guest),
|
||||
$content,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guest
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
*
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
#[When('guest :guest sends HTTP method :method to URL :url with the guest session')]
|
||||
public function guestSendsHttpMethodToUrlWithTheGuestSession(string $guest, string $method, string $url): void {
|
||||
$url = $this->featureContext->substituteInLineCodes($url);
|
||||
$this->featureContext->setResponse(
|
||||
HttpRequestHelper::sendRequest(
|
||||
$this->featureContext->getBaseUrl() . $url,
|
||||
$this->featureContext->getStepLineRef(),
|
||||
$method,
|
||||
null,
|
||||
null,
|
||||
$this->getSessionHeaders($guest),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
#[Then('the response should contain a guest session cookie')]
|
||||
public function theResponseShouldContainAGuestSessionCookie(): void {
|
||||
$setCookie = GuestLinkHelper::getSessionSetCookieHeader($this->featureContext->getResponse());
|
||||
Assert::assertNotNull($setCookie, "The response does not set the guest session cookie");
|
||||
$this->assertCookieIsHardened($setCookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
#[Then('the response should not contain a guest session cookie')]
|
||||
public function theResponseShouldNotContainAGuestSessionCookie(): void {
|
||||
Assert::assertNull(
|
||||
GuestLinkHelper::getSessionSetCookieHeader($this->featureContext->getResponse()),
|
||||
"The response was not expected to set the guest session cookie"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $setCookie
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function assertCookieIsHardened(string $setCookie): void {
|
||||
$attributes = \array_map('trim', \explode(';', \strtolower($setCookie)));
|
||||
foreach (['path=/', 'httponly', 'secure', 'samesite=lax'] as $expectedAttribute) {
|
||||
Assert::assertContains(
|
||||
$expectedAttribute,
|
||||
$attributes,
|
||||
"The cookie '$setCookie' is missing the attribute '$expectedAttribute'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The guest link token is random, so it is compared as the placeholder %guest_link_token%
|
||||
*
|
||||
* @param string $guest
|
||||
* @param string $sender
|
||||
* @param PyStringNode $content
|
||||
*
|
||||
* @return void
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
#[Then('guest :guest should have received the following email from user :sender')]
|
||||
public function guestShouldHaveReceivedTheFollowingEmailFromUser(
|
||||
string $guest,
|
||||
string $sender,
|
||||
PyStringNode $content
|
||||
): void {
|
||||
$expectedContent = $this->featureContext->substituteInLineCodes(
|
||||
\str_replace("\r\n", "\n", $content->getRaw()),
|
||||
$sender
|
||||
);
|
||||
$actualContent = $this->readNewEmail($guest);
|
||||
$actualContent = \preg_replace(GuestLinkHelper::TOKEN_IN_EMAIL_REGEX, '/g/%guest_link_token%', $actualContent);
|
||||
Assert::assertStringContainsString(
|
||||
$expectedContent,
|
||||
$actualContent,
|
||||
"The new email of guest '$guest' was expected to contain '$expectedContent'"
|
||||
. " but the received email is '$actualContent'"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -401,6 +401,9 @@ class SharingNgContext implements Context {
|
||||
}
|
||||
} elseif ($shareType === "group") {
|
||||
$shareeId = $this->featureContext->getAttributeOfCreatedGroup($sharee, 'id');
|
||||
} elseif ($shareType === "guest") {
|
||||
$shareeId = $sharee;
|
||||
$this->featureContext->pushEmailRecipientAsMailBox($sharee);
|
||||
}
|
||||
// for non-existing group or user, generate random id
|
||||
$shareeIds[] = $shareeId ?: WebDavHelper::generateUUIDv4();
|
||||
@@ -476,6 +479,9 @@ class SharingNgContext implements Context {
|
||||
}
|
||||
} elseif ($shareType === "group") {
|
||||
$shareeId = $this->featureContext->getAttributeOfCreatedGroup($sharee, 'id');
|
||||
} elseif ($shareType === "guest") {
|
||||
$shareeId = $sharee;
|
||||
$this->featureContext->pushEmailRecipientAsMailBox($sharee);
|
||||
}
|
||||
// for non-existing group or user, generate random id
|
||||
$shareeIds[] = $shareeId ?: WebDavHelper::generateUUIDv4();
|
||||
@@ -1131,7 +1137,7 @@ class SharingNgContext implements Context {
|
||||
* @throws JsonException
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
#[Given('/^user "([^"]*)" has removed the access of (user|group) "([^"]*)" from (?:file|folder|resource) "([^"]*)" of space "([^"]*)"$/')]
|
||||
#[Given('/^user "([^"]*)" has removed the access of (user|group|guest) "([^"]*)" from (?:file|folder|resource) "([^"]*)" of space "([^"]*)"$/')]
|
||||
public function userHasRemovedAccessOfUserOrGroupFromResourceOfSpace(
|
||||
string $sharer,
|
||||
string $recipientType,
|
||||
@@ -1155,7 +1161,7 @@ class SharingNgContext implements Context {
|
||||
* @throws JsonException
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
#[When('/^user "([^"]*)" removes the access of (user|group) "([^"]*)" from (?:file|folder|resource) "([^"]*)" of space "([^"]*)" using the Graph API$/')]
|
||||
#[When('/^user "([^"]*)" removes the access of (user|group|guest) "([^"]*)" from (?:file|folder|resource) "([^"]*)" of space "([^"]*)" using the Graph API$/')]
|
||||
public function userRemovesAccessOfUserOrGroupFromResourceOfSpaceUsingGraphAPI(
|
||||
string $sharer,
|
||||
string $recipientType,
|
||||
|
||||
@@ -189,6 +189,18 @@ default:
|
||||
- OcmContext:
|
||||
- CliContext:
|
||||
|
||||
apiGuestLink:
|
||||
paths:
|
||||
- "%paths.base%/../features/apiGuestLink"
|
||||
context: *common_ldap_suite_context
|
||||
contexts:
|
||||
- FeatureContext: *common_feature_context_params
|
||||
- SpacesContext:
|
||||
- GraphContext:
|
||||
- SharingNgContext:
|
||||
- NotificationContext:
|
||||
- GuestLinkContext:
|
||||
|
||||
apiAntivirus:
|
||||
paths:
|
||||
- "%paths.base%/../features/apiAntivirus"
|
||||
|
||||
@@ -319,6 +319,10 @@ _ocdav: api compatibility, return correct status code_
|
||||
- [coreApiWebdavPreviews/previews.feature:250](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L250)
|
||||
- [coreApiWebdavPreviews/previews.feature:251](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L251)
|
||||
|
||||
#### [Guest invited to a project space does not get a guest link email](https://github.com/opencloud-eu/opencloud/issues/3744)
|
||||
|
||||
- [apiGuestLink/guestLinkProjectSpace.feature:98](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiGuestLink/guestLinkProjectSpace.feature#L98)
|
||||
|
||||
#### [Preview of text file truncated](https://github.com/opencloud-eu/opencloud/issues/1452)
|
||||
|
||||
- [coreApiWebdavPreviews/previews.feature:263](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L263)
|
||||
|
||||
@@ -322,6 +322,10 @@ _ocdav: api compatibility, return correct status code_
|
||||
- [coreApiWebdavPreviews/previews.feature:250](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L250)
|
||||
- [coreApiWebdavPreviews/previews.feature:251](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L251)
|
||||
|
||||
#### [Guest invited to a project space does not get a guest link email](https://github.com/opencloud-eu/opencloud/issues/3744)
|
||||
|
||||
- [apiGuestLink/guestLinkProjectSpace.feature:98](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiGuestLink/guestLinkProjectSpace.feature#L98)
|
||||
|
||||
#### [Preview of text file truncated](https://github.com/opencloud-eu/opencloud/issues/1452)
|
||||
|
||||
- [coreApiWebdavPreviews/previews.feature:263](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L263)
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
Feature: access a shared resource as a guest
|
||||
As a guest without an account
|
||||
I want to open the guest link I received by email
|
||||
So that I can access the resource that was shared with me
|
||||
|
||||
Background:
|
||||
Given using SharingNG
|
||||
And user "Alice" has been created with default attributes
|
||||
And user "Alice" has uploaded file with content "to share" to "textfile.txt"
|
||||
|
||||
@email
|
||||
Scenario: guest verifies the guest link and gets a session
|
||||
Given user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
When guest "guest@example.org" verifies the guest link from the last email using the Graph API
|
||||
Then the HTTP status code should be "200"
|
||||
And the response should contain a guest session cookie
|
||||
And the JSON data of the response should match
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["permissionId"],
|
||||
"properties": {
|
||||
"permissionId": { "const": "%last_share_id%" }
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@email
|
||||
Scenario: guest link can only be used once
|
||||
Given user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
And guest "guest@example.org" has verified the guest link from the last email
|
||||
When guest "guest@example.org" verifies the guest link from the last email using the Graph API
|
||||
Then the HTTP status code should be "409"
|
||||
And the response should not contain a guest session cookie
|
||||
And the JSON data of the response should match
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["errorType", "permissionId"],
|
||||
"properties": {
|
||||
"errorType": { "const": "tokenAlreadyRedeemed" },
|
||||
"permissionId": { "const": "%last_share_id%" }
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
Scenario Outline: verifying an invalid guest link token fails
|
||||
When guest "guest@example.org" verifies the guest link token "<token>" using the Graph API
|
||||
Then the HTTP status code should be "<http-status-code>"
|
||||
And the response should not contain a guest session cookie
|
||||
And the JSON data of the response should match
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["errorType", "permissionId"],
|
||||
"properties": {
|
||||
"errorType": { "const": "<error-type>" },
|
||||
"permissionId": { "const": "" }
|
||||
}
|
||||
}
|
||||
"""
|
||||
Examples:
|
||||
| token | http-status-code | error-type |
|
||||
| | 400 | invalidRequest |
|
||||
| invalid | 401 | tokenInvalid |
|
||||
| v2.bJTW-HnCYqpGxkk1S1PQUOMm1vlt8W2FgZDlFSGtyu0.DXNg71vlfMV80QIp1qxl1G0MCi4RbRFSfM4rJk5Ibmw | 401 | tokenInvalid |
|
||||
| v1.a.b | 401 | tokenInvalid |
|
||||
| v1.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.DXNg71vlfMV80QIp1qxl1G0MCi4RbRFSfM4rJk5Ibmw | 404 | tokenNotFound |
|
||||
|
||||
@email
|
||||
Scenario: guest lists the resource shared with them
|
||||
Given user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
And guest "guest@example.org" has verified the guest link from the last email
|
||||
When guest "guest@example.org" lists the shares shared with them using the Graph API
|
||||
Then the HTTP status code should be "200"
|
||||
And the JSON data of the response should match
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["value"],
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "remoteItem"],
|
||||
"properties": {
|
||||
"name": { "const": "textfile.txt" },
|
||||
"remoteItem": {
|
||||
"type": "object",
|
||||
"required": ["permissions"],
|
||||
"properties": {
|
||||
"permissions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "grantedToV2"],
|
||||
"properties": {
|
||||
"id": { "const": "%last_share_id%" },
|
||||
"grantedToV2": {
|
||||
"type": "object",
|
||||
"required": ["user"],
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"required": ["@libre.graph.userType", "id"],
|
||||
"properties": {
|
||||
"@libre.graph.userType": { "const": "Guest" },
|
||||
"id": { "const": "guest@example.org" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@email
|
||||
Scenario: guest downloads the shared file
|
||||
Given user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
And guest "guest@example.org" has verified the guest link from the last email
|
||||
When guest "guest@example.org" downloads file "textfile.txt" shared from space "Personal" of user "Alice" using the WebDAV API
|
||||
Then the HTTP status code should be "200"
|
||||
And the downloaded content should be "to share"
|
||||
|
||||
@email
|
||||
Scenario Outline: guest updates the shared file depending on the role
|
||||
Given user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | <permissions-role> |
|
||||
And guest "guest@example.org" has verified the guest link from the last email
|
||||
When guest "guest@example.org" uploads content "changed by guest" to file "textfile.txt" shared from space "Personal" of user "Alice" using the WebDAV API
|
||||
Then the HTTP status code should be "<http-status-code>"
|
||||
And for user "Alice" the content of the file "textfile.txt" of the space "Personal" should be "<content>"
|
||||
Examples:
|
||||
| permissions-role | http-status-code | content |
|
||||
| Viewer | 403 | to share |
|
||||
| File Editor | 204 | changed by guest |
|
||||
|
||||
@email
|
||||
Scenario Outline: guest session only grants access to the guest endpoints
|
||||
Given user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
And guest "guest@example.org" has verified the guest link from the last email
|
||||
When guest "guest@example.org" sends HTTP method "<method>" to URL "<url>" with the guest session
|
||||
Then the HTTP status code should be "401"
|
||||
Examples:
|
||||
| method | url |
|
||||
| GET | /graph/v1.0/me |
|
||||
| GET | /graph/v1.0/me/drives |
|
||||
| GET | /graph/v1.0/users |
|
||||
| GET | /ocs/v2.php/cloud/user |
|
||||
| GET | /graph/v1beta1/me/drive/root |
|
||||
|
||||
@email
|
||||
Scenario: guest cannot access other resources of the sharer
|
||||
Given user "Alice" has uploaded file with content "not shared" to "private.txt"
|
||||
And user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
And guest "guest@example.org" has verified the guest link from the last email
|
||||
When guest "guest@example.org" downloads file "private.txt" shared from space "Personal" of user "Alice" using the WebDAV API
|
||||
Then the HTTP status code should be "404"
|
||||
|
||||
@email
|
||||
Scenario: guest loses access when the share is removed
|
||||
Given user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
And guest "guest@example.org" has verified the guest link from the last email
|
||||
And user "Alice" has removed the access of guest "guest@example.org" from resource "textfile.txt" of space "Personal"
|
||||
When guest "guest@example.org" downloads file "textfile.txt" shared from space "Personal" of user "Alice" using the WebDAV API
|
||||
Then the HTTP status code should be "401"
|
||||
When guest "guest@example.org" lists the shares shared with them using the Graph API
|
||||
Then the HTTP status code should be "401"
|
||||
|
||||
@email
|
||||
Scenario: guest link cannot be used after the share is removed
|
||||
Given user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
And user "Alice" has removed the access of guest "guest@example.org" from resource "textfile.txt" of space "Personal"
|
||||
When guest "guest@example.org" verifies the guest link from the last email using the Graph API
|
||||
Then the HTTP status code should be "404"
|
||||
And the response should not contain a guest session cookie
|
||||
|
||||
@email
|
||||
Scenario: guest loses write access when the owner downgrades the role
|
||||
Given user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | File Editor |
|
||||
And guest "guest@example.org" has verified the guest link from the last email
|
||||
And user "Alice" has updated the last resource share with the following properties:
|
||||
| permissionsRole | Viewer |
|
||||
| space | Personal |
|
||||
| resource | textfile.txt |
|
||||
When guest "guest@example.org" uploads content "changed by guest" to file "textfile.txt" shared from space "Personal" of user "Alice" using the WebDAV API
|
||||
Then the HTTP status code should be "403"
|
||||
And for user "Alice" the content of the file "textfile.txt" of the space "Personal" should be "to share"
|
||||
When guest "guest@example.org" downloads file "textfile.txt" shared from space "Personal" of user "Alice" using the WebDAV API
|
||||
Then the HTTP status code should be "200"
|
||||
And the downloaded content should be "to share"
|
||||
|
||||
@email
|
||||
Scenario: guest keeps access when the owner renames the shared resource
|
||||
Given user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
And guest "guest@example.org" has verified the guest link from the last email
|
||||
And user "Alice" has moved file "textfile.txt" to "renamed.txt"
|
||||
When guest "guest@example.org" downloads file "renamed.txt" shared from space "Personal" of user "Alice" using the WebDAV API
|
||||
Then the HTTP status code should be "200"
|
||||
And the downloaded content should be "to share"
|
||||
When guest "guest@example.org" lists the shares shared with them using the Graph API
|
||||
Then the HTTP status code should be "200"
|
||||
And the JSON data of the response should match
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["value"],
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": { "const": "renamed.txt" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -0,0 +1,239 @@
|
||||
Feature: share a resource with a guest via email address
|
||||
As a user
|
||||
I want to share a resource with an email address of a person without an account
|
||||
So that the person can access the resource through a guest link
|
||||
|
||||
Background:
|
||||
Given user "Alice" has been created with default attributes
|
||||
And user "Alice" has uploaded file with content "to share" to "textfile.txt"
|
||||
|
||||
|
||||
Scenario Outline: share a resource with a guest using an email address
|
||||
Given user "Alice" has created folder "folderToShare"
|
||||
When user "Alice" sends the following resource share invitation using the Graph API:
|
||||
| resource | <resource> |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | <permissions-role> |
|
||||
Then the HTTP status code should be "200"
|
||||
And the JSON data of the response should match
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["value"],
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "roles", "grantedToV2", "invitation"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^%role_id_pattern%$"
|
||||
}
|
||||
},
|
||||
"grantedToV2": {
|
||||
"type": "object",
|
||||
"required": ["user"],
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"required": ["@libre.graph.userType", "displayName", "id"],
|
||||
"properties": {
|
||||
"@libre.graph.userType": { "const": "Guest" },
|
||||
"displayName": { "const": "guest@example.org" },
|
||||
"id": { "const": "guest@example.org" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"invitation": {
|
||||
"type": "object",
|
||||
"required": ["invitedBy"],
|
||||
"properties": {
|
||||
"invitedBy": {
|
||||
"type": "object",
|
||||
"required": ["user"],
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"required": ["@libre.graph.userType", "displayName", "id"],
|
||||
"properties": {
|
||||
"@libre.graph.userType": { "const": "Member" },
|
||||
"displayName": { "const": "Alice Hansen" },
|
||||
"id": { "type": "string", "pattern": "^%user_id_pattern%$" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
Examples:
|
||||
| resource | permissions-role |
|
||||
| textfile.txt | Viewer |
|
||||
| textfile.txt | File Editor |
|
||||
| folderToShare | Viewer |
|
||||
| folderToShare | Editor |
|
||||
|
||||
|
||||
Scenario: email address of the guest is stored in its canonical lowercase form
|
||||
When user "Alice" sends the following resource share invitation using the Graph API:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | Guest@Example.ORG |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
Then the HTTP status code should be "200"
|
||||
And the JSON data of the response should match
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["value"],
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["grantedToV2"],
|
||||
"properties": {
|
||||
"grantedToV2": {
|
||||
"type": "object",
|
||||
"required": ["user"],
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "const": "guest@example.org" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@email
|
||||
Scenario: guest receives an email with a guest link
|
||||
When user "Alice" sends the following resource share invitation using the Graph API:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
Then the HTTP status code should be "200"
|
||||
And guest "guest@example.org" should have received the following email from user "Alice"
|
||||
"""
|
||||
Hello,
|
||||
|
||||
%displayname% has shared "textfile.txt" with you.
|
||||
|
||||
The link below is personal and can only be used once. Please do not forward it.
|
||||
|
||||
The link is only valid for 30 minutes.
|
||||
|
||||
Click here to view it: %base_url%/g/%guest_link_token%
|
||||
"""
|
||||
|
||||
|
||||
Scenario Outline: share the same resource with the same guest again
|
||||
Given user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
When user "Alice" sends the following resource share invitation using the Graph API:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | <sharee> |
|
||||
| shareType | guest |
|
||||
| permissionsRole | <permissions-role> |
|
||||
Then the HTTP status code should be "409"
|
||||
And the JSON data of the response should match
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["error"],
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"required": ["code"],
|
||||
"properties": {
|
||||
"code": { "const": "nameAlreadyExists" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
Examples:
|
||||
| sharee | permissions-role |
|
||||
| guest@example.org | Viewer |
|
||||
| guest@example.org | File Editor |
|
||||
| Guest@Example.ORG | Viewer |
|
||||
|
||||
@email
|
||||
Scenario: share a resource with a guest again after the previous share was removed
|
||||
Given using SharingNG
|
||||
And user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
And guest "guest@example.org" has received a new guest link
|
||||
And user "Alice" has removed the access of guest "guest@example.org" from resource "textfile.txt" of space "Personal"
|
||||
And user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | Personal |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
And guest "guest@example.org" has received a new guest link
|
||||
When guest "guest@example.org" verifies the previous guest link using the Graph API
|
||||
Then the HTTP status code should be "404"
|
||||
And the JSON data of the response should match
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["errorType"],
|
||||
"properties": {
|
||||
"errorType": { "const": "tokenNotFound" }
|
||||
}
|
||||
}
|
||||
"""
|
||||
When guest "guest@example.org" verifies the guest link from the last email using the Graph API
|
||||
Then the HTTP status code should be "200"
|
||||
And the JSON data of the response should match
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["permissionId"],
|
||||
"properties": {
|
||||
"permissionId": { "const": "%last_share_id%" }
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -0,0 +1,127 @@
|
||||
Feature: share a project space with a guest via email address
|
||||
As a space manager
|
||||
I want to invite a person without an account to a project space using their email address
|
||||
So that the person can access the space through a guest link
|
||||
|
||||
Background:
|
||||
Given using spaces DAV path
|
||||
And using SharingNG
|
||||
And user "Alice" has been created with default attributes
|
||||
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
|
||||
And user "Alice" has created a space "new-space" with the default quota using the Graph API
|
||||
And user "Alice" has uploaded a file inside space "new-space" with content "space content" to "textfile.txt"
|
||||
|
||||
|
||||
Scenario Outline: invite a guest to a project space using an email address
|
||||
When user "Alice" sends the following space share invitation using root endpoint of the Graph API:
|
||||
| space | new-space |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | <permissions-role> |
|
||||
Then the HTTP status code should be "200"
|
||||
And the JSON data of the response should match
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["value"],
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "roles", "grantedToV2", "invitation"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^%role_id_pattern%$"
|
||||
}
|
||||
},
|
||||
"grantedToV2": {
|
||||
"type": "object",
|
||||
"required": ["user"],
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"required": ["@libre.graph.userType", "displayName", "id"],
|
||||
"properties": {
|
||||
"@libre.graph.userType": { "const": "Guest" },
|
||||
"displayName": { "const": "guest@example.org" },
|
||||
"id": { "const": "guest@example.org" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"invitation": {
|
||||
"type": "object",
|
||||
"required": ["invitedBy"],
|
||||
"properties": {
|
||||
"invitedBy": {
|
||||
"type": "object",
|
||||
"required": ["user"],
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"required": ["@libre.graph.userType", "displayName", "id"],
|
||||
"properties": {
|
||||
"@libre.graph.userType": { "const": "Member" },
|
||||
"displayName": { "const": "Alice Hansen" },
|
||||
"id": { "type": "string", "pattern": "^%user_id_pattern%$" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
Examples:
|
||||
| permissions-role |
|
||||
| Space Viewer |
|
||||
| Space Editor |
|
||||
| Manager |
|
||||
|
||||
@email
|
||||
Scenario: guest invited to a project space gets access through the guest link
|
||||
Given user "Alice" has sent the following space share invitation:
|
||||
| space | new-space |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Space Viewer |
|
||||
When guest "guest@example.org" verifies the guest link from the last email using the Graph API
|
||||
Then the HTTP status code should be "200"
|
||||
And the response should contain a guest session cookie
|
||||
When guest "guest@example.org" downloads file "textfile.txt" shared from space "new-space" of user "Alice" using the WebDAV API
|
||||
Then the HTTP status code should be "200"
|
||||
And the downloaded content should be "space content"
|
||||
|
||||
@email
|
||||
Scenario: guest gets access to a file shared from a project space
|
||||
Given user "Alice" has uploaded a file inside space "new-space" with content "not shared" to "private.txt"
|
||||
And user "Alice" has sent the following resource share invitation:
|
||||
| resource | textfile.txt |
|
||||
| space | new-space |
|
||||
| sharee | guest@example.org |
|
||||
| shareType | guest |
|
||||
| permissionsRole | Viewer |
|
||||
When guest "guest@example.org" verifies the guest link from the last email using the Graph API
|
||||
Then the HTTP status code should be "200"
|
||||
And the response should contain a guest session cookie
|
||||
When guest "guest@example.org" downloads file "textfile.txt" shared from space "new-space" of user "Alice" using the WebDAV API
|
||||
Then the HTTP status code should be "200"
|
||||
And the downloaded content should be "space content"
|
||||
When guest "guest@example.org" downloads file "private.txt" shared from space "new-space" of user "Alice" using the WebDAV API
|
||||
Then the HTTP status code should be "404"
|
||||
Reference in new issue
Block a user