. * */ namespace OCA\Talk\Tests\php\Signaling; use OCA\Talk\AppInfo\Application; use OCA\Talk\Chat\CommentsManager; use OCA\Talk\Config; use OCA\Talk\Events\SignalingRoomPropertiesEvent; use OCA\Talk\Manager; use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Signaling\BackendNotifier; use OCA\Talk\TalkSession; use OCA\Talk\Webinary; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; use OCP\Http\Client\IClientService; use OCP\IGroupManager; use OCP\IL10N; use OCP\ILogger; use OCP\IUser; use OCP\IUserManager; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; use PHPUnit\Framework\MockObject\MockObject; class CustomBackendNotifier extends BackendNotifier { private $requests = []; public function getRequests(): array { return $this->requests; } public function clearRequests() { $this->requests = []; } protected function doRequest(string $url, array $params): void { $this->requests[] = [ 'url' => $url, 'params' => $params, ]; } } /** * @group DB */ class BackendNotifierTest extends \Test\TestCase { /** @var Config */ private $config; /** @var ISecureRandom */ private $secureRandom; /** @var ITimeFactory|MockObject */ private $timeFactory; /** @var \OCA\Talk\Signaling\Manager|MockObject */ private $signalingManager; /** @var CustomBackendNotifier */ private $controller; /** @var Manager */ private $manager; /** @var string */ private $userId; /** @var string */ private $signalingSecret; /** @var string */ private $baseUrl; /** @var Application */ protected $app; /** @var BackendNotifier */ protected $originalBackendNotifier; /** @var IEventDispatcher */ private $dispatcher; public function setUp(): void { parent::setUp(); $this->userId = 'testUser'; $this->secureRandom = \OC::$server->getSecureRandom(); $this->timeFactory = $this->createMock(ITimeFactory::class); $groupManager = $this->createMock(IGroupManager::class); $config = \OC::$server->getConfig(); $this->signalingSecret = 'the-signaling-secret'; $this->baseUrl = 'https://localhost/signaling'; $config->setAppValue('spreed', 'signaling_servers', json_encode([ 'secret' => $this->signalingSecret, 'servers' => [ [ 'server' => $this->baseUrl, ], ], ])); $this->signalingManager = $this->createMock(\OCA\Talk\Signaling\Manager::class); $this->signalingManager->expects($this->any()) ->method('getSignalingServerForConversation') ->willReturn(['server' => $this->baseUrl]); $this->config = new Config($config, $this->secureRandom, $groupManager, $this->timeFactory); $this->recreateBackendNotifier(); $this->app = new Application(); $this->app->register(); $this->originalBackendNotifier = $this->app->getContainer()->query(BackendNotifier::class); $this->app->getContainer()->registerService(BackendNotifier::class, function () { return $this->controller; }); $dbConnection = \OC::$server->getDatabaseConnection(); $this->dispatcher = \OC::$server->query(IEventDispatcher::class); $this->manager = new Manager( $dbConnection, $config, $this->secureRandom, $this->createMock(IUserManager::class), $this->createMock(CommentsManager::class), $this->createMock(TalkSession::class), $this->dispatcher, $this->timeFactory, $this->createMock(IHasher::class), $this->createMock(IL10N::class) ); } public function tearDown(): void { $config = \OC::$server->getConfig(); $config->deleteAppValue('spreed', 'signaling_servers'); $this->app->getContainer()->registerService(BackendNotifier::class, function () { return $this->originalBackendNotifier; }); parent::tearDown(); } private function recreateBackendNotifier() { $this->controller = new CustomBackendNotifier( $this->config, $this->createMock(ILogger::class), $this->createMock(IClientService::class), $this->secureRandom, $this->signalingManager ); } private function calculateBackendChecksum($data, $random) { if (empty($random) || strlen($random) < 32) { return false; } return hash_hmac('sha256', $random . $data, $this->signalingSecret); } private function validateBackendRequest($expectedUrl, $request) { $this->assertTrue(isset($request)); $this->assertEquals($expectedUrl, $request['url']); $headers = $request['params']['headers']; $this->assertEquals('application/json', $headers['Content-Type']); $random = $headers['Spreed-Signaling-Random']; $checksum = $headers['Spreed-Signaling-Checksum']; $body = $request['params']['body']; $this->assertEquals($this->calculateBackendChecksum($body, $random), $checksum); return $body; } private function assertMessageWasSent(Room $room, array $message): void { $requests = $this->controller->getRequests(); $bodies = array_map(function ($request) use ($room) { return json_decode($this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request), true); }, $requests); $bodies = array_filter($bodies, function (array $body) use ($message) { return $body['type'] === $message['type']; }); $bodies = array_map([$this, 'sortParticipantUsers'], $bodies); $message = $this->sortParticipantUsers($message); $this->assertContains($message, $bodies); } private function sortParticipantUsers(array $message): array { if ($message['type'] === 'participants') { usort($message['participants']['users'], static function ($a, $b) { return [$a['userId'] ?? '', $a['participantType'], $a['sessionId'], $a['lastPing']] <=> [$b['userId'] ?? '', $b['participantType'], $b['sessionId'], $b['lastPing']] ; }); } return $message; } public function testRoomInvite() { $room = $this->manager->createPublicRoom(); $room->addUsers([ 'userId' => $this->userId, ]); $this->assertMessageWasSent($room, [ 'type' => 'invite', 'invite' => [ 'userids' => [ $this->userId, ], 'alluserids' => [ $this->userId, ], 'properties' => [ 'name' => $room->getDisplayName(''), 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, 'read-only' => Room::READ_WRITE, 'active-since' => null, ], ], ]); } public function testRoomDisinvite() { $room = $this->manager->createPublicRoom(); $room->addUsers([ 'userId' => $this->userId, ]); $this->controller->clearRequests(); /** @var IUser|MockObject $testUser */ $testUser = $this->createMock(IUser::class); $testUser->expects($this->any()) ->method('getUID') ->willReturn($this->userId); $room->removeUser($testUser, Room::PARTICIPANT_REMOVED); $this->assertMessageWasSent($room, [ 'type' => 'disinvite', 'disinvite' => [ 'userids' => [ $this->userId, ], 'alluserids' => [ ], 'properties' => [ 'name' => $room->getDisplayName(''), 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, 'read-only' => Room::READ_WRITE, 'active-since' => null, ], ], ]); } public function testRoomNameChanged() { $room = $this->manager->createPublicRoom(); $room->setName('Test room'); $this->assertMessageWasSent($room, [ 'type' => 'update', 'update' => [ 'userids' => [ ], 'properties' => [ 'name' => $room->getDisplayName(''), 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, 'read-only' => Room::READ_WRITE, 'active-since' => null, ], ], ]); } public function testRoomPasswordChanged() { $room = $this->manager->createPublicRoom(); $room->setPassword('password'); $this->assertMessageWasSent($room, [ 'type' => 'update', 'update' => [ 'userids' => [ ], 'properties' => [ 'name' => $room->getDisplayName(''), 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, 'read-only' => Room::READ_WRITE, 'active-since' => null, ], ], ]); } public function testRoomTypeChanged() { $room = $this->manager->createPublicRoom(); $room->setType(Room::GROUP_CALL); $this->assertMessageWasSent($room, [ 'type' => 'update', 'update' => [ 'userids' => [ ], 'properties' => [ 'name' => $room->getDisplayName(''), 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, 'read-only' => Room::READ_WRITE, 'active-since' => null, ], ], ]); } public function testRoomReadOnlyChanged() { $room = $this->manager->createPublicRoom(); $room->setReadOnly(Room::READ_ONLY); $this->assertMessageWasSent($room, [ 'type' => 'update', 'update' => [ 'userids' => [ ], 'properties' => [ 'name' => $room->getDisplayName(''), 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, 'read-only' => Room::READ_ONLY, 'active-since' => null, ], ], ]); } public function testRoomLobbyStateChanged() { $room = $this->manager->createPublicRoom(); $room->setLobby(Webinary::LOBBY_NON_MODERATORS, null); $this->assertMessageWasSent($room, [ 'type' => 'update', 'update' => [ 'userids' => [ ], 'properties' => [ 'name' => $room->getDisplayName(''), 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NON_MODERATORS, 'lobby-timer' => null, 'read-only' => Room::READ_WRITE, 'active-since' => null, ], ], ]); } public function testRoomDelete() { $room = $this->manager->createPublicRoom(); $room->addUsers([ 'userId' => $this->userId, ]); $room->deleteRoom(); $this->assertMessageWasSent($room, [ 'type' => 'delete', 'delete' => [ 'userids' => [ $this->userId, ], ], ]); } public function testRoomInCallChanged() { $room = $this->manager->createPublicRoom(); $userSession = 'user-session'; $room->addUsers([ 'userId' => $this->userId, 'sessionId' => $userSession, ]); $participant = $room->getParticipantBySession($userSession); $room->changeInCall($participant, Participant::FLAG_IN_CALL | Participant::FLAG_WITH_AUDIO | Participant::FLAG_WITH_VIDEO); $this->assertMessageWasSent($room, [ 'type' => 'incall', 'incall' => [ 'incall' => 7, 'changed' => [ [ 'inCall' => 7, 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::USER, 'userId' => $this->userId, ], ], 'users' => [ [ 'inCall' => 7, 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::USER, 'userId' => $this->userId, ], ], ], ]); $this->controller->clearRequests(); $guestSession = $room->joinRoomGuest(''); $guestParticipant = $room->getParticipantBySession($guestSession); $room->changeInCall($guestParticipant, Participant::FLAG_IN_CALL); $this->assertMessageWasSent($room, [ 'type' => 'incall', 'incall' => [ 'incall' => 1, 'changed' => [ [ 'inCall' => 1, 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST, ], ], 'users' => [ [ 'inCall' => 7, 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::USER, 'userId' => $this->userId, ], [ 'inCall' => 1, 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST, ], ], ], ]); $this->controller->clearRequests(); $room->changeInCall($participant, Participant::FLAG_DISCONNECTED); $this->assertMessageWasSent($room, [ 'type' => 'incall', 'incall' => [ 'incall' => 0, 'changed' => [ [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::USER, 'userId' => $this->userId, ], ], 'users' => [ [ 'inCall' => 1, 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST, ], ], ], ]); } public function testRoomPropertiesEvent(): void { $listener = static function (SignalingRoomPropertiesEvent $event) { $room = $event->getRoom(); $event->setProperty('foo', 'bar'); $event->setProperty('room', $room->getToken()); }; $this->dispatcher->addListener(Room::EVENT_BEFORE_SIGNALING_PROPERTIES, $listener); $room = $this->manager->createPublicRoom(); $this->controller->clearRequests(); $room->setName('Test room'); $this->assertMessageWasSent($room, [ 'type' => 'update', 'update' => [ 'userids' => [ ], 'properties' => [ 'name' => $room->getDisplayName(''), 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, 'read-only' => Room::READ_WRITE, 'active-since' => null, 'foo' => 'bar', 'room' => $room->getToken(), ], ], ]); } public function testParticipantsTypeChanged() { $room = $this->manager->createPublicRoom(); $userSession = 'user-session'; $room->addUsers([ 'userId' => $this->userId, 'sessionId' => $userSession, ]); $participant = $room->getParticipantBySession($userSession); $room->setParticipantType($participant, Participant::MODERATOR); $this->assertMessageWasSent($room, [ 'type' => 'participants', 'participants' => [ 'changed' => [ [ 'permissions' => ['publish-media', 'publish-screen', 'control'], 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::MODERATOR, 'userId' => $this->userId, ], ], 'users' => [ [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::MODERATOR, 'userId' => $this->userId, ], ], ], ]); $this->controller->clearRequests(); $guestSession = $room->joinRoomGuest(''); $guestParticipant = $room->getParticipantBySession($guestSession); $room->setParticipantType($guestParticipant, Participant::GUEST_MODERATOR); $this->assertMessageWasSent($room, [ 'type' => 'participants', 'participants' => [ 'changed' => [ [ 'permissions' => ['publish-media', 'publish-screen'], 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST_MODERATOR, ], ], 'users' => [ [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::MODERATOR, 'userId' => $this->userId, ], [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST_MODERATOR, ], ], ], ]); $this->controller->clearRequests(); $notJoinedUserId = 'not-joined-user-id'; $room->addUsers([ 'userId' => $notJoinedUserId, ]); $notJoinedParticipant = $room->getParticipant($notJoinedUserId); $room->setParticipantType($notJoinedParticipant, Participant::MODERATOR); $this->assertMessageWasSent($room, [ 'type' => 'participants', 'participants' => [ 'changed' => [ ], 'users' => [ [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::MODERATOR, 'userId' => $this->userId, ], [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => 0, 'participantType' => Participant::MODERATOR, 'userId' => $notJoinedUserId, ], [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST_MODERATOR, ], ], ], ]); $this->controller->clearRequests(); $room->setParticipantType($participant, Participant::USER); $this->assertMessageWasSent($room, [ 'type' => 'participants', 'participants' => [ 'changed' => [ [ 'permissions' => ['publish-media', 'publish-screen'], 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::USER, 'userId' => $this->userId, ], ], 'users' => [ [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::USER, 'userId' => $this->userId, ], [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => 0, 'participantType' => Participant::MODERATOR, 'userId' => $notJoinedUserId, ], [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST_MODERATOR, ], ], ], ]); $this->controller->clearRequests(); $room->setParticipantType($guestParticipant, Participant::GUEST); $this->assertMessageWasSent($room, [ 'type' => 'participants', 'participants' => [ 'changed' => [ [ 'permissions' => ['publish-media', 'publish-screen'], 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST, ], ], 'users' => [ [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $userSession, 'participantType' => Participant::USER, 'userId' => $this->userId, ], [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => 0, 'participantType' => Participant::MODERATOR, 'userId' => $notJoinedUserId, ], [ 'inCall' => 0, 'lastPing' => 0, 'sessionId' => $guestSession, 'participantType' => Participant::GUEST, ], ], ], ]); } }