* @author Thomas Müller * * @copyright Copyright (c) 2016, ownCloud, Inc. * @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 * */ require __DIR__ . '/../../vendor/autoload.php'; use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; use Behat\Behat\Hook\Scope\BeforeScenarioScope; use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\ClientException; use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; /** * Defines application features from the specific context. */ class FeatureContext implements Context, SnippetAcceptingContext { public const TEST_PASSWORD = '123456'; /** @var array[] */ protected static $identifierToToken; /** @var array[] */ protected static $tokenToIdentifier; /** @var array[] */ protected static $sessionIdToUser; /** @var array[] */ protected static $userToSessionId; /** @var array[] */ protected static $userToAttendeeId; /** @var array[] */ protected static $messages; protected static $permissionsMap = [ 'D' => 0, // PERMISSIONS_DEFAULT 'C' => 1, // PERMISSIONS_CUSTOM 'S' => 2, // PERMISSIONS_CALL_START 'J' => 4, // PERMISSIONS_CALL_JOIN 'L' => 8, // PERMISSIONS_LOBBY_IGNORE 'A' => 16, // PERMISSIONS_PUBLISH_AUDIO 'V' => 32, // PERMISSIONS_PUBLISH_VIDEO 'P' => 64, // PERMISSIONS_PUBLISH_SCREEN ]; /** @var string */ protected $currentUser; /** @var ResponseInterface */ private $response; /** @var CookieJar[] */ private $cookieJars; /** @var string */ protected $baseUrl; /** @var string */ protected $baseRemoteUrl; /** @var string */ protected $lastEtag; /** @var array */ protected $createdUsers = []; /** @var array */ protected $createdGroups = []; /** @var array */ protected $createdGuestAccountUsers = []; /** @var array */ protected $changedConfigs = []; /** @var SharingContext */ private $sharingContext; /** @var null|bool */ private $guestsAppWasEnabled = null; /** @var string */ private $guestsOldWhitelist; use CommandLineTrait; public static function getTokenForIdentifier(string $identifier) { return self::$identifierToToken[$identifier]; } public function getAttendeeId(string $type, string $id, string $room, string $user = null) { if (!isset(self::$userToAttendeeId[$type][$id])) { if ($user !== null) { $this->userLoadsAttendeeIdsInRoom($user, $room, 'v4'); } else { throw new \Exception('Attendee id unknown, please call userLoadsAttendeeIdsInRoom with a user that has access before'); } } return self::$userToAttendeeId[$type][$id]; } /** * FeatureContext constructor. */ public function __construct() { $this->cookieJars = []; $this->baseUrl = getenv('TEST_SERVER_URL'); $this->baseRemoteUrl = getenv('TEST_REMOTE_URL'); $this->guestsAppWasEnabled = null; } /** * @BeforeScenario */ public function setUp() { self::$identifierToToken = []; self::$tokenToIdentifier = []; self::$sessionIdToUser = []; self::$userToSessionId = []; self::$userToAttendeeId = []; self::$messages = []; $this->createdUsers = []; $this->createdGroups = []; $this->createdGuestAccountUsers = []; } /** * @BeforeScenario */ public function getOtherRequiredSiblingContexts(BeforeScenarioScope $scope) { $environment = $scope->getEnvironment(); $this->sharingContext = $environment->getContext("SharingContext"); } /** * @AfterScenario */ public function tearDown() { foreach ($this->createdUsers as $user) { $this->deleteUser($user); } foreach ($this->createdGroups as $group) { $this->deleteGroup($group); } foreach ($this->createdGuestAccountUsers as $user) { $this->deleteUser($user); } } /** * @Then /^user "([^"]*)" cannot find any listed rooms \((v4)\)$/ * * @param string $user * @param string $apiVersion */ public function userCannotFindAnyListedRooms(string $user, string $apiVersion): void { $this->userCanFindListedRoomsWithTerm($user, '', $apiVersion, null); } /** * @Then /^user "([^"]*)" cannot find any listed rooms with (\d+) \((v4)\)$/ * * @param string $user * @param int $statusCode * @param string $apiVersion */ public function userCannotFindAnyListedRoomsWithStatus(string $user, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/listed-room'); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" cannot find any listed rooms with term "([^"]*)" \((v4)\)$/ * * @param string $user * @param string $term * @param string $apiVersion */ public function userCannotFindAnyListedRoomsWithTerm(string $user, string $term, string $apiVersion): void { $this->userCanFindListedRoomsWithTerm($user, $term, $apiVersion, null); } /** * @Then /^user "([^"]*)" can find listed rooms \((v4)\)$/ * * @param string $user * @param string $apiVersion * @param TableNode|null $formData */ public function userCanFindListedRooms(string $user, string $apiVersion, TableNode $formData = null): void { $this->userCanFindListedRoomsWithTerm($user, '', $apiVersion, $formData); } /** * @Then /^user "([^"]*)" can find listed rooms with term "([^"]*)" \((v4)\)$/ * * @param string $user * @param string $term * @param string $apiVersion * @param TableNode|null $formData */ public function userCanFindListedRoomsWithTerm(string $user, string $term, string $apiVersion, TableNode $formData = null): void { $this->setCurrentUser($user); $suffix = ''; if ($term !== '') { $suffix = '?searchTerm=' . \rawurlencode($term); } $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/listed-room' . $suffix); $this->assertStatusCode($this->response, 200); $rooms = $this->getDataFromResponse($this->response); if ($formData === null) { Assert::assertEmpty($rooms); return; } $this->assertRooms($rooms, $formData); } /** * @Then /^user "([^"]*)" is participant of the following rooms \((v4)\)$/ * * @param string $user * @param string $apiVersion * @param TableNode|null $formData */ public function userIsParticipantOfRooms(string $user, string $apiVersion, TableNode $formData = null): void { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/room'); $this->assertStatusCode($this->response, 200); $rooms = $this->getDataFromResponse($this->response); $rooms = array_filter($rooms, function ($room) { return $room['type'] !== 4; }); if ($formData === null) { Assert::assertEmpty($rooms); return; } $this->assertRooms($rooms, $formData); } /** * @param array $rooms * @param TableNode $formData */ private function assertRooms($rooms, TableNode $formData) { Assert::assertCount(count($formData->getHash()), $rooms, 'Room count does not match'); Assert::assertEquals($formData->getHash(), array_map(function ($room, $expectedRoom) { if (!isset(self::$identifierToToken[$room['name']])) { self::$identifierToToken[$room['name']] = $room['token']; } if (!isset(self::$tokenToIdentifier[$room['token']])) { self::$tokenToIdentifier[$room['token']] = $room['name']; } $data = []; if (isset($expectedRoom['id'])) { $data['id'] = self::$tokenToIdentifier[$room['token']]; } if (isset($expectedRoom['name'])) { $data['name'] = $room['name']; } if (isset($expectedRoom['description'])) { $data['description'] = $room['description']; } if (isset($expectedRoom['type'])) { $data['type'] = (string) $room['type']; } if (isset($expectedRoom['hasPassword'])) { $data['hasPassword'] = (string) $room['hasPassword']; } if (isset($expectedRoom['readOnly'])) { $data['readOnly'] = (string) $room['readOnly']; } if (isset($expectedRoom['listable'])) { $data['listable'] = (string) $room['listable']; } if (isset($expectedRoom['participantType'])) { $data['participantType'] = (string) $room['participantType']; } if (isset($expectedRoom['sipEnabled'])) { $data['sipEnabled'] = (string) $room['sipEnabled']; } if (isset($expectedRoom['callFlag'])) { $data['callFlag'] = (int) $room['callFlag']; } if (isset($expectedRoom['attendeePin'])) { $data['attendeePin'] = $room['attendeePin'] ? '**PIN**' : ''; } if (isset($expectedRoom['lastMessage'])) { $data['lastMessage'] = $room['lastMessage'] ? $room['lastMessage']['message'] : ''; } if (isset($expectedRoom['unreadMessages'])) { $data['unreadMessages'] = (int) $room['unreadMessages']; } if (isset($expectedRoom['unreadMention'])) { $data['unreadMention'] = (int) $room['unreadMention']; } if (isset($expectedRoom['unreadMentionDirect'])) { $data['unreadMentionDirect'] = (int) $room['unreadMentionDirect']; } if (isset($expectedRoom['participants'])) { throw new \Exception('participants key needs to be checked via participants endpoint'); } return $data; }, $rooms, $formData->getHash())); } /** * @Then /^user "([^"]*)" has the following invitations \((v1)\)$/ * * @param string $user * @param string $apiVersion * @param TableNode|null $formData */ public function userHasInvites(string $user, string $apiVersion, TableNode $formData = null): void { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/federation/invitation'); $this->assertStatusCode($this->response, 200); $invites = $this->getDataFromResponse($this->response); if ($formData === null) { Assert::assertEmpty($invites); return; } $this->assertInvites($invites, $formData); } /** * @param array $invites * @param TableNode $formData */ private function assertInvites($invites, TableNode $formData) { Assert::assertCount(count($formData->getHash()), $invites, 'Invite count does not match'); Assert::assertEquals($formData->getHash(), array_map(function ($invite, $expectedInvite) { $data = []; if (isset($expectedInvite['id'])) { $data['id'] = self::$tokenToIdentifier[$invite['token']]; } if (isset($expectedInvite['access_token'])) { $data['access_token'] = (string) $invite['access_token']; } if (isset($expectedInvite['remote_token'])) { $data['remote_token'] = self::$tokenToIdentifier[$invite['remote_token']] ?? 'unknown-token'; } if (isset($expectedInvite['remote_server'])) { if ($invite['remote_server'] === 'localhost:8080') { $data['remote_server'] = 'LOCAL'; } elseif ($invite['remote_server'] === 'localhost:8180') { $data['remote_server'] = 'REMOTE'; } else { $data['remote_server'] = 'unknown-server'; } } return $data; }, $invites, $formData->getHash())); } /** * @Then /^user "([^"]*)" (is|is not) participant of room "([^"]*)" \((v4)\)$/ * * @param string $user * @param string $isOrNotParticipant * @param string $identifier * @param string $apiVersion * @param TableNode|null $formData */ public function userIsParticipantOfRoom(string $user, string $isOrNotParticipant, string $identifier, string $apiVersion, TableNode $formData = null): void { if (strpos($user, 'guest') === 0) { $this->guestIsParticipantOfRoom($user, $isOrNotParticipant, $identifier, $apiVersion, $formData); return; } $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/room'); $this->assertStatusCode($this->response, 200); $isParticipant = $isOrNotParticipant === 'is'; $rooms = $this->getDataFromResponse($this->response); $rooms = array_filter($rooms, function ($room) { return $room['type'] !== 4; }); if ($isParticipant) { Assert::assertNotEmpty($rooms); } foreach ($rooms as $room) { if (self::$tokenToIdentifier[$room['token']] === $identifier) { Assert::assertEquals($isParticipant, true, 'Room ' . $identifier . ' found in user´s room list'); if ($formData) { $this->assertRooms([$room], $formData); } return; } } Assert::assertEquals($isParticipant, false, 'Room ' . $identifier . ' not found in user´s room list'); } /** * @Then /^user "([^"]*)" sees the following attendees in room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param int $statusCode * @param string $apiVersion * @param TableNode $formData */ public function userSeesAttendeesInRoom(string $user, string $identifier, int $statusCode, string $apiVersion, TableNode $formData = null): void { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/participants'); $this->assertStatusCode($this->response, $statusCode); if ($formData instanceof TableNode) { $attendees = $this->getDataFromResponse($this->response); $expectedKeys = array_flip($formData->getRows()[0]); $result = []; foreach ($attendees as $attendee) { $data = []; if (isset($expectedKeys['actorType'])) { $data['actorType'] = $attendee['actorType']; } if (isset($expectedKeys['actorId'])) { $data['actorId'] = $attendee['actorId']; } if (isset($expectedKeys['participantType'])) { $data['participantType'] = (string) $attendee['participantType']; } if (isset($expectedKeys['inCall'])) { $data['inCall'] = (string) $attendee['inCall']; } if (isset($expectedKeys['attendeePin'])) { $data['attendeePin'] = $attendee['attendeePin'] ? '**PIN**' : ''; } if (isset($expectedKeys['permissions'])) { $data['permissions'] = (string) $attendee['permissions']; } if (isset($expectedKeys['attendeePermissions'])) { $data['attendeePermissions'] = (string) $attendee['attendeePermissions']; } if (!isset(self::$userToAttendeeId[$attendee['actorType']])) { self::$userToAttendeeId[$attendee['actorType']] = []; } self::$userToAttendeeId[$attendee['actorType']][$attendee['actorId']] = $attendee['attendeeId']; $result[] = $data; } $expected = array_map(function ($attendee) { if (isset($attendee['actorId']) && substr($attendee['actorId'], 0, strlen('"guest')) === '"guest') { $attendee['actorId'] = sha1(self::$userToSessionId[trim($attendee['actorId'], '"')]); } if (isset($attendee['actorId'], $attendee['actorType']) && $attendee['actorType'] === 'federated_users') { $attendee['actorId'] .= '@' . rtrim($this->baseRemoteUrl, '/'); } if (isset($attendee['participantType'])) { $attendee['participantType'] = (string)$this->mapParticipantTypeTestInput($attendee['participantType']); } return $attendee; }, $formData->getHash()); $result = array_map(function ($attendee) { if (isset($attendee['permissions'])) { $attendee['permissions'] = $this->mapPermissionsAPIOutput($attendee['permissions']); } if (isset($attendee['attendeePermissions'])) { $attendee['attendeePermissions'] = $this->mapPermissionsAPIOutput($attendee['attendeePermissions']); } return $attendee; }, $result); usort($expected, [self::class, 'sortAttendees']); usort($result, [self::class, 'sortAttendees']); Assert::assertEquals($expected, $result); } else { Assert::assertNull($formData); } } /** * @Then /^user "([^"]*)" loads attendees attendee ids in room "([^"]*)" \((v4)\)$/ * * @param string $user * @param string $identifier * @param string $apiVersion */ public function userLoadsAttendeeIdsInRoom(string $user, string $identifier, string $apiVersion, TableNode $formData = null): void { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/participants'); $this->assertStatusCode($this->response, 200); $attendees = $this->getDataFromResponse($this->response); foreach ($attendees as $attendee) { if (!isset(self::$userToAttendeeId[$attendee['actorType']])) { self::$userToAttendeeId[$attendee['actorType']] = []; } self::$userToAttendeeId[$attendee['actorType']][$attendee['actorId']] = $attendee['attendeeId']; } } protected static function sortAttendees(array $a1, array $a2): int { if (array_key_exists('participantType', $a1) && array_key_exists('participantType', $a2) && $a1['participantType'] !== $a2['participantType']) { return $a1['participantType'] <=> $a2['participantType']; } if ($a1['actorType'] !== $a2['actorType']) { return $a1['actorType'] <=> $a2['actorType']; } return $a1['actorId'] <=> $a2['actorId']; } private function mapParticipantTypeTestInput($participantType) { if (is_numeric($participantType)) { return $participantType; } switch ($participantType) { case 'OWNER': return 1; case 'MODERATOR': return 2; case 'USER': return 3; case 'GUEST': return 4; case 'USER_SELF_JOINED': return 5; case 'GUEST_MODERATOR': return 6; } Assert::fail('Invalid test input value for participant type'); } private function mapPermissionsTestInput($permissions): int { if (is_numeric($permissions)) { return $permissions; } $numericPermissions = 0; foreach (self::$permissionsMap as $char => $int) { if (strpos($permissions, $char) !== false) { $numericPermissions += $int; $permissions = str_replace($char, '', $permissions); } } if (trim($permissions) !== '') { Assert::fail('Invalid test input value for permissions'); } return $numericPermissions; } private function mapPermissionsAPIOutput($permissions): string { $permissions = (int) $permissions; $permissionsString = !$permissions ? 'D' : ''; foreach (self::$permissionsMap as $char => $int) { if ($permissions & $int) { $permissionsString .= $char; $permissions &= ~ $int; } } if ($permissions !== 0) { Assert::fail('Invalid API output value for permissions'); } return $permissionsString; } /** * @param string $guest * @param string $isOrNotParticipant * @param string $identifier * @param string $apiVersion * @param TableNode|null $formData */ private function guestIsParticipantOfRoom(string $guest, string $isOrNotParticipant, string $identifier, string $apiVersion, TableNode $formData = null): void { $this->setCurrentUser($guest); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier]); $response = $this->getDataFromResponse($this->response); $isParticipant = $isOrNotParticipant === 'is'; if ($formData) { $rooms = [$response]; $this->assertRooms($rooms, $formData); } if ($isParticipant) { $this->assertStatusCode($this->response, 200); Assert::assertEquals(self::$userToSessionId[$guest], $response['sessionId']); return; } if ($this->response->getStatusCode() === 200) { // Public rooms can always be got, but if the guest is not a // participant the sessionId will be 0. Assert::assertEquals(0, $response['sessionId']); return; } $this->assertStatusCode($this->response, 404); } /** * @Then /^user "([^"]*)" creates room "([^"]*)" \((v4)\)$/ * * @param string $user * @param string $identifier * @param string $apiVersion * @param TableNode|null $formData */ public function userCreatesRoom(string $user, string $identifier, string $apiVersion, TableNode $formData = null): void { $this->userCreatesRoomWith($user, $identifier, 201, $apiVersion, $formData); } /** * @Then /^user "([^"]*)" creates room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param int $statusCode * @param string $apiVersion * @param TableNode|null $formData */ public function userCreatesRoomWith(string $user, string $identifier, int $statusCode, string $apiVersion = 'v1', TableNode $formData = null): void { $this->setCurrentUser($user); $this->sendRequest('POST', '/apps/spreed/api/' . $apiVersion . '/room', $formData); $this->assertStatusCode($this->response, $statusCode); $response = $this->getDataFromResponse($this->response); if ($statusCode === 201) { self::$identifierToToken[$identifier] = $response['token']; self::$tokenToIdentifier[$response['token']] = $identifier; } } /** * @Then /^user "([^"]*)" tries to create room with (\d+) \((v4)\)$/ * * @param string $user * @param int $statusCode * @param string $apiVersion * @param TableNode|null $formData */ public function userTriesToCreateRoom(string $user, int $statusCode, string $apiVersion = 'v1', TableNode $formData = null): void { $this->setCurrentUser($user); $this->sendRequest('POST', '/apps/spreed/api/' . $apiVersion . '/room', $formData); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" gets the room for path "([^"]*)" with (\d+) \((v1)\)$/ * * @param string $user * @param string $path * @param int $statusCode * @param string $apiVersion */ public function userGetsTheRoomForPath(string $user, string $path, int $statusCode, string $apiVersion): void { $fileId = $this->getFileIdForPath($user, $path); $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/file/' . $fileId); $this->assertStatusCode($this->response, $statusCode); if ($statusCode !== 200) { return; } $response = $this->getDataFromResponse($this->response); $identifier = 'file ' . $path . ' room'; self::$identifierToToken[$identifier] = $response['token']; self::$tokenToIdentifier[$response['token']] = $identifier; } /** * @param string $user * @param string $path * @return int */ private function getFileIdForPath($user, $path) { $this->currentUser = $user; $url = "/$user/$path"; $headers = []; $headers['Depth'] = 0; $body = '' . ' ' . ' ' . ' ' . ''; $this->sendingToDav('PROPFIND', $url, $headers, $body); $this->assertStatusCode($this->response, 207); $xmlResponse = simplexml_load_string($this->response->getBody()); $xmlResponse->registerXPathNamespace('oc', 'http://owncloud.org/ns'); return (int)$xmlResponse->xpath('//oc:fileid')[0]; } /** * @param string $verb * @param string $url * @param array $headers * @param string $body */ private function sendingToDav(string $verb, string $url, array $headers = null, string $body = null) { $fullUrl = $this->baseUrl . 'remote.php/dav/files' . $url; $client = new Client(); $options = []; if ($this->currentUser === 'admin') { $options['auth'] = 'admin'; } else { $options['auth'] = [$this->currentUser, self::TEST_PASSWORD]; } $options['headers'] = [ 'OCS_APIREQUEST' => 'true' ]; if ($headers !== null) { $options['headers'] = array_merge($options['headers'], $headers); } if ($body !== null) { $options['body'] = $body; } try { $this->response = $client->{$verb}($fullUrl, $options); } catch (GuzzleHttp\Exception\ClientException $ex) { $this->response = $ex->getResponse(); } } /** * @Then /^user "([^"]*)" gets the room for last share with (\d+) \((v1)\)$/ * * @param string $user * @param int $statusCode * @param string $apiVersion */ public function userGetsTheRoomForLastShare(string $user, int $statusCode, string $apiVersion): void { $shareToken = $this->sharingContext->getLastShareToken(); $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/publicshare/' . $shareToken); $this->assertStatusCode($this->response, $statusCode); if ($statusCode !== 200) { return; } $response = $this->getDataFromResponse($this->response); $identifier = 'file last share room'; self::$identifierToToken[$identifier] = $response['token']; self::$tokenToIdentifier[$response['token']] = $identifier; } /** * @Then /^user "([^"]*)" creates the password request room for last share with (\d+) \((v1)\)$/ * * @param string $user * @param int $statusCode * @param string $apiVersion */ public function userCreatesThePasswordRequestRoomForLastShare(string $user, int $statusCode, string $apiVersion): void { $shareToken = $this->sharingContext->getLastShareToken(); $this->setCurrentUser($user); $this->sendRequest('POST', '/apps/spreed/api/' . $apiVersion . '/publicshareauth', ['shareToken' => $shareToken]); $this->assertStatusCode($this->response, $statusCode); if ($statusCode !== 201) { return; } $response = $this->getDataFromResponse($this->response); $identifier = 'password request for last share room'; self::$identifierToToken[$identifier] = $response['token']; self::$tokenToIdentifier[$response['token']] = $identifier; } /** * @Then /^user "([^"]*)" joins room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param int $statusCode * @param string $apiVersion * @param TableNode|null $formData */ public function userJoinsRoom(string $user, string $identifier, int $statusCode, string $apiVersion, TableNode $formData = null): void { $this->setCurrentUser($user); $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/participants/active', $formData ); $this->assertStatusCode($this->response, $statusCode); if ($statusCode !== 200) { return; } $response = $this->getDataFromResponse($this->response); if (array_key_exists('sessionId', $response)) { // In the chat guest users are identified by their sessionId. The // sessionId is larger than the size of the actorId column in the // database, though, so the ID stored in the database and returned // in chat messages is a hashed version instead. self::$sessionIdToUser[sha1($response['sessionId'])] = $user; self::$userToSessionId[$user] = $response['sessionId']; } } /** * @Then /^user "([^"]*)" leaves room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userExitsRoom(string $user, string $identifier, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest('DELETE', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/participants/active'); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" removes themselves from room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userLeavesRoom(string $user, string $identifier, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest('DELETE', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/participants/self'); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" removes "([^"]*)" from room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $toRemove * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userRemovesUserFromRoom(string $user, string $toRemove, string $identifier, int $statusCode, string$apiVersion): void { if ($toRemove === 'stranger') { $attendeeId = 123456789; } else { $attendeeId = $this->getAttendeeId('users', $toRemove, $identifier, $statusCode === 200 ? $user : null); } $this->setCurrentUser($user); $this->sendRequest( 'DELETE', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/attendees', new TableNode([['attendeeId', $attendeeId]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" removes (user|group|email) "([^"]*)" from room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $actorType * @param string $actorId * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userRemovesAttendeeFromRoom(string $user, string $actorType, string $actorId, string $identifier, int $statusCode, string$apiVersion): void { if ($actorId === 'stranger') { $attendeeId = 123456789; } else { $attendeeId = $this->getAttendeeId($actorType . 's', $actorId, $identifier, $statusCode === 200 ? $user : null); } $this->setCurrentUser($user); $this->sendRequest( 'DELETE', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/attendees', new TableNode([['attendeeId', $attendeeId]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" deletes room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userDeletesRoom(string $user, string $identifier, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest('DELETE', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier]); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" gets room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userGetsRoom(string $user, string $identifier, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier]); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" renames room "([^"]*)" to "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param string $newName * @param int $statusCode * @param string $apiVersion */ public function userRenamesRoom(string $user, string $identifier, string $newName, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest( 'PUT', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier], new TableNode([['roomName', $newName]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @When /^user "([^"]*)" sets description for room "([^"]*)" to "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param string $description * @param int $statusCode * @param string $apiVersion * @param TableNode */ public function userSetsDescriptionForRoomTo(string $user, string $identifier, string $description, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest( 'PUT', '/apps/spreed/api/' .$apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/description', new TableNode([['description', $description]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @When /^user "([^"]*)" sets password "([^"]*)" for room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $password * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userSetsTheRoomPassword(string $user, string $password, string $identifier, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest( 'PUT', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/password', new TableNode([['password', $password]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @When /^user "([^"]*)" sets lobby state for room "([^"]*)" to "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param string $lobbyStateString * @param int $statusCode * @param string $apiVersion */ public function userSetsLobbyStateForRoomTo(string $user, string $identifier, string $lobbyStateString, int $statusCode, string $apiVersion): void { if ($lobbyStateString === 'no lobby') { $lobbyState = 0; } elseif ($lobbyStateString === 'non moderators') { $lobbyState = 1; } else { Assert::fail('Invalid lobby state'); } $this->setCurrentUser($user); $this->sendRequest( 'PUT', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/webinar/lobby', new TableNode([['state', $lobbyState]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @When /^user "([^"]*)" sets SIP state for room "([^"]*)" to "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param string $SIPStateString * @param int $statusCode * @param string $apiVersion */ public function userSetsSIPStateForRoomTo(string $user, string $identifier, string $SIPStateString, int $statusCode, string $apiVersion): void { if ($SIPStateString === 'disabled') { $SIPState = 0; } elseif ($SIPStateString === 'enabled') { $SIPState = 1; } else { Assert::fail('Invalid SIP state'); } $this->setCurrentUser($user); $this->sendRequest( 'PUT', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/webinar/sip', new TableNode([['state', $SIPState]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" makes room "([^"]*)" (public|private) with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param string $newType * @param int $statusCode * @param string $apiVersion */ public function userChangesTypeOfTheRoom(string $user, string $identifier, string $newType, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest( $newType === 'public' ? 'POST' : 'DELETE', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/public' ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" (locks|unlocks) room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $newState * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userChangesReadOnlyStateOfTheRoom(string $user, string $newState, string $identifier, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest( 'PUT', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/read-only', new TableNode([['state', $newState === 'unlocks' ? 0 : 1]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" allows listing room "([^"]*)" for "(none|users|all|\d+)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param string|int $newState * @param int $statusCode * @param string $apiVersion */ public function userChangesListableScopeOfTheRoom(string $user, string $identifier, $newState, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); if ($newState === 'none') { $newStateValue = 0; // Room::LISTABLE_NONE } elseif ($newState === 'users') { $newStateValue = 1; // Room::LISTABLE_USERS } elseif ($newState === 'all') { $newStateValue = 2; // Room::LISTABLE_ALL } elseif (is_numeric($newState)) { $newStateValue = (int)$newState; } else { Assert::fail('Invalid listable scope value'); } $this->sendRequest( 'PUT', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/listable', new TableNode([['scope', $newStateValue]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" adds (user|group|email|circle|remote) "([^"]*)" to room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $newType * @param string $newId * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userAddAttendeeToRoom(string $user, string $newType, string $newId, string $identifier, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); if ($newType === 'remote') { $newId .= '@' . $this->baseRemoteUrl; } $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/participants', new TableNode([ ['source', $newType . 's'], ['newParticipant', $newId], ]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" (promotes|demotes) "([^"]*)" in room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $isPromotion * @param string $participant * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userPromoteDemoteInRoom(string $user, string $isPromotion, string $participant, string $identifier, int $statusCode, string $apiVersion): void { if ($participant === 'stranger') { $attendeeId = 123456789; } elseif (strpos($participant, 'guest') === 0) { $sessionId = self::$userToSessionId[$participant]; $attendeeId = $this->getAttendeeId('guests', sha1($sessionId), $identifier, $statusCode === 200 ? $user : null); } else { $attendeeId = $this->getAttendeeId('users', $participant, $identifier, $statusCode === 200 ? $user : null); } $requestParameters = [['attendeeId', $attendeeId]]; $this->setCurrentUser($user); $this->sendRequest( $isPromotion === 'promotes' ? 'POST' : 'DELETE', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/moderators', new TableNode($requestParameters) ); $this->assertStatusCode($this->response, $statusCode); } /** * @When /^user "([^"]*)" sets permissions for "([^"]*)" in room "([^"]*)" to "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $participant * @param string $identifier * @param string $permissionsString * @param int $statusCode * @param string $apiVersion */ public function userSetsPermissionsForInRoomTo(string $user, string $participant, string $identifier, string $permissionsString, int $statusCode, string $apiVersion): void { if ($participant === 'stranger') { $attendeeId = 123456789; } elseif (strpos($participant, 'guest') === 0) { $sessionId = self::$userToSessionId[$participant]; $attendeeId = $this->getAttendeeId('guests', sha1($sessionId), $identifier, $statusCode === 200 ? $user : null); } else { $attendeeId = $this->getAttendeeId('users', $participant, $identifier, $statusCode === 200 ? $user : null); } $permissions = $this->mapPermissionsTestInput($permissionsString); $requestParameters = [ ['attendeeId', $attendeeId], ['permissions', $permissions], ['method', 'set'], ]; $this->setCurrentUser($user); $this->sendRequest( 'PUT', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/attendees/permissions', new TableNode($requestParameters) ); $this->assertStatusCode($this->response, $statusCode); } /** * @When /^user "([^"]*)" sets (call|default) permissions for room "([^"]*)" to "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $mode * @param string $identifier * @param string $permissionsString * @param int $statusCode * @param string $apiVersion */ public function userSetsPermissionsForRoomTo(string $user, string $mode, string $identifier, string $permissionsString, int $statusCode, string $apiVersion): void { $permissions = $this->mapPermissionsTestInput($permissionsString); $requestParameters = [ ['permissions', $permissions], ]; $this->setCurrentUser($user); $this->sendRequest( 'PUT', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/permissions/' . $mode, new TableNode($requestParameters) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" joins call "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param int $statusCode * @param string $apiVersion * @param TableNode|null $formData */ public function userJoinsCall(string $user, string $identifier, int $statusCode, string $apiVersion, TableNode $formData = null): void { $this->setCurrentUser($user); $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/call/' . self::$identifierToToken[$identifier], $formData ); $this->assertStatusCode($this->response, $statusCode); $response = $this->getDataFromResponse($this->response); if (array_key_exists('sessionId', $response)) { // In the chat guest users are identified by their sessionId. The // sessionId is larger than the size of the actorId column in the // database, though, so the ID stored in the database and returned // in chat messages is a hashed version instead. self::$sessionIdToUser[sha1($response['sessionId'])] = $user; self::$userToSessionId[$user] = $response['sessionId']; } } /** * @Then /^user "([^"]*)" updates call flags in room "([^"]*)" to "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param string $flags * @param int $statusCode * @param string $apiVersion */ public function userUpdatesCallFlagsInRoomTo(string $user, string $identifier, string $flags, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest( 'PUT', '/apps/spreed/api/' . $apiVersion . '/call/' . self::$identifierToToken[$identifier], new TableNode([['flags', $flags]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" leaves call "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userLeavesCall(string $user, string $identifier, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest('DELETE', '/apps/spreed/api/' . $apiVersion . '/call/' . self::$identifierToToken[$identifier]); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" ends call "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userEndsCall(string $user, string $identifier, int $statusCode, string $apiVersion): void { $requestParameters = [ ['all', true], ]; $this->setCurrentUser($user); $this->sendRequest( 'DELETE', '/apps/spreed/api/' . $apiVersion . '/call/' . self::$identifierToToken[$identifier], new TableNode($requestParameters) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" sees (\d+) peers in call "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param int $numPeers * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userSeesPeersInCall(string $user, int $numPeers, string $identifier, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/call/' . self::$identifierToToken[$identifier]); $this->assertStatusCode($this->response, $statusCode); if ($statusCode === 200) { $response = $this->getDataFromResponse($this->response); Assert::assertCount((int) $numPeers, $response); } else { Assert::assertEquals((int) $numPeers, 0); } } /** * @Then /^user "([^"]*)" sends message "([^"]*)" to room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $message * @param string $identifier * @param string $statusCode * @param string $apiVersion */ public function userSendsMessageToRoom($user, $message, $identifier, $statusCode, $apiVersion = 'v1') { $this->setCurrentUser($user); $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier], new TableNode([['message', $message]]) ); $this->assertStatusCode($this->response, $statusCode); sleep(1); // make sure Postgres manages the order of the messages $response = $this->getDataFromResponse($this->response); if (isset($response['id'])) { self::$messages[$message] = $response['id']; } } /** * @Then /^user "([^"]*)" shares rich-object "([^"]*)" "([^"]*)" '([^']*)' to room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $type * @param string $id * @param string $metaData * @param string $identifier * @param string $statusCode * @param string $apiVersion */ public function userSharesRichObjectToRoom($user, $type, $id, $metaData, $identifier, $statusCode, $apiVersion = 'v1') { $this->setCurrentUser($user); $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/share', new TableNode([ ['objectType', $type], ['objectId', $id], ['metaData', $metaData], ]) ); $this->assertStatusCode($this->response, $statusCode); sleep(1); // make sure Postgres manages the order of the messages $response = $this->getDataFromResponse($this->response); if (isset($response['id'])) { self::$messages['shared::' . $type . '::' . $id] = $response['id']; } } /** * @Then /^user "([^"]*)" deletes message "([^"]*)" from room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $message * @param string $identifier * @param string $statusCode * @param string $apiVersion */ public function userDeletesMessageFromRoom($user, $message, $identifier, $statusCode, $apiVersion = 'v1') { $this->setCurrentUser($user); $this->sendRequest( 'DELETE', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/' . self::$messages[$message], new TableNode([['message', $message]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" deletes chat history for room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $identifier * @param int $statusCode * @param string $apiVersion */ public function userDeletesHistoryFromRoom(string $user, string $identifier, int $statusCode, string $apiVersion = 'v1'): void { $this->setCurrentUser($user); $this->sendRequest( 'DELETE', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" reads message "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $message * @param string $identifier * @param string $statusCode * @param string $apiVersion */ public function userReadsMessageInRoom($user, $message, $identifier, $statusCode, $apiVersion = 'v1') { $this->setCurrentUser($user); $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/read', new TableNode([['lastReadMessage', self::$messages[$message]]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" marks room "([^"]*)" as unread with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $identifier * @param string $statusCode * @param string $apiVersion */ public function userMarkUnreadRoom($user, $identifier, $statusCode, $apiVersion = 'v1') { $this->setCurrentUser($user); $this->sendRequest( 'DELETE', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/read', ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" sends message "([^"]*)" with reference id "([^"]*)" to room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $message * @param string $referenceId * @param string $identifier * @param string $statusCode * @param string $apiVersion */ public function userSendsMessageWithReferenceIdToRoom($user, $message, $referenceId, $identifier, $statusCode, $apiVersion = 'v1') { $this->setCurrentUser($user); $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier], new TableNode([['message', $message], ['referenceId', $referenceId]]) ); $this->assertStatusCode($this->response, $statusCode); sleep(1); // make sure Postgres manages the order of the messages $response = $this->getDataFromResponse($this->response); if (isset($response['id'])) { self::$messages[$message] = $response['id']; } Assert::assertStringStartsWith($response['referenceId'], $referenceId); } /** * @Then /^user "([^"]*)" sends reply "([^"]*)" on message "([^"]*)" to room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $reply * @param string $message * @param string $identifier * @param string $statusCode * @param string $apiVersion */ public function userSendsReplyToRoom($user, $reply, $message, $identifier, $statusCode, $apiVersion = 'v1') { $replyTo = self::$messages[$message]; $this->setCurrentUser($user); $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier], new TableNode([['message', $reply], ['replyTo', $replyTo]]) ); $this->assertStatusCode($this->response, $statusCode); sleep(1); // make sure Postgres manages the order of the messages $response = $this->getDataFromResponse($this->response); if (isset($response['id'])) { self::$messages[$reply] = $response['id']; } } /** * @Then /^user "([^"]*)" sees the following messages in room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $identifier * @param string $statusCode * @param string $apiVersion */ public function userSeesTheFollowingMessagesInRoom($user, $identifier, $statusCode, $apiVersion = 'v1', TableNode $formData = null) { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '?lookIntoFuture=0'); $this->assertStatusCode($this->response, $statusCode); $this->compareDataResponse($formData); } /** * @Then /^user "([^"]*)" sees the following shared media in room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $identifier * @param string $statusCode * @param string $apiVersion */ public function userSeesTheFollowingSharedMediaInRoom($user, $identifier, $statusCode, $apiVersion = 'v1', TableNode $formData = null): void { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/share'); $this->assertStatusCode($this->response, $statusCode); $this->compareDataResponse($formData); } /** * @Then /^user "([^"]*)" received a system messages in room "([^"]*)" to delete "([^"]*)"(?: \((v1)\))?$/ * * @param string $user * @param string $identifier * @param string $message * @param string $apiVersion */ public function userReceivedDeleteMessage($user, $identifier, $message, $apiVersion = 'v1') { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '?lookIntoFuture=0'); $this->assertStatusCode($this->response, 200); $actual = $this->getDataFromResponse($this->response); foreach ($actual as $m) { if ($m['systemMessage'] === 'message_deleted') { if (isset($m['parent']['id']) && $m['parent']['id'] === self::$messages[$message]) { return; } } } Assert::fail('Missing message_deleted system message for "' . $message . '"'); } /** * @Then /^user "([^"]*)" sees the following messages in room "([^"]*)" starting with "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $identifier * @param string $knownMessage * @param string $statusCode * @param string $apiVersion * @param TableNode|null $formData */ public function userAwaitsTheFollowingMessagesInRoom($user, $identifier, $knownMessage, $statusCode, $apiVersion = 'v1', TableNode $formData = null) { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '?lookIntoFuture=1&includeLastKnown=1&lastKnownMessageId=' . self::$messages[$knownMessage]); $this->assertStatusCode($this->response, $statusCode); $this->compareDataResponse($formData); } /** * @param TableNode|null $formData */ protected function compareDataResponse(TableNode $formData = null) { $actual = $this->getDataFromResponse($this->response); $messages = []; array_map(function (array $message) use (&$messages) { // Filter out system messages if ($message['systemMessage'] === '') { $messages[] = $message; } }, $actual); foreach ($messages as $message) { // Include the received messages in the list of messages used for // replies; this is needed to get special messages not explicitly // sent like those for shared files. self::$messages[$message['message']] = $message['id']; if ($message['message'] === '{file}' && isset($message['messageParameters']['file']['name'])) { self::$messages['shared::file::' . $message['messageParameters']['file']['name']] = $message['id']; } } if ($formData === null) { Assert::assertEmpty($messages); return; } $includeParents = in_array('parentMessage', $formData->getRow(0), true); $includeReferenceId = in_array('referenceId', $formData->getRow(0), true); $includeReactions = in_array('reactions', $formData->getRow(0), true); $count = count($formData->getHash()); Assert::assertCount($count, $messages, 'Message count does not match'); for ($i = 0; $i < $count; $i++) { if ($formData->getHash()[$i]['messageParameters'] === '"IGNORE"') { $messages[$i]['messageParameters'] = 'IGNORE'; } } Assert::assertEquals($formData->getHash(), array_map(function ($message) use ($includeParents, $includeReferenceId, $includeReactions) { $data = [ 'room' => self::$tokenToIdentifier[$message['token']], 'actorType' => $message['actorType'], 'actorId' => ($message['actorType'] === 'guests')? self::$sessionIdToUser[$message['actorId']]: $message['actorId'], 'actorDisplayName' => $message['actorDisplayName'], // TODO test timestamp; it may require using Runkit, php-timecop // or something like that. 'message' => $message['message'], 'messageParameters' => json_encode($message['messageParameters']), ]; if ($includeParents) { $data['parentMessage'] = $message['parent']['message'] ?? ''; } if ($includeReferenceId) { $data['referenceId'] = $message['referenceId']; } if ($includeReactions) { $data['reactions'] = json_encode($message['reactions'], JSON_UNESCAPED_UNICODE); } return $data; }, $messages)); } /** * @Then /^user "([^"]*)" sees the following system messages in room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $identifier * @param string $statusCode * @param string $apiVersion */ public function userSeesTheFollowingSystemMessagesInRoom($user, $identifier, $statusCode, $apiVersion = 'v1', TableNode $formData = null) { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '?lookIntoFuture=0'); $this->assertStatusCode($this->response, $statusCode); $messages = $this->getDataFromResponse($this->response); $messages = array_filter($messages, function (array $message) { return $message['systemMessage'] !== ''; }); // Fix index gaps after the array_filter above $messages = array_values($messages); foreach ($messages as $systemMessage) { // Include the received system messages in the list of messages used // for replies. self::$messages[$systemMessage['systemMessage']] = $systemMessage['id']; } if ($formData === null) { Assert::assertEmpty($messages); return; } Assert::assertCount(count($formData->getHash()), $messages, 'Message count does not match'); Assert::assertEquals($formData->getHash(), array_map(function ($message) { return [ 'room' => self::$tokenToIdentifier[$message['token']], 'actorType' => (string) $message['actorType'], 'actorId' => ($message['actorType'] === 'guests')? self::$sessionIdToUser[$message['actorId']]: (string) $message['actorId'], 'actorDisplayName' => (string) $message['actorDisplayName'], 'systemMessage' => (string) $message['systemMessage'], ]; }, $messages)); } /** * @Then /^user "([^"]*)" gets the following candidate mentions in room "([^"]*)" for "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $identifier * @param string $search * @param string $statusCode * @param string $apiVersion * @param TableNode|null $formData */ public function userGetsTheFollowingCandidateMentionsInRoomFor($user, $identifier, $search, $statusCode, $apiVersion = 'v1', TableNode $formData = null) { $this->setCurrentUser($user); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/mentions?search=' . $search); $this->assertStatusCode($this->response, $statusCode); $mentions = $this->getDataFromResponse($this->response); if ($formData === null) { Assert::assertEmpty($mentions); return; } Assert::assertCount(count($formData->getHash()), $mentions, 'Mentions count does not match'); usort($mentions, function ($a, $b) { if ($a['source'] === $b['source']) { return $a['label'] <=> $b['label']; } return $a['source'] <=> $b['source']; }); $expected = $formData->getHash(); usort($expected, function ($a, $b) { if ($a['source'] === $b['source']) { return $a['label'] <=> $b['label']; } return $a['source'] <=> $b['source']; }); foreach ($expected as $key => $row) { if ($row['id'] === 'GUEST_ID') { Assert::assertRegExp('/^guest\/[0-9a-f]{40}$/', $mentions[$key]['id']); $mentions[$key]['id'] = 'GUEST_ID'; } Assert::assertEquals($row, $mentions[$key]); } } /** * @Then /^guest "([^"]*)" sets name to "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $name * @param string $identifier * @param string $statusCode * @param string $apiVersion */ public function guestSetsName($user, $name, $identifier, $statusCode, $apiVersion = 'v1') { $this->setCurrentUser($user); $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/guest/' . self::$identifierToToken[$identifier] . '/name', new TableNode([['displayName', $name]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^last response has (no) last common read message header$/ * * @param string $no */ public function hasNoChatLastCommonReadHeader($no) { Assert::assertArrayNotHasKey('X-Chat-Last-Common-Read', $this->response->getHeaders(), 'X-Chat-Last-Common-Read is set to ' . ($this->response->getHeader('X-Chat-Last-Common-Read')[0] ?? '0')); } /** * @Then /^last response has last common read message header (set to|less than) "([^"]*)"$/ * * @param string $setOrLower * @param string $message */ public function hasChatLastCommonReadHeader($setOrLower, $message) { Assert::assertArrayHasKey('X-Chat-Last-Common-Read', $this->response->getHeaders()); if ($setOrLower === 'set to') { Assert::assertEquals(self::$messages[$message], $this->response->getHeader('X-Chat-Last-Common-Read')[0]); } else { // Less than might be required for the first message, because the last read message before is the join/room creation message and we don't know that ID Assert::assertLessThan(self::$messages[$message], $this->response->getHeader('X-Chat-Last-Common-Read')[0]); } } /** * @Then /^user "([^"]*)" sets setting "([^"]*)" to "([^"]*)" with (\d+)(?: \((v1)\))?$/ * * @param string $user * @param string $setting * @param string $value * @param string $statusCode * @param string $apiVersion */ public function userSetting($user, $setting, $value, $statusCode, $apiVersion = 'v1') { $this->setCurrentUser($user); $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/settings/user', new TableNode([['key', $setting], ['value', $value]]) ); $this->assertStatusCode($this->response, $statusCode); } /** * @Then /^user "([^"]*)" has capability "([^"]*)" set to "([^"]*)"$/ * * @param string $user * @param string $capability * @param string $value */ public function userCheckCapability($user, $capability, $value) { $this->setCurrentUser($user); $this->sendRequest( 'GET', '/cloud/capabilities' ); $data = $this->getDataFromResponse($this->response); $capabilities = $data['capabilities']; $keys = explode('=>', $capability); $finalKey = array_pop($keys); $cur = $capabilities; foreach ($keys as $key) { Assert::assertArrayHasKey($key, $cur); $cur = $cur[$key]; } Assert::assertEquals($value, $cur[$finalKey]); } /** * Parses the xml answer to get the array of users returned. * @param ResponseInterface $response * @return array */ protected function getDataFromResponse(ResponseInterface $response) { $jsonBody = json_decode($response->getBody()->getContents(), true); return $jsonBody['ocs']['data']; } /** * @Then /^status code is ([0-9]*)$/ * * @param int $statusCode */ public function isStatusCode($statusCode) { $this->assertStatusCode($this->response, $statusCode); } /** * @Given the following :appId app config is set * * @param TableNode $formData */ public function setAppConfig(string $appId, TableNode $formData): void { $currentUser = $this->currentUser; $this->setCurrentUser('admin'); foreach ($formData->getRows() as $row) { $this->sendRequest('POST', '/apps/provisioning_api/api/v1/config/apps/' . $appId . '/' . $row[0], [ 'value' => $row[1], ]); $this->changedConfigs[$appId][] = $row[0]; } $this->setCurrentUser($currentUser); } /** * @Given /^guest accounts can be created$/ * * @param TableNode $formData */ public function allowGuestAccountsCreation(): void { $currentUser = $this->currentUser; $this->setCurrentUser('admin'); // save old state and restore at the end $this->sendRequest('GET', '/cloud/apps?filter=enabled'); $this->assertStatusCode($this->response, 200); $data = $this->getDataFromResponse($this->response); $this->guestsAppWasEnabled = in_array('guests', $data['apps'], true); if (!$this->guestsAppWasEnabled) { // enable guests app /* $this->sendRequest('POST', '/cloud/apps/guests'); $this->assertStatusCode($this->response, 200); */ // seems using provisioning API doesn't create tables... $this->runOcc(['app:enable', 'guests']); } // save previously set whitelist $this->sendRequest('GET', '/apps/provisioning_api/api/v1/config/apps/guests/whitelist'); $this->assertStatusCode($this->response, 200); $this->guestsOldWhitelist = $this->getDataFromResponse($this->response)['data']; // set whitelist to allow spreed only $this->sendRequest('POST', '/apps/provisioning_api/api/v1/config/apps/guests/whitelist', [ 'value' => 'spreed', ]); $this->setCurrentUser($currentUser); } /** * @BeforeScenario * @AfterScenario */ public function resetSpreedAppData() { $currentUser = $this->currentUser; $this->setCurrentUser('admin'); $this->sendRequest('DELETE', '/apps/spreedcheats/'); foreach ($this->changedConfigs as $appId => $configs) { foreach ($configs as $config) { $this->sendRequest('DELETE', '/apps/provisioning_api/api/v1/config/apps/' . $appId . '/' . $config); } } $this->setCurrentUser($currentUser); } /** * @AfterScenario */ public function resetGuestsAppState() { if ($this->guestsAppWasEnabled === null) { // guests app was not touched return; } $currentUser = $this->currentUser; $this->setCurrentUser('admin'); if ($this->guestsOldWhitelist) { // restore old whitelist $this->sendRequest('POST', '/apps/provisioning_api/api/v1/config/apps/guests/whitelist', [ 'value' => $this->guestsOldWhitelist, ]); } else { // restore to default $this->sendRequest('DELETE', '/apps/provisioning_api/api/v1/config/apps/guests/whitelist'); } // restore app's enabled state $this->sendRequest($this->guestsAppWasEnabled ? 'POST' : 'DELETE', '/cloud/apps/guests'); $this->setCurrentUser($currentUser); $this->guestsAppWasEnabled = null; } /* * User management */ /** * @Given /^as user "([^"]*)"$/ * @param string $user */ public function setCurrentUser($user) { $this->currentUser = $user; } /** * @Given /^user "([^"]*)" exists$/ * @param string $user */ public function assureUserExists($user) { $response = $this->userExists($user); if ($response->getStatusCode() !== 200) { $this->createUser($user); // Set a display name different than the user ID to be able to // ensure in the tests that the right value was returned. $this->setUserDisplayName($user); $response = $this->userExists($user); $this->assertStatusCode($response, 200); } } /** * @Given /^user "([^"]*)" is a guest account user/ * @param string $email email address */ public function createGuestUser($email) { $currentUser = $this->currentUser; $this->setCurrentUser('admin'); // in case it exists $this->deleteUser($email); $lastCode = $this->runOcc([ 'guests:add', // creator user 'admin', // email $email, '--display-name', $email . '-displayname', '--password-from-env', ], [ 'OC_PASS' => self::TEST_PASSWORD, ]); Assert::assertEquals(0, $lastCode, 'Guest creation succeeded for ' . $email); $this->createdGuestAccountUsers[] = $email; $this->setCurrentUser($currentUser); } private function userExists($user) { $currentUser = $this->currentUser; $this->setCurrentUser('admin'); $this->sendRequest('GET', '/cloud/users/' . $user); $this->setCurrentUser($currentUser); return $this->response; } private function createUser($user) { $currentUser = $this->currentUser; $this->setCurrentUser('admin'); $this->sendRequest('POST', '/cloud/users', [ 'userid' => $user, 'password' => self::TEST_PASSWORD, ]); $this->assertStatusCode($this->response, 200, 'Failed to create user'); //Quick hack to login once with the current user $this->setCurrentUser($user); $this->sendRequest('GET', '/cloud/users' . '/' . $user); $this->assertStatusCode($this->response, 200, 'Failed to do first login'); $this->createdUsers[] = $user; $this->setCurrentUser($currentUser); } /** * @Given /^user "([^"]*)" is deleted$/ * @param string $user */ public function userIsDeleted($user) { $deleted = false; $this->deleteUser($user); $response = $this->userExists($user); $deleted = $response->getStatusCode() === 404; if (!$deleted) { Assert::fail("User $user exists"); } } private function deleteUser($user) { $currentUser = $this->currentUser; $this->setCurrentUser('admin'); $this->sendRequest('DELETE', '/cloud/users/' . $user); $this->setCurrentUser($currentUser); unset($this->createdUsers[array_search($user, $this->createdUsers, true)]); return $this->response; } private function setUserDisplayName($user) { $currentUser = $this->currentUser; $this->setCurrentUser('admin'); $this->sendRequest('PUT', '/cloud/users/' . $user, [ 'key' => 'displayname', 'value' => $user . '-displayname' ]); $this->setCurrentUser($currentUser); } /** * @Given /^group "([^"]*)" exists$/ * @param string $group */ public function assureGroupExists($group) { $currentUser = $this->currentUser; $this->setCurrentUser('admin'); $this->sendRequest('POST', '/cloud/groups', [ 'groupid' => $group, ]); $jsonBody = json_decode($this->response->getBody()->getContents(), true); if (isset($jsonBody['ocs']['meta'])) { // 102 = group exists // 200 = created with success Assert::assertContains( $jsonBody['ocs']['meta']['statuscode'], [102, 200], $jsonBody['ocs']['meta']['message'] ); } else { throw new \Exception('Invalid response when create group'); } $this->setCurrentUser($currentUser); $this->createdGroups[] = $group; } private function deleteGroup($group) { $currentUser = $this->currentUser; $this->setCurrentUser('admin'); $this->sendRequest('DELETE', '/cloud/groups/' . $group); $this->setCurrentUser($currentUser); unset($this->createdGroups[array_search($group, $this->createdGroups, true)]); } /** * @When /^user "([^"]*)" is member of group "([^"]*)"$/ * @param string $user * @param string $group */ public function addingUserToGroup($user, $group) { $currentUser = $this->currentUser; $this->setCurrentUser('admin'); $this->sendRequest('POST', "/cloud/users/$user/groups", [ 'groupid' => $group, ]); $this->assertStatusCode($this->response, 200); $this->setCurrentUser($currentUser); } /** * @When /^user "([^"]*)" is not member of group "([^"]*)"$/ * @param string $user * @param string $group */ public function removeUserFromGroup($user, $group) { $currentUser = $this->currentUser; $this->setCurrentUser('admin'); $this->sendRequest('DELETE', "/cloud/users/$user/groups", [ 'groupid' => $group, ]); $this->assertStatusCode($this->response, 200); $this->setCurrentUser($currentUser); } /** * @Given /^user "([^"]*)" (delete react|react) with "([^"]*)" on message "([^"]*)" to room "([^"]*)" with (\d+)(?: \((v1)\))?$/ */ public function userReactWithOnMessageToRoomWith(string $user, string $action, string $reaction, string $message, string $identifier, int $statusCode, string $apiVersion = 'v1', TableNode $formData = null): void { $token = self::$identifierToToken[$identifier]; $messageId = self::$messages[$message]; $this->setCurrentUser($user); $verb = $action === 'react' ? 'POST' : 'DELETE'; $this->sendRequest($verb, '/apps/spreed/api/' . $apiVersion . '/reaction/' . $token . '/' . $messageId, [ 'reaction' => $reaction ]); $this->assertStatusCode($this->response, $statusCode); $this->assertReactionList($formData); } /** * @Given /^user "([^"]*)" retrieve reactions "([^"]*)" of message "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v1)\))?$/ */ public function userRetrieveReactionsOfMessageInRoomWith(string $user, string $reaction, string $message, string $identifier, int $statusCode, string $apiVersion = 'v1', TableNode $formData): void { $token = self::$identifierToToken[$identifier]; $messageId = self::$messages[$message]; $this->setCurrentUser($user); $reaction = $reaction !== 'all' ? '?reaction=' . $reaction : ''; $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/reaction/' . $token . '/' . $messageId . $reaction); $this->assertStatusCode($this->response, $statusCode); $this->assertReactionList($formData); } private function assertReactionList(?TableNode $formData): void { $expected = []; if (!$formData instanceof TableNode) { return; } foreach ($formData->getHash() as $row) { $reaction = $row['reaction']; unset($row['reaction']); $expected[$reaction][] = $row; } $result = $this->getDataFromResponse($this->response); $result = array_map(static function ($reaction, $list) use ($expected): array { $list = array_map(function ($reaction) { unset($reaction['timestamp']); return $reaction; }, $list); Assert::assertArrayHasKey($reaction, $expected, 'Not expected reaction: ' . $reaction); Assert::assertCount(count($list), $expected[$reaction], 'Reaction count by type does not match'); usort($expected[$reaction], [self::class, 'sortAttendees']); usort($list, [self::class, 'sortAttendees']); Assert::assertEquals($expected[$reaction], $list, 'Reaction list by type does not match'); return $list; }, array_keys($result), array_values($result)); Assert::assertCount(count($expected), $result, 'Reaction count does not match'); } /* * Requests */ /** * @Given /^user "([^"]*)" logs in$/ * @param string $user */ public function userLogsIn(string $user) { $loginUrl = $this->baseUrl . '/login'; $cookieJar = $this->getUserCookieJar($user); // Request a new session and extract CSRF token $client = new Client(); $this->response = $client->get( $loginUrl, [ 'cookies' => $cookieJar, ] ); $requestToken = $this->extractRequestTokenFromResponse($this->response); // Login and extract new token $password = ($user === 'admin') ? 'admin' : self::TEST_PASSWORD; $client = new Client(); $this->response = $client->post( $loginUrl, [ 'form_params' => [ 'user' => $user, 'password' => $password, 'requesttoken' => $requestToken, ], 'cookies' => $cookieJar, ] ); $this->assertStatusCode($this->response, 200); } /** * @param ResponseInterface $response * @return string */ private function extractRequestTokenFromResponse(ResponseInterface $response): string { return substr(preg_replace('/(.*)data-requesttoken="(.*)">(.*)/sm', '\2', $response->getBody()->getContents()), 0, 89); } /** * @When /^sending "([^"]*)" to "([^"]*)" with$/ * @param string $verb * @param string $url * @param TableNode|array|null $body * @param array $headers */ public function sendRequest($verb, $url, $body = null, array $headers = []) { $fullUrl = $this->baseUrl . 'ocs/v2.php' . $url; $client = new Client(); $options = ['cookies' => $this->getUserCookieJar($this->currentUser)]; if ($this->currentUser === 'admin') { $options['auth'] = ['admin', 'admin']; } elseif (strpos($this->currentUser, 'guest') !== 0) { $options['auth'] = [$this->currentUser, self::TEST_PASSWORD]; } if ($body instanceof TableNode) { $fd = $body->getRowsHash(); $options['form_params'] = $fd; } elseif (is_array($body)) { $options['form_params'] = $body; } $options['headers'] = array_merge($headers, [ 'OCS-ApiRequest' => 'true', 'Accept' => 'application/json', ]); try { $this->response = $client->{$verb}($fullUrl, $options); } catch (ClientException $ex) { $this->response = $ex->getResponse(); } catch (\GuzzleHttp\Exception\ServerException $ex) { $this->response = $ex->getResponse(); } } protected function getUserCookieJar($user) { if (!isset($this->cookieJars[$user])) { $this->cookieJars[$user] = new CookieJar(); } return $this->cookieJars[$user]; } /** * @param ResponseInterface $response * @param int $statusCode * @param string $message */ protected function assertStatusCode(ResponseInterface $response, int $statusCode, string $message = '') { Assert::assertEquals($statusCode, $response->getStatusCode(), $message); } }