UPSTREAM BASELINE: nextcloud/spreed v22.0.12 (без изменений)
Type checking / changes (push) Has been cancelled
Type checking / test (push) Has been cancelled
Type checking / typescript-summary (push) Has been cancelled
Node tests / changes (push) Has been cancelled
Node tests / test (push) Has been cancelled
Node tests / test-summary (push) Has been cancelled

Источник: https://github.com/nextcloud/spreed/archive/refs/tags/v22.0.12.tar.gz
С этого коммита ветка официального Nextcloud Talk отрезана (решение владельца 2026-07-06).
Все дальнейшие изменения — только наши; версии релизов: 22.0.12-f7.N.
This commit is contained in:
2026-07-06 14:07:50 +00:00
commit 01acfa3b40
1716 changed files with 613013 additions and 0 deletions
@@ -0,0 +1,312 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Tests\php\Chat\AutoComplete;
use OC\Collaboration\Collaborators\SearchResult;
use OCA\Talk\Chat\AutoComplete\SearchPlugin;
use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Files\Util;
use OCA\Talk\GuestManager;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\Session;
use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\TalkSession;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\IL10N;
use OCP\IUser;
use OCP\IUserManager;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class SearchPluginTest extends TestCase {
protected IUserManager&MockObject $userManager;
protected GuestManager&MockObject $guestManager;
protected TalkSession&MockObject $talkSession;
protected ParticipantService&MockObject $participantService;
protected Util&MockObject $util;
protected Authenticator&MockObject $federationAuthenticator;
protected IL10N&MockObject $l;
protected ?string $userId = null;
protected SearchPlugin $plugin;
public function setUp(): void {
parent::setUp();
$this->userManager = $this->createMock(IUserManager::class);
$this->guestManager = $this->createMock(GuestManager::class);
$this->talkSession = $this->createMock(TalkSession::class);
$this->participantService = $this->createMock(ParticipantService::class);
$this->util = $this->createMock(Util::class);
$this->federationAuthenticator = $this->createMock(Authenticator::class);
$this->userId = 'current';
$this->l = $this->createMock(IL10N::class);
$this->l->expects($this->any())
->method('t')
->willReturnCallback(function ($text, $parameters = []) {
return vsprintf($text, $parameters);
});
}
/**
* @param string[] $methods
* @return SearchPlugin|MockObject
*/
protected function getPlugin(array $methods = []) {
if (empty($methods)) {
return new SearchPlugin(
$this->userManager,
$this->guestManager,
$this->talkSession,
$this->participantService,
$this->util,
$this->userId,
$this->l,
$this->federationAuthenticator,
);
}
return $this->getMockBuilder(SearchPlugin::class)
->setConstructorArgs([
$this->userManager,
$this->guestManager,
$this->talkSession,
$this->participantService,
$this->util,
$this->userId,
$this->l,
$this->federationAuthenticator,
])
->onlyMethods($methods)
->getMock();
}
protected function createParticipantMock(string $uid, string $displayName, string $session = ''): Participant {
/** @var Participant&MockObject $p */
$p = $this->createMock(Participant::class);
$a = Attendee::fromRow([
'actor_type' => $uid ? 'users' : 'guests',
'actor_id' => $uid ?: sha1($session),
'display_name' => $displayName,
]);
$s = Session::fromRow([
'session_id' => $session,
]);
$p->expects($this->any())
->method('getAttendee')
->willReturn($a);
$p->expects($this->any())
->method('getSession')
->willReturn($s);
$p->expects($this->any())
->method('isGuest')
->willReturn($uid === '');
return $p;
}
public function testSearch(): void {
$result = $this->createMock(ISearchResult::class);
$room = $this->createMock(Room::class);
$this->participantService->expects($this->once())
->method('getParticipantsForRoom')
->with($room)
->willReturn([
$this->createParticipantMock('123', 'OneTwoThree'),
$this->createParticipantMock('foo', 'Foo Bar'),
$this->createParticipantMock('', 'Guest 1-6', '123456'),
$this->createParticipantMock('bar', 'Bar Tender'),
$this->createParticipantMock('', 'Guest a-f', 'abcdef'),
]);
$plugin = $this->getPlugin(['searchUsers', 'searchGuests']);
$plugin->setContext(['room' => $room]);
$plugin->expects($this->once())
->method('searchUsers')
->with('fo', ['123' => 'OneTwoThree', 'foo' => 'Foo Bar', 'bar' => 'Bar Tender'], $result)
->willReturnCallback(function ($search, $users, $result): void {
array_map(function ($user): void {
$this->assertIsString($user);
}, $users);
});
$plugin->expects($this->once())
->method('searchGuests')
->with('fo', $this->anything(), $result)
->willReturnCallback(function ($search, $guests, $result): void {
array_map(function ($guest): void {
$this->assertInstanceOf(Attendee::class, $guest);
}, $guests);
});
$plugin->search('fo', 10, 0, $result);
}
public static function dataSearchUsers(): array {
return [
['test', [], [], [], []],
['test', [
'current' => 'test',
'foo' => '',
'test' => 'Te st',
'test1' => 'Te st 1',
], [['test1' => 'Te st 1']], [['current' => 'test'], ['test' => 'Te st']]],
['test', [
'foo' => 'Test',
'bar' => 'test One',
], [['bar' => 'test One']], [['foo' => 'Test']]],
['', ['foo' => '', 'bar' => ''], [['foo' => ''], ['bar' => '']], []],
];
}
/**
* @param array<string, string> $users
*/
#[DataProvider('dataSearchUsers')]
public function testSearchUsers(string $search, array $users, array $expected, array $expectedExact): void {
$result = $this->createMock(ISearchResult::class);
$result->expects($this->once())
->method('addResultSet')
->with($this->anything(), $expected, $expectedExact);
$plugin = $this->getPlugin(['createResult']);
$plugin->method('createResult')
->willReturnCallback(function ($type, $uid, $name) {
return [$uid => $name];
});
self::invokePrivate($plugin, 'searchUsers', [$search, $users, $result]);
}
public static function dataSearchGuests(): array {
return [
['test', [], [], []],
['', ['abcdef' => ''], [['abcdef' => 'Guest']], []],
['Guest', ['abcdef' => ''], [], [['abcdef' => 'Guest']]],
['est', ['abcdef' => '', 'foobar' => 'est'], [['abcdef' => 'Guest']], [['foobar' => 'est']]],
['Ast', ['abcdef' => '', 'foobar' => 'ast'], [], [['foobar' => 'ast']]],
];
}
#[DataProvider('dataSearchGuests')]
public function testSearchGuests(string $search, array $guests, array $expected, array $expectedExact): void {
$result = $this->createMock(ISearchResult::class);
$result->expects($this->once())
->method('addResultSet')
->with($this->anything(), $expected, $expectedExact);
$attendees = [];
foreach ($guests as $actorId => $displayName) {
$attendees[] = Attendee::fromRow([
'actorId' => $actorId,
'displayName' => $displayName,
]);
}
$plugin = $this->getPlugin(['createGuestResult']);
$plugin->expects($this->any())
->method('createGuestResult')
->willReturnCallback(function ($hash, $name) {
return [$hash => $name];
});
self::invokePrivate($plugin, 'searchGuests', [$search, $attendees, $result]);
}
protected function createUserMock(array $userData) {
$user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn($userData['uid']);
$user->expects($this->any())
->method('getDisplayName')
->willReturn($userData['name']);
return $user;
}
public static function dataCreateResult(): array {
return [
['user', 'foo', 'bar', '', ['label' => 'bar', 'value' => ['shareType' => 'user', 'shareWith' => 'foo']]],
['user', 'test', 'Test', '', ['label' => 'Test', 'value' => ['shareType' => 'user', 'shareWith' => 'test']]],
['user', 'test', '', 'Test', ['label' => 'Test', 'value' => ['shareType' => 'user', 'shareWith' => 'test']]],
['user', 'test', '', null, ['label' => 'test', 'value' => ['shareType' => 'user', 'shareWith' => 'test']]],
];
}
#[DataProvider('dataCreateResult')]
public function testCreateResult(string $type, string $uid, string $name, ?string $managerName, array $expected): void {
if ($managerName !== null) {
$this->userManager->expects($this->any())
->method('getDisplayName')
->with($uid)
->willReturn($managerName);
} else {
$this->userManager->expects($this->any())
->method('getDisplayName')
->with($uid)
->willReturn(null);
}
$plugin = $this->getPlugin();
$this->assertEquals($expected, self::invokePrivate($plugin, 'createResult', [$type, $uid, $name]));
}
public static function dataCreateGuestResult(): array {
return [
['1234', 'foo', ['label' => 'foo', 'value' => ['shareType' => 'guest', 'shareWith' => 'guest/1234']]],
['abcd', 'bar', ['label' => 'bar', 'value' => ['shareType' => 'guest', 'shareWith' => 'guest/abcd']]],
];
}
#[DataProvider('dataCreateGuestResult')]
public function testCreateGuestResult(string $actorId, string $name, array $expected): void {
$plugin = $this->getPlugin();
$this->assertEquals($expected, self::invokePrivate($plugin, 'createGuestResult', [$actorId, $name]));
}
public static function dataSearchGroups(): array {
return [
// $search, $groups, $isGroup, $totalMatches, $totalExactMatches
['', ['groupid' => 'group'], true, 1, 0],
['groupid', ['groupid' => 'group'], true, 0, 1],
['gro', ['groupid' => 'group'], true, 1, 0],
['not', ['groupid' => 'group'], false, 0, 0],
['name', ['groupid' => 'name'], true, 0, 1],
['na', ['groupid' => 'name'], true, 1, 0],
['not', ['groupid' => 'group'], true, 0, 0],
];
}
#[DataProvider('dataSearchGroups')]
public function testSearchGroups(string $search, array $groups, bool $isGroup, int $totalMatches, int $totalExactMatches): void {
$plugin = $this->getPlugin(['createGroupResult']);
$plugin->expects($this->any())
->method('createGroupResult')
->willReturnCallback(function ($groupId) {
return [
'label' => $groupId,
'value' => [
'shareType' => 'group',
'shareWith' => 'group/' . $groupId,
],
];
});
$searchResult = new SearchResult();
self::invokePrivate($plugin, 'searchGroups', [$search, $groups, $searchResult]);
$actual = $searchResult->asArray();
$this->assertCount($totalMatches, $actual['groups']);
$this->assertCount($totalExactMatches, $actual['exact']['groups']);
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Tests\php\Chat\AutoComplete;
use OCA\Talk\Chat\AutoComplete\Sorter;
use OCA\Talk\Chat\CommentsManager;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class SorterTest extends TestCase {
protected CommentsManager&MockObject $commentsManager;
protected string $userId;
protected ?Sorter $sorter = null;
protected static array $user1 = [
'label' => 'Seattle',
'value' => [
'shareType' => 'user',
'shareWith' => 'seattle',
],
];
protected static array $user2 = [
'label' => 'New York',
'value' => [
'shareType' => 'user',
'shareWith' => 'new_york',
],
];
protected static array $user3 = [
'label' => 'ttle Sea',
'value' => [
'shareType' => 'user',
'shareWith' => 'ttle_sea',
],
];
public function setUp(): void {
parent::setUp();
$this->commentsManager = $this->createMock(CommentsManager::class);
$this->sorter = new Sorter($this->commentsManager);
}
public function testGetId(): void {
$this->assertSame('talk_chat_participants', $this->sorter->getId());
}
public static function dataSort(): array {
return [
'no user posted' => ['', ['users' => [self::$user1, self::$user2]], [], ['users' => [self::$user1, self::$user2]]],
'second user posted' => ['', ['users' => [self::$user1, self::$user2]], ['new_york' => new \DateTime('2000-01-01')], ['users' => [self::$user2, self::$user1]]],
'second user posted later' => ['', ['users' => [self::$user1, self::$user2]], ['seattle' => new \DateTime('2017-01-01'), 'new_york' => new \DateTime('2018-01-01')], ['users' => [self::$user2, self::$user1]]],
'second user posted earlier' => ['', ['users' => [self::$user1, self::$user2]], ['seattle' => new \DateTime('2018-01-01'), 'new_york' => new \DateTime('2017-01-01')], ['users' => [self::$user1, self::$user2]]],
'starting match first1' => ['Sea', ['users' => [self::$user1, self::$user3]], [], ['users' => [self::$user1, self::$user3]]],
'starting match first2' => ['Sea', ['users' => [self::$user3, self::$user1]], [], ['users' => [self::$user1, self::$user3]]],
'no users' => ['', ['groups' => [self::$user1, self::$user2]], [], ['groups' => [self::$user1, self::$user2]]],
];
}
#[DataProvider('dataSort')]
public function testSort(string $search, array $toSort, array $comments, array $expected): void {
$this->commentsManager->expects(isset($toSort['users']) ? $this->once() : $this->never())
->method('getLastCommentDateByActor')
->with('chat', '23', 'comment', 'users', $this->anything())
->willReturn($comments);
$this->sorter->sort($toSort, [
'itemType' => 'chat',
'itemId' => '23',
'search' => $search,
]);
$this->assertSame($expected, $toSort);
}
}
+854
View File
@@ -0,0 +1,854 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Tests\php\Chat;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\CommentsManager;
use OCA\Talk\Chat\Notifier;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\AttendeeMapper;
use OCA\Talk\Model\Invitation;
use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\AttachmentService;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\PollService;
use OCA\Talk\Service\RoomService;
use OCA\Talk\Service\ThreadService;
use OCA\Talk\Share\RoomShareProvider;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Collaboration\Reference\IReferenceManager;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\ICacheFactory;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUser;
use OCP\Notification\IManager as INotificationManager;
use OCP\Security\RateLimiting\ILimiter;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;
#[Group('DB')]
class ChatManagerTest extends TestCase {
protected CommentsManager|ICommentsManager|MockObject $commentsManager;
protected IEventDispatcher&MockObject $dispatcher;
protected INotificationManager&MockObject $notificationManager;
protected IManager&MockObject $shareManager;
protected RoomShareProvider&MockObject $shareProvider;
protected ParticipantService&MockObject $participantService;
protected RoomService&MockObject $roomService;
protected PollService&MockObject $pollService;
protected ThreadService&MockObject $threadService;
protected Notifier&MockObject $notifier;
protected ITimeFactory&MockObject $timeFactory;
protected AttachmentService&MockObject $attachmentService;
protected IReferenceManager&MockObject $referenceManager;
protected ILimiter&MockObject $rateLimiter;
protected IRequest&MockObject $request;
protected LoggerInterface&MockObject $logger;
protected IL10N&MockObject $l;
protected ?ChatManager $chatManager = null;
public function setUp(): void {
parent::setUp();
$this->commentsManager = $this->createMock(CommentsManager::class);
$this->dispatcher = $this->createMock(IEventDispatcher::class);
$this->notificationManager = $this->createMock(INotificationManager::class);
$this->shareManager = $this->createMock(IManager::class);
$this->shareProvider = $this->createMock(RoomShareProvider::class);
$this->participantService = $this->createMock(ParticipantService::class);
$this->roomService = $this->createMock(RoomService::class);
$this->pollService = $this->createMock(PollService::class);
$this->threadService = $this->createMock(ThreadService::class);
$this->notifier = $this->createMock(Notifier::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->referenceManager = $this->createMock(IReferenceManager::class);
$this->rateLimiter = $this->createMock(ILimiter::class);
$this->request = $this->createMock(IRequest::class);
$this->l = $this->createMock(IL10N::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->l->method('n')
->willReturnCallback(function (string $singular, string $plural, int $count, array $parameters = []) {
$text = $count === 1 ? $singular : $plural;
return vsprintf(str_replace('%n', (string)$count, $text), $parameters);
});
$this->chatManager = $this->getManager();
}
/**
* @param string[] $methods
* @return ChatManager|MockObject
*/
protected function getManager(array $methods = []): ChatManager {
$cacheFactory = $this->createMock(ICacheFactory::class);
if (!empty($methods)) {
return $this->getMockBuilder(ChatManager::class)
->setConstructorArgs([
$this->commentsManager,
$this->dispatcher,
\OCP\Server::get(IDBConnection::class),
$this->notificationManager,
$this->shareManager,
$this->shareProvider,
$this->participantService,
$this->roomService,
$this->pollService,
$this->threadService,
$this->notifier,
$cacheFactory,
$this->timeFactory,
$this->attachmentService,
$this->referenceManager,
$this->rateLimiter,
$this->request,
$this->l,
$this->logger,
])
->onlyMethods($methods)
->getMock();
}
return new ChatManager(
$this->commentsManager,
$this->dispatcher,
\OCP\Server::get(IDBConnection::class),
$this->notificationManager,
$this->shareManager,
$this->shareProvider,
$this->participantService,
$this->roomService,
$this->pollService,
$this->threadService,
$this->notifier,
$cacheFactory,
$this->timeFactory,
$this->attachmentService,
$this->referenceManager,
$this->rateLimiter,
$this->request,
$this->l,
$this->logger,
);
}
private function newComment($id, string $actorType, string $actorId, \DateTime $creationDateTime, string $message): IComment {
$comment = $this->createMock(IComment::class);
$id = (string)$id;
$comment->method('getId')->willReturn($id);
$comment->method('getActorType')->willReturn($actorType);
$comment->method('getActorId')->willReturn($actorId);
$comment->method('getCreationDateTime')->willReturn($creationDateTime);
$comment->method('getMessage')->willReturn($message);
return $comment;
}
/**
* @param array $data
* @return IComment|MockObject
*/
private function newCommentFromArray(array $data): IComment {
$comment = $this->createMock(IComment::class);
foreach ($data as $key => $value) {
if ($key === 'id') {
$value = (string)$value;
}
$comment->method('get' . ucfirst($key))->willReturn($value);
}
return $comment;
}
protected function assertCommentEquals(array $data, IComment $comment): void {
if (isset($data['id'])) {
$id = $data['id'];
unset($data['id']);
$this->assertEquals($id, $comment->getId());
}
$this->assertEquals($data, [
'actorType' => $comment->getActorType(),
'actorId' => $comment->getActorId(),
'creationDateTime' => $comment->getCreationDateTime(),
'message' => $comment->getMessage(),
'referenceId' => $comment->getReferenceId(),
'parentId' => $comment->getParentId(),
]);
}
public static function dataSendMessage(): array {
return [
'simple message' => ['testUser1', 'testMessage1', '', '0'],
'reference id' => ['testUser2', 'testMessage2', 'referenceId2', '0'],
'as a reply' => ['testUser3', 'testMessage3', '', '23'],
'reply w/ ref' => ['testUser4', 'testMessage4', 'referenceId4', '23'],
];
}
#[DataProvider('dataSendMessage')]
public function testSendMessage(string $userId, string $message, string $referenceId, string $parentId): void {
$creationDateTime = new \DateTime();
$commentExpected = [
'actorType' => 'users',
'actorId' => $userId,
'creationDateTime' => $creationDateTime,
'message' => $message,
'referenceId' => $referenceId,
'parentId' => $parentId,
];
$comment = $this->newCommentFromArray($commentExpected);
if ($parentId !== '0') {
$replyTo = $this->newCommentFromArray([
'id' => $parentId,
]);
$comment->expects($this->once())
->method('setParentId')
->with($parentId);
} else {
$replyTo = null;
}
$this->commentsManager->expects($this->once())
->method('create')
->with('users', $userId, 'chat', 1234)
->willReturn($comment);
$comment->expects($this->once())
->method('setMessage')
->with($message);
$comment->expects($this->once())
->method('setCreationDateTime')
->with($creationDateTime);
$comment->expects($referenceId === '' ? $this->never() : $this->once())
->method('setReferenceId')
->with($referenceId);
$comment->expects($this->once())
->method('setVerb')
->with('comment');
$this->commentsManager->expects($this->once())
->method('save')
->with($comment);
$chat = $this->createMock(Room::class);
$chat->expects($this->any())
->method('getId')
->willReturn(1234);
$this->notifier->expects($this->once())
->method('notifyMentionedUsers')
->with($chat, $comment);
$participant = $this->createMock(Participant::class);
$return = $this->chatManager->sendMessage($chat, $participant, 'users', $userId, $message, $creationDateTime, $replyTo, $referenceId, false);
$this->assertCommentEquals($commentExpected, $return);
}
public function testGetHistory(): void {
$offset = 1;
$limit = 42;
$expected = [
$this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000042), 'testMessage3'),
$this->newComment(109, 'guests', 'testSpreedSession', new \DateTime('@' . 1000000023), 'testMessage2'),
$this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage1')
];
$chat = $this->createMock(Room::class);
$chat->expects($this->any())
->method('getId')
->willReturn(1234);
$this->commentsManager->expects($this->once())
->method('getCommentsWithVerbForObjectSinceComment')
->with('chat', 1234, [], $offset, 'desc', $limit)
->willReturn($expected);
$comments = $this->chatManager->getHistory($chat, $offset, $limit, false);
$this->assertEquals($expected, $comments);
}
public function testWaitForNewMessages(): void {
$offset = 1;
$limit = 42;
$timeout = 23;
$expected = [
$this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage1'),
$this->newComment(109, 'guests', 'testSpreedSession', new \DateTime('@' . 1000000023), 'testMessage2'),
$this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000042), 'testMessage3'),
];
$chat = $this->createMock(Room::class);
$chat->expects($this->any())
->method('getId')
->willReturn(1234);
$this->commentsManager->expects($this->once())
->method('getCommentsWithVerbForObjectSinceComment')
->with('chat', 1234, [], $offset, 'asc', $limit)
->willReturn($expected);
$this->notifier->expects($this->once())
->method('markMentionNotificationsRead')
->with($chat, 'userId');
/** @var IUser&MockObject $user */
$user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn('userId');
$comments = $this->chatManager->waitForNewMessages($chat, $offset, $limit, $timeout, $user, false, true);
$this->assertEquals($expected, $comments);
}
public function testWaitForNewMessagesWithWaiting(): void {
$offset = 1;
$limit = 42;
$timeout = 23;
$expected = [
$this->newComment(108, 'users', 'testUser', new \DateTime('@' . 1000000016), 'testMessage1'),
$this->newComment(109, 'guests', 'testSpreedSession', new \DateTime('@' . 1000000023), 'testMessage2'),
$this->newComment(110, 'users', 'testUnknownUser', new \DateTime('@' . 1000000042), 'testMessage3'),
];
$chat = $this->createMock(Room::class);
$chat->expects($this->any())
->method('getId')
->willReturn(1234);
$this->commentsManager->expects($this->exactly(2))
->method('getCommentsWithVerbForObjectSinceComment')
->with('chat', 1234, [], $offset, 'asc', $limit)
->willReturnOnConsecutiveCalls(
[],
$expected
);
$this->notifier->expects($this->once())
->method('markMentionNotificationsRead')
->with($chat, 'userId');
/** @var IUser&MockObject $user */
$user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn('userId');
$comments = $this->chatManager->waitForNewMessages($chat, $offset, $limit, $timeout, $user, false, true);
$this->assertEquals($expected, $comments);
}
public function testGetUnreadCount(): void {
/** @var Room&MockObject $chat */
$chat = $this->createMock(Room::class);
$chat->expects($this->atLeastOnce())
->method('getId')
->willReturn(23);
$this->commentsManager->expects($this->once())
->method('getNumberOfCommentsWithVerbsForObjectSinceComment')
->with('chat', 23, 42, ['comment', 'object_shared']);
$this->chatManager->getUnreadCount($chat, 42);
}
public function testDeleteMessages(): void {
$chat = $this->createMock(Room::class);
$chat->expects($this->any())
->method('getId')
->willReturn(1234);
$this->commentsManager->expects($this->once())
->method('deleteCommentsAtObject')
->with('chat', 1234);
$this->notifier->expects($this->once())
->method('removePendingNotificationsForRoom')
->with($chat);
$this->chatManager->deleteMessages($chat);
}
public function testDeleteMessage(): void {
$mapper = new AttendeeMapper(\OCP\Server::get(IDBConnection::class));
$attendee = $mapper->createAttendeeFromRow([
'a_id' => 1,
'room_id' => 123,
'actor_type' => Attendee::ACTOR_USERS,
'actor_id' => 'user',
'display_name' => 'user-display',
'pin' => '',
'participant_type' => Participant::USER,
'favorite' => true,
'notification_level' => Participant::NOTIFY_MENTION,
'notification_calls' => Participant::NOTIFY_CALLS_ON,
'last_joined_call' => 0,
'last_read_message' => 0,
'last_mention_message' => 0,
'last_mention_direct' => 0,
'read_privacy' => Participant::PRIVACY_PUBLIC,
'permissions' => Attendee::PERMISSIONS_DEFAULT,
'access_token' => '',
'remote_id' => '',
'phone_number' => '',
'call_id' => '',
'invited_cloud_id' => '',
'state' => Invitation::STATE_ACCEPTED,
'unread_messages' => 0,
'last_attendee_activity' => 0,
'archived' => 0,
'important' => 0,
'sensitive' => 0,
'has_unread_threads' => false,
'has_unread_thread_mentions' => false,
'has_unread_thread_directs' => false,
]);
$chat = $this->createMock(Room::class);
$chat->expects($this->any())
->method('getId')
->willReturn(1234);
$participant = new Participant($chat, $attendee, null);
$date = new \DateTime();
$comment = $this->createMock(IComment::class);
$comment->method('getId')
->willReturn('123456');
$comment->method('getVerb')
->willReturn('comment');
$comment->expects($this->once())
->method('setMessage');
$comment->expects($this->once())
->method('setVerb')
->with('comment_deleted');
$this->commentsManager->expects($this->once())
->method('save')
->with($comment);
$systemMessage = $this->createMock(IComment::class);
$chatManager = $this->getManager(['addSystemMessage']);
$chatManager->expects($this->once())
->method('addSystemMessage')
->with($chat, $participant, Attendee::ACTOR_USERS, 'user', $this->anything(), $this->anything(), false, null, $comment)
->willReturn($systemMessage);
$this->assertSame($systemMessage, $chatManager->deleteMessage($chat, $comment, $participant, $date));
}
public function testDeleteMessageFileShare(): void {
$mapper = new AttendeeMapper(\OCP\Server::get(IDBConnection::class));
$attendee = $mapper->createAttendeeFromRow([
'a_id' => 1,
'room_id' => 123,
'actor_type' => Attendee::ACTOR_USERS,
'actor_id' => 'user',
'display_name' => 'user-display',
'pin' => '',
'participant_type' => Participant::USER,
'favorite' => true,
'notification_level' => Participant::NOTIFY_MENTION,
'notification_calls' => Participant::NOTIFY_CALLS_ON,
'last_joined_call' => 0,
'last_read_message' => 0,
'last_mention_message' => 0,
'last_mention_direct' => 0,
'read_privacy' => Participant::PRIVACY_PUBLIC,
'permissions' => Attendee::PERMISSIONS_DEFAULT,
'access_token' => '',
'remote_id' => '',
'phone_number' => '',
'call_id' => '',
'invited_cloud_id' => '',
'state' => Invitation::STATE_ACCEPTED,
'unread_messages' => 0,
'last_attendee_activity' => 0,
'archived' => 0,
'important' => 0,
'sensitive' => 0,
'has_unread_threads' => false,
'has_unread_thread_mentions' => false,
'has_unread_thread_directs' => false,
]);
$chat = $this->createMock(Room::class);
$chat->expects($this->any())
->method('getId')
->willReturn(1234);
$chat->expects($this->any())
->method('getToken')
->willReturn('T0k3N');
$participant = new Participant($chat, $attendee, null);
$date = new \DateTime();
$comment = $this->createMock(IComment::class);
$comment->method('getId')
->willReturn('123456');
$comment->method('getVerb')
->willReturn('object_shared');
$comment->expects($this->once())
->method('getMessage')
->willReturn(json_encode(['message' => 'file_shared', 'parameters' => ['share' => '42']]));
$comment->expects($this->once())
->method('setMessage');
$comment->expects($this->once())
->method('setVerb')
->with('comment_deleted');
$share = $this->createMock(IShare::class);
$share->method('getShareType')
->willReturn(IShare::TYPE_ROOM);
$share->method('getSharedWith')
->willReturn('T0k3N');
$share->method('getShareOwner')
->willReturn('user');
$this->shareManager->method('getShareById')
->with('ocRoomShare:42')
->willReturn($share);
$this->shareManager->expects($this->once())
->method('deleteShare')
->willReturn($share);
$this->commentsManager->expects($this->once())
->method('save')
->with($comment);
$systemMessage = $this->createMock(IComment::class);
$chatManager = $this->getManager(['addSystemMessage']);
$chatManager->expects($this->once())
->method('addSystemMessage')
->with($chat, $participant, Attendee::ACTOR_USERS, 'user', $this->anything(), $this->anything(), false, null, $comment)
->willReturn($systemMessage);
$this->assertSame($systemMessage, $chatManager->deleteMessage($chat, $comment, $participant, $date));
}
public function testDeleteMessageFileShareNotFound(): void {
$mapper = new AttendeeMapper(\OCP\Server::get(IDBConnection::class));
$attendee = $mapper->createAttendeeFromRow([
'a_id' => 1,
'room_id' => 123,
'actor_type' => Attendee::ACTOR_USERS,
'actor_id' => 'user',
'display_name' => 'user-display',
'pin' => '',
'participant_type' => Participant::USER,
'favorite' => true,
'notification_level' => Participant::NOTIFY_MENTION,
'notification_calls' => Participant::NOTIFY_CALLS_ON,
'last_joined_call' => 0,
'last_read_message' => 0,
'last_mention_message' => 0,
'last_mention_direct' => 0,
'read_privacy' => Participant::PRIVACY_PUBLIC,
'permissions' => Attendee::PERMISSIONS_DEFAULT,
'access_token' => '',
'remote_id' => '',
'phone_number' => '',
'call_id' => '',
'invited_cloud_id' => '',
'state' => Invitation::STATE_ACCEPTED,
'unread_messages' => 0,
'last_attendee_activity' => 0,
'archived' => 0,
'important' => 0,
'sensitive' => 0,
'has_unread_threads' => false,
'has_unread_thread_mentions' => false,
'has_unread_thread_directs' => false,
]);
$chat = $this->createMock(Room::class);
$chat->expects($this->any())
->method('getId')
->willReturn(1234);
$participant = new Participant($chat, $attendee, null);
$date = new \DateTime();
$comment = $this->createMock(IComment::class);
$comment->method('getId')
->willReturn('123456');
$comment->method('getVerb')
->willReturn('object_shared');
$comment->expects($this->once())
->method('getMessage')
->willReturn(json_encode(['message' => 'file_shared', 'parameters' => ['share' => '42']]));
$this->shareManager->method('getShareById')
->with('ocRoomShare:42')
->willThrowException(new ShareNotFound());
$this->commentsManager->expects($this->never())
->method('save');
$systemMessage = $this->createMock(IComment::class);
$chatManager = $this->getManager(['addSystemMessage']);
$chatManager->expects($this->never())
->method('addSystemMessage');
$this->expectException(ShareNotFound::class);
$this->assertSame($systemMessage, $chatManager->deleteMessage($chat, $comment, $participant, $date));
}
public function testClearHistory(): void {
$chat = $this->createMock(Room::class);
$chat->expects($this->any())
->method('getId')
->willReturn(1234);
$chat->expects($this->any())
->method('getToken')
->willReturn('t0k3n');
$this->commentsManager->expects($this->once())
->method('deleteCommentsAtObject')
->with('chat', 1234);
$this->shareProvider->expects($this->once())
->method('deleteInRoom')
->with('t0k3n');
$this->notifier->expects($this->once())
->method('removePendingNotificationsForRoom')
->with($chat, true);
$this->participantService->expects($this->once())
->method('resetChatDetails')
->with($chat);
$date = new \DateTime();
$this->timeFactory->method('getDateTime')
->willReturn($date);
$manager = $this->getManager(['addSystemMessage']);
$manager->expects($this->once())
->method('addSystemMessage')
->with(
$chat,
null,
'users',
'admin',
json_encode(['message' => 'history_cleared', 'parameters' => []]),
$date,
false
);
$manager->clearHistory($chat, 'users', 'admin');
}
public static function dataSearchIsPartOfConversationNameOrAtAll(): array {
return [
'found a in all' => [
'a', 'room', true
],
'found h in here' => [
'h', 'room', true
],
'case sensitive, not found A in all' => [
'A', 'room', false
],
'case sensitive, not found H in here' => [
'H', 'room', false
],
'non case sensitive, found r in room' => [
'R', 'room', true
],
'found r in begin of room' => [
'r', 'room', true
],
'found o in middle of room' => [
'o', 'room', true
],
'not found all in middle of text' => [
'notbeginingall', 'room', false
],
'not found here in middle of text' => [
'notbegininghere', 'room', false
],
'not found room in middle of text' => [
'notbeginingroom', 'room', false
],
];
}
#[DataProvider('dataSearchIsPartOfConversationNameOrAtAll')]
public function testSearchIsPartOfConversationNameOrAtAll(string $search, string $roomDisplayName, bool $expected): void {
$actual = self::invokePrivate($this->chatManager, 'searchIsPartOfConversationNameOrAtAll', [$search, $roomDisplayName]);
$this->assertEquals($expected, $actual);
}
public static function dataAddConversationNotify(): array {
return [
[
'',
['getType' => Room::TYPE_ONE_TO_ONE],
[],
null,
[],
],
[
'',
['getDisplayName' => 'test', 'getMentionPermissions' => 0],
['getAttendee' => Attendee::fromRow([
'actor_type' => Attendee::ACTOR_USERS,
'actor_id' => 'user',
])],
324,
[['id' => 'all', 'label' => 'test', 'source' => 'calls', 'mentionId' => 'all', 'details' => 'All 324 participants']]
],
[
'',
['getMentionPermissions' => 1],
['hasModeratorPermissions' => false],
null,
[],
],
[
'all',
['getDisplayName' => 'test', 'getMentionPermissions' => 0],
['getAttendee' => Attendee::fromRow([
'actor_type' => Attendee::ACTOR_USERS,
'actor_id' => 'user',
])],
1,
[['id' => 'all', 'label' => 'test', 'source' => 'calls', 'mentionId' => 'all']],
],
[
'all',
['getDisplayName' => 'test', 'getMentionPermissions' => 1],
[
'getAttendee' => Attendee::fromRow([
'actor_type' => Attendee::ACTOR_USERS,
'actor_id' => 'user',
]),
'hasModeratorPermissions' => true,
],
8,
[['id' => 'all', 'label' => 'test', 'source' => 'calls', 'mentionId' => 'all', 'details' => 'All 8 participants']],
],
[
'here',
['getDisplayName' => 'test', 'getMentionPermissions' => 0],
['getAttendee' => Attendee::fromRow([
'actor_type' => Attendee::ACTOR_GUESTS,
'actor_id' => 'guest',
])],
12,
[['id' => 'all', 'label' => 'test', 'source' => 'calls', 'mentionId' => 'all', 'details' => 'All 12 participants']],
],
];
}
#[DataProvider('dataAddConversationNotify')]
public function testAddConversationNotify(string $search, array $roomMocks, array $participantMocks, ?int $totalCount, array $expected): void {
$room = $this->createMock(Room::class);
foreach ($roomMocks as $method => $return) {
$room->expects($this->once())
->method($method)
->willReturn($return);
}
$participant = $this->createMock(Participant::class);
foreach ($participantMocks as $method => $return) {
$participant->expects($this->once())
->method($method)
->willReturn($return);
}
if ($totalCount !== null) {
$this->participantService->method('getNumberOfUsers')
->willReturn($totalCount);
}
$actual = $this->chatManager->addConversationNotify([], $search, $room, $participant);
$this->assertEquals($expected, $actual);
}
#[DataProvider('dataIsSharedFile')]
public function testIsSharedFile(string $message, bool $expected): void {
$actual = $this->chatManager->isSharedFile($message);
$this->assertEquals($expected, $actual);
}
public static function dataIsSharedFile(): array {
return [
['', false],
[json_encode([]), false],
[json_encode(['parameters' => '']), false],
[json_encode(['parameters' => []]), false],
[json_encode(['parameters' => ['share' => null]]), false],
[json_encode(['parameters' => ['share' => '']]), false],
[json_encode(['parameters' => ['share' => []]]), false],
[json_encode(['parameters' => ['share' => 0]]), false],
[json_encode(['parameters' => ['share' => 1]]), true],
];
}
#[DataProvider('dataFilterCommentsWithNonExistingFiles')]
public function testFilterCommentsWithNonExistingFiles(array $list, int $expectedCount): void {
// Transform text messages in instance of comment and mock with the message
foreach ($list as $key => $message) {
$list[$key] = $this->createMock(IComment::class);
$list[$key]->method('getMessage')
->willReturn($message);
$messageDecoded = json_decode($message, true);
if (isset($messageDecoded['parameters']['share']) && $messageDecoded['parameters']['share'] === 'notExists') {
$this->shareProvider->expects($this->once())
->method('getShareById')
->with('notExists')
->willThrowException(new ShareNotFound());
}
}
if (count($list) !== $expectedCount) {
}
$result = $this->chatManager->filterCommentsWithNonExistingFiles($list);
$this->assertCount($expectedCount, $result);
}
public static function dataFilterCommentsWithNonExistingFiles(): array {
return [
[[], 0],
[[json_encode(['parameters' => ['not a shared file']])], 1],
[[json_encode(['parameters' => ['share' => 'notExists']])], 0],
[[json_encode(['parameters' => ['share' => 1]])], 1],
];
}
}
+465
View File
@@ -0,0 +1,465 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Tests\php\Chat;
use OC\Comments\Comment;
use OCA\Talk\Chat\Notifier;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Files\Util;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\Session;
use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\ThreadService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IUserManager;
use OCP\Notification\IManager as INotificationManager;
use OCP\Notification\INotification;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class NotifierTest extends TestCase {
protected INotificationManager&MockObject $notificationManager;
protected IUserManager&MockObject $userManager;
protected IGroupManager&MockObject $groupManager;
protected ParticipantService&MockObject $participantService;
protected ThreadService&MockObject $threadService;
protected IConfig&MockObject $config;
protected ITimeFactory&MockObject $timeFactory;
protected Util&MockObject $util;
public function setUp(): void {
parent::setUp();
$this->notificationManager = $this->createMock(INotificationManager::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->userManager
->method('userExists')
->willReturnCallback(function ($userId) {
return $userId !== 'unknownUser';
});
$this->groupManager = $this->createMock(IGroupManager::class);
$this->participantService = $this->createMock(ParticipantService::class);
$this->threadService = $this->createMock(ThreadService::class);
$this->config = $this->createMock(IConfig::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->util = $this->createMock(Util::class);
}
/**
* @param string[] $methods
* @return Notifier|MockObject
*/
protected function getNotifier(array $methods = []) {
if (!empty($methods)) {
return $this->getMockBuilder(Notifier::class)
->setConstructorArgs([
$this->notificationManager,
$this->userManager,
$this->groupManager,
$this->participantService,
$this->threadService,
$this->config,
$this->timeFactory,
$this->util,
])
->onlyMethods($methods)
->getMock();
}
return new Notifier(
$this->notificationManager,
$this->userManager,
$this->groupManager,
$this->participantService,
$this->threadService,
$this->config,
$this->timeFactory,
$this->util
);
}
private function newComment($id, $actorType, $actorId, $creationDateTime, $message): IComment {
$comment = new Comment([
'id' => $id,
'object_id' => '1234',
'object_type' => 'chat',
'actor_type' => $actorType,
'actor_id' => $actorId,
'creation_date_time' => $creationDateTime,
'message' => $message,
'verb' => 'comment',
]);
return $comment;
}
/**
* @return Room|MockObject
*/
private function getRoom($settings = []) {
/** @var Room|MockObject */
$room = $this->createMock(Room::class);
$this->participantService->expects($this->any())
->method('getParticipant')
->willReturnCallback(function (Room $room, string $actorId) use ($settings): Participant {
if ($actorId === 'userNotInOneToOneChat') {
throw new ParticipantNotFoundException();
}
$attendeeRow = [
'actor_type' => 'user',
'actor_id' => $actorId,
];
if (isset($settings['attendee'][$actorId])) {
$attendeeRow = array_merge($attendeeRow, $settings['attendee'][$actorId]);
}
$attendee = Attendee::fromRow($attendeeRow);
return new Participant($room, $attendee, null);
});
return $room;
}
public static function dataNotifyMentionedUsers(): array {
return [
'no notifications' => [
'No mentions', [], [], [],
],
'notify a mentioned user' => [
'Mention @anotherUser', [], [['id' => 'anotherUser', 'type' => 'users', 'reason' => 'direct']], [['id' => 'anotherUser', 'type' => 'users', 'reason' => 'direct']],
],
'not notify mentioned user if already notified' => [
'Mention @anotherUser', [['id' => 'anotherUser', 'type' => 'users', 'reason' => 'reply']], [], [['id' => 'anotherUser', 'type' => 'users', 'reason' => 'reply']],
],
'notify mentioned Users With Long Message Start Mention' => [
'123456789 @anotherUserWithOddLengthName 123456789-123456789-123456789-123456789-123456789-123456789', [], [['id' => 'anotherUserWithOddLengthName', 'type' => 'users', 'reason' => 'direct']], [['id' => 'anotherUserWithOddLengthName', 'type' => 'users', 'reason' => 'direct']],
],
'notify mentioned users with long message middle mention' => [
'123456789-123456789-123456789-1234 @anotherUserWithOddLengthName 6789-123456789-123456789-123456789', [], [['id' => 'anotherUserWithOddLengthName', 'type' => 'users', 'reason' => 'direct']], [['id' => 'anotherUserWithOddLengthName', 'type' => 'users', 'reason' => 'direct']],
],
'notify mentioned users with long message end mention' => [
'123456789-123456789-123456789-123456789-123456789-123456789 @anotherUserWithOddLengthName 123456789', [], [['id' => 'anotherUserWithOddLengthName', 'type' => 'users', 'reason' => 'direct']], [['id' => 'anotherUserWithOddLengthName', 'type' => 'users', 'reason' => 'direct']],
],
'mention herself' => [
'Mention @testUser', [], [], [],
],
'not notify unknownuser' => [
'Mention @unknownUser', [], [], [],
],
'notify mentioned users several mentions' => [
'Mention @anotherUser, and @unknownUser, and @testUser, and @userAbleToJoin', [],
[['id' => 'anotherUser', 'type' => 'users', 'reason' => 'direct'], ['id' => 'userAbleToJoin', 'type' => 'users', 'reason' => 'direct']],
[['id' => 'anotherUser', 'type' => 'users', 'reason' => 'direct'], ['id' => 'userAbleToJoin', 'type' => 'users', 'reason' => 'direct']],
],
'notify mentioned users to user not invited to chat' => [
'Mention @userNotInOneToOneChat', [], [], [],
]
];
}
#[DataProvider('dataNotifyMentionedUsers')]
public function testNotifyMentionedUsers(string $message, array $alreadyNotifiedUsers, array $notify, array $expectedReturn): void {
if (count($notify)) {
$this->notificationManager->expects($this->exactly(count($notify)))
->method('notify');
}
$room = $this->getRoom();
$comment = $this->newComment('108', 'users', 'testUser', new \DateTime('@' . 1000000016), $message);
$notifier = $this->getNotifier([]);
$participant = $this->createMock(Participant::class);
$actual = $notifier->notifyMentionedUsers($room, $comment, $alreadyNotifiedUsers, false, $participant);
$this->assertEqualsCanonicalizing($expectedReturn, $actual);
}
public static function dataShouldParticipantBeNotified(): array {
return [
[Attendee::ACTOR_GROUPS, 'test1', null, Attendee::ACTOR_USERS, 'test1', [], false, Notifier::PRIORITY_NONE],
[Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test1', [], false, Notifier::PRIORITY_NONE],
[Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [], false, Notifier::PRIORITY_NORMAL],
[Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [['id' => 'test1', 'type' => Attendee::ACTOR_USERS]], false, Notifier::PRIORITY_NONE],
[Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [['id' => 'test1', 'type' => Attendee::ACTOR_FEDERATED_USERS]], false, Notifier::PRIORITY_NORMAL],
[Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT - 5, Attendee::ACTOR_USERS, 'test2', [], false, Notifier::PRIORITY_NONE],
[Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT + 5, Attendee::ACTOR_USERS, 'test2', [], false, Notifier::PRIORITY_NORMAL],
// Marked as important, still blocked by session and being the author, but otherwise with PRIORITY_IMPORTANT
[Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test1', [], true, Notifier::PRIORITY_NONE],
[Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [], true, Notifier::PRIORITY_IMPORTANT],
[Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT - 5, Attendee::ACTOR_USERS, 'test2', [], true, Notifier::PRIORITY_NONE],
[Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT + 5, Attendee::ACTOR_USERS, 'test2', [], true, Notifier::PRIORITY_IMPORTANT],
];
}
#[DataProvider('dataShouldParticipantBeNotified')]
public function testShouldParticipantBeNotified(string $actorType, string $actorId, ?int $sessionAge, string $commentActorType, string $commentActorId, array $alreadyNotifiedUsers, bool $isImportant, int $expected): void {
$comment = $this->createMock(IComment::class);
$comment->method('getActorType')
->willReturn($commentActorType);
$comment->method('getActorId')
->willReturn($commentActorId);
$room = $this->createMock(Room::class);
$attendee = Attendee::fromRow([
'actor_type' => $actorType,
'actor_id' => $actorId,
'important' => $isImportant,
]);
$session = null;
if ($sessionAge !== null) {
$current = 1234567;
$this->timeFactory->method('getTime')
->willReturn($current);
$session = Session::fromRow([
'last_ping' => $current - $sessionAge,
]);
}
$participant = new Participant($room, $attendee, $session);
self::assertSame($expected, self::invokePrivate($this->getNotifier(), 'shouldParticipantBeNotified', [$participant, $comment, $alreadyNotifiedUsers]));
}
public function testRemovePendingNotificationsForRoom(): void {
$notification = $this->createMock(INotification::class);
$room = $this->createMock(Room::class);
$room->expects($this->any())
->method('getToken')
->willReturn('Token123');
$this->notificationManager->expects($this->once())
->method('createNotification')
->willReturn($notification);
$notification->expects($this->once())
->method('setApp')
->with('spreed')
->willReturnSelf();
$notification->expects($this->atLeastOnce())
->method('setObject')
->with($this->anything(), 'Token123')
->willReturnSelf();
$this->notificationManager->expects($this->atLeastOnce())
->method('markProcessed')
->with($notification);
$this->getNotifier()->removePendingNotificationsForRoom($room);
}
public function testRemovePendingNotificationsForChatOnly(): void {
$notification = $this->createMock(INotification::class);
$room = $this->createMock(Room::class);
$room->expects($this->any())
->method('getToken')
->willReturn('Token123');
$this->notificationManager->expects($this->once())
->method('createNotification')
->willReturn($notification);
$notification->expects($this->once())
->method('setApp')
->with('spreed')
->willReturnSelf();
$notification->expects($this->atLeastOnce())
->method('setObject')
->with($this->anything(), 'Token123')
->willReturnSelf();
$this->notificationManager->expects($this->atLeastOnce())
->method('markProcessed')
->with($notification);
$this->getNotifier()->removePendingNotificationsForRoom($room, true);
}
public static function dataAddMentionAllToList(): array {
return [
'not notify' => [
[],
[],
0,
true,
[],
],
'preserve notify list and do not notify all' => [
[
['id' => 'user1', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
],
[],
0,
true,
[
['id' => 'user1', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
],
],
'mention all' => [
[
['id' => 'user1', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
['id' => 'all', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
],
[
Attendee::fromRow(['actor_id' => 'user1', 'actor_type' => Attendee::ACTOR_USERS]),
Attendee::fromRow(['actor_id' => 'user2', 'actor_type' => Attendee::ACTOR_USERS]),
],
0,
false,
[
['id' => 'user1', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
['id' => 'user2', 'type' => Attendee::ACTOR_USERS, 'reason' => 'all'],
],
],
'prevent non-moderator to notify all' => [
[
['id' => 'user1', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
['id' => 'all', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
],
[
Attendee::fromRow(['actor_id' => 'user1', 'actor_type' => Attendee::ACTOR_USERS]),
Attendee::fromRow(['actor_id' => 'user2', 'actor_type' => Attendee::ACTOR_USERS]),
],
1,
false,
[
['id' => 'user1', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
],
],
];
}
#[DataProvider('dataAddMentionAllToList')]
public function testAddMentionAllToList(array $usersToNotify, array $participants, int $mentionPermissions, bool $moderatorPermissions, array $return): void {
$room = $this->createMock(Room::class);
$room->method('getMentionPermissions')
->willReturn($mentionPermissions);
$this->participantService
->method('getActorsByType')
->willReturn($participants);
$participant = $this->createMock(Participant::class);
$participant->method('hasModeratorPermissions')
->willReturn($moderatorPermissions);
$actual = self::invokePrivate($this->getNotifier(), 'addMentionAllToList', [$room, $usersToNotify, $participant]);
$this->assertCount(count($return), $actual);
foreach ($actual as $key => $value) {
$this->assertIsArray($value);
if (array_key_exists('attendee', $value)) {
$this->assertInstanceOf(Attendee::class, $value['attendee']);
unset($value['attendee']);
}
$this->assertEqualsCanonicalizing($return[$key], $value);
}
}
public static function dataNotifyReacted(): array {
return [
'author react to own message'
=> [0, Participant::NOTIFY_MENTION, Room::TYPE_GROUP, 'testUser'],
'notify never'
=> [0, Participant::NOTIFY_NEVER, Room::TYPE_GROUP, 'testUser2'],
'notify default, not one to one'
=> [0, Participant::NOTIFY_DEFAULT, Room::TYPE_GROUP, 'testUser2'],
'notify default, one to one'
=> [1, Participant::NOTIFY_DEFAULT, Room::TYPE_ONE_TO_ONE, 'testUser2'],
'notify always'
=> [1, Participant::NOTIFY_ALWAYS, Room::TYPE_GROUP, 'testUser2'],
];
}
#[DataProvider('dataNotifyReacted')]
public function testNotifyReacted(int $notify, int $notifyType, int $roomType, string $authorId): void {
$this->notificationManager->expects($this->exactly($notify))
->method('notify');
$room = $this->getRoom([
'attendee' => [
'testUser' => [
'notificationLevel' => $notifyType,
]
]
]);
$room->method('getType')
->willReturn($roomType);
$comment = $this->newComment('108', 'users', 'testUser', new \DateTime('@' . 1000000016), 'message');
$reaction = $this->newComment('108', 'users', $authorId, new \DateTime('@' . 1000000016), 'message');
$notifier = $this->getNotifier([]);
$notifier->notifyReacted($room, $comment, $reaction);
}
public static function dataGetMentionedUsers(): array {
return [
'mention one user' => [
'Mention @anotherUser',
[
['id' => 'anotherUser', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
],
],
'mention two user' => [
'Mention @anotherUser, and @unknownUser',
[
['id' => 'anotherUser', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
['id' => 'unknownUser', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
],
],
'mention all' => [
'Mention @all',
[
['id' => 'all', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
],
],
'mention user, all, guest and group' => [
'mention @test, @all, @"guest/1" @"group/1"',
[
['id' => 'test', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
['id' => 'all', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'],
],
],
];
}
#[DataProvider('dataGetMentionedUsers')]
public function testGetMentionedUsers(string $message, array $expectedReturn): void {
$comment = $this->newComment('108', 'users', 'testUser', new \DateTime('@' . 1000000016), $message);
$actual = self::invokePrivate($this->getNotifier(), 'getMentionedUsers', [$comment]);
$this->assertEqualsCanonicalizing($expectedReturn, $actual);
}
public static function dataGetMentionedUserIds(): array {
$return = self::dataGetMentionedUsers();
array_walk($return, function (array &$scenario) {
array_walk($scenario[1], function (array &$params): void {
$params = $params['id'];
});
return $scenario;
});
return $return;
}
#[DataProvider('dataGetMentionedUserIds')]
public function testGetMentionedUserIds(string $message, array $expectedReturn): void {
$comment = $this->newComment('108', 'users', 'testUser', new \DateTime('@' . 1000000016), $message);
$actual = self::invokePrivate($this->getNotifier(), 'getMentionedUserIds', [$comment]);
$this->assertEqualsCanonicalizing($expectedReturn, $actual);
}
}
File diff suppressed because it is too large Load Diff
+618
View File
@@ -0,0 +1,618 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Tests\php\Chat\Parser;
use OCA\Talk\Chat\Parser\UserMention;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\GuestManager;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\Message;
use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\AvatarService;
use OCA\Talk\Service\ParticipantService;
use OCP\App\IAppManager;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\Federation\ICloudId;
use OCP\Federation\ICloudIdManager;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IUserManager;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class UserMentionTest extends TestCase {
protected IAppManager&MockObject $appManager;
protected ICommentsManager&MockObject $commentsManager;
protected IUserManager&MockObject $userManager;
protected IGroupManager&MockObject $groupManager;
protected GuestManager&MockObject $guestManager;
protected AvatarService&MockObject $avatarService;
protected ICloudIdManager&MockObject $cloudIdManager;
protected ParticipantService&MockObject $participantService;
protected IL10N&MockObject $l;
protected ?UserMention $parser = null;
public function setUp(): void {
parent::setUp();
$this->appManager = $this->createMock(IAppManager::class);
$this->commentsManager = $this->createMock(ICommentsManager::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->guestManager = $this->createMock(GuestManager::class);
$this->avatarService = $this->createMock(AvatarService::class);
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
$this->participantService = $this->createMock(ParticipantService::class);
$this->l = $this->createMock(IL10N::class);
$this->parser = new UserMention(
$this->appManager,
$this->commentsManager,
$this->userManager,
$this->groupManager,
$this->guestManager,
$this->avatarService,
$this->cloudIdManager,
$this->participantService,
$this->l,
);
}
/**
* @param array $mentions
* @param array|null $metadata
* @return MockObject|IComment
*/
private function newComment(array $mentions, ?array $metadata = null): IComment {
$comment = $this->createMock(IComment::class);
$comment->method('getMentions')->willReturn($mentions);
if ($metadata !== null) {
$comment->method('getMetaData')->willReturn($metadata);
}
return $comment;
}
public function testGetRichMessageWithoutEnrichableReferences(): void {
$comment = $this->newComment([]);
/** @var Room&MockObject $room */
$room = $this->createMock(Room::class);
/** @var Participant&MockObject $participant */
$participant = $this->createMock(Participant::class);
/** @var IL10N&MockObject $l */
$l = $this->createMock(IL10N::class);
$chatMessage = new Message($room, $participant, $comment, $l);
$chatMessage->setMessage('Message without enrichable references', []);
self::invokePrivate($this->parser, 'parseMessage', [$chatMessage]);
$this->assertEquals('Message without enrichable references', $chatMessage->getMessage());
$this->assertEquals([], $chatMessage->getMessageParameters());
}
public function testGetRichMessageWithSingleMention(): void {
$mentions = [
['type' => 'user', 'id' => 'testUser'],
];
$comment = $this->newComment($mentions);
$this->commentsManager->expects($this->once())
->method('resolveDisplayName')
->with('user', 'testUser')
->willReturn('testUser display name');
$this->userManager->expects($this->once())
->method('getDisplayName')
->with('testUser')
->willReturn('testUser display name');
/** @var Room&MockObject $room */
$room = $this->createMock(Room::class);
/** @var Participant&MockObject $participant */
$participant = $this->createMock(Participant::class);
/** @var IL10N&MockObject $l */
$l = $this->createMock(IL10N::class);
$chatMessage = new Message($room, $participant, $comment, $l);
$chatMessage->setMessage('Mention to @testUser', []);
self::invokePrivate($this->parser, 'parseMessage', [$chatMessage]);
$expectedMessageParameters = [
'mention-user1' => [
'type' => 'user',
'id' => 'testUser',
'name' => 'testUser display name',
'mention-id' => 'testUser',
]
];
$this->assertEquals('Mention to {mention-user1}', $chatMessage->getMessage());
$this->assertEquals($expectedMessageParameters, $chatMessage->getMessageParameters());
}
public function testGetRichMessageWithDuplicatedMention(): void {
$mentions = [
['type' => 'user', 'id' => 'testUser'],
];
$comment = $this->newComment($mentions);
$this->commentsManager->expects($this->once())
->method('resolveDisplayName')
->with('user', 'testUser')
->willReturn('testUser display name');
$this->userManager->expects($this->once())
->method('getDisplayName')
->with('testUser')
->willReturn('testUser display name');
/** @var Room&MockObject $room */
$room = $this->createMock(Room::class);
/** @var Participant&MockObject $participant */
$participant = $this->createMock(Participant::class);
/** @var IL10N&MockObject $l */
$l = $this->createMock(IL10N::class);
$chatMessage = new Message($room, $participant, $comment, $l);
$chatMessage->setMessage('Mention to @testUser and @testUser again', []);
self::invokePrivate($this->parser, 'parseMessage', [$chatMessage]);
$expectedMessageParameters = [
'mention-user1' => [
'type' => 'user',
'id' => 'testUser',
'name' => 'testUser display name',
'mention-id' => 'testUser',
]
];
$this->assertEquals('Mention to {mention-user1} and {mention-user1} again', $chatMessage->getMessage());
$this->assertEquals($expectedMessageParameters, $chatMessage->getMessageParameters());
}
public static function dataGetRichMessageWithMentionsFullyIncludedInOtherMentions(): array {
// Based on valid characters from server/lib/private/User/Manager.php
return [
['testUser', 'testUser1', false],
['testUser', 'testUser1', true],
['testUser', 'testUser_1', false],
['testUser', 'testUser_1', true],
['testUser', 'testUser.1', false],
['testUser', 'testUser.1', true],
['testUser', 'testUser@1', false],
['testUser', 'testUser@1', true],
['testUser', 'testUser-1', false],
['testUser', 'testUser-1', true],
['testUser', 'testUser\'1', false],
['testUser', 'testUser\'1', true],
];
}
#[DataProvider('dataGetRichMessageWithMentionsFullyIncludedInOtherMentions')]
public function testGetRichMessageWithMentionsFullyIncludedInOtherMentions(string $baseId, string $longerId, bool $quoted): void {
$mentions = [
['type' => 'user', 'id' => $baseId],
['type' => 'user', 'id' => $longerId],
];
$comment = $this->newComment($mentions);
$this->commentsManager->expects($this->exactly(2))
->method('resolveDisplayName')
->willReturnCallback(function ($type, $id) {
return $id . ' display name';
});
$this->userManager->expects($this->exactly(2))
->method('getDisplayName')
->willReturnMap([
[$longerId, $longerId . ' display name'],
[$baseId, $baseId . ' display name']
]);
/** @var Room&MockObject $room */
$room = $this->createMock(Room::class);
/** @var Participant&MockObject $participant */
$participant = $this->createMock(Participant::class);
/** @var IL10N&MockObject $l */
$l = $this->createMock(IL10N::class);
$chatMessage = new Message($room, $participant, $comment, $l);
if ($quoted) {
$chatMessage->setMessage('Mention to @"' . $baseId . '" and @"' . $longerId . '"', []);
} else {
$chatMessage->setMessage('Mention to @' . $baseId . ' and @' . $longerId, []);
}
self::invokePrivate($this->parser, 'parseMessage', [$chatMessage]);
$expectedMessageParameters = [
'mention-user1' => [
'type' => 'user',
'id' => $longerId,
'name' => $longerId . ' display name',
'mention-id' => $longerId,
],
'mention-user2' => [
'type' => 'user',
'id' => $baseId,
'name' => $baseId . ' display name',
'mention-id' => $baseId,
],
];
$this->assertEquals('Mention to {mention-user2} and {mention-user1}', $chatMessage->getMessage());
$this->assertEquals($expectedMessageParameters, $chatMessage->getMessageParameters());
}
public function testGetRichMessageWithSeveralMentions(): void {
$mentions = [
['type' => 'user', 'id' => 'testUser1'],
['type' => 'user', 'id' => 'testUser2'],
['type' => 'user', 'id' => 'testUser3']
];
$comment = $this->newComment($mentions);
$this->commentsManager->expects($this->exactly(3))
->method('resolveDisplayName')
->willReturnMap([
['user', 'testUser1', 'testUser1 display name'],
['user', 'testUser2', 'testUser2 display name'],
['user', 'testUser3', 'testUser3 display name'],
]);
$this->userManager->expects($this->exactly(3))
->method('getDisplayName')
->willReturnMap([
['testUser1', 'testUser1 display name'],
['testUser2', 'testUser2 display name'],
['testUser3', 'testUser3 display name'],
]);
/** @var Room&MockObject $room */
$room = $this->createMock(Room::class);
/** @var Participant&MockObject $participant */
$participant = $this->createMock(Participant::class);
/** @var IL10N&MockObject $l */
$l = $this->createMock(IL10N::class);
$chatMessage = new Message($room, $participant, $comment, $l);
$chatMessage->setMessage('Mention to @testUser1, @testUser2, @testUser1 again and @testUser3', []);
self::invokePrivate($this->parser, 'parseMessage', [$chatMessage]);
$expectedMessageParameters = [
'mention-user1' => [
'type' => 'user',
'id' => 'testUser1',
'name' => 'testUser1 display name',
'mention-id' => 'testUser1',
],
'mention-user2' => [
'type' => 'user',
'id' => 'testUser2',
'name' => 'testUser2 display name',
'mention-id' => 'testUser2',
],
'mention-user3' => [
'type' => 'user',
'id' => 'testUser3',
'name' => 'testUser3 display name',
'mention-id' => 'testUser3',
]
];
$this->assertEquals('Mention to {mention-user1}, {mention-user2}, {mention-user1} again and {mention-user3}', $chatMessage->getMessage());
$this->assertEquals($expectedMessageParameters, $chatMessage->getMessageParameters());
}
public function testGetRichMessageWithNonExistingUserMention(): void {
$mentions = [
['type' => 'user', 'id' => 'me'],
['type' => 'user', 'id' => 'testUser'],
];
$comment = $this->newComment($mentions);
$this->commentsManager->expects($this->once())
->method('resolveDisplayName')
->with('user', 'testUser')
->willReturn('testUser display name');
$this->userManager->expects($this->exactly(2))
->method('getDisplayName')
->willReturnMap([
['me', null],
['testUser', 'testUser display name'],
]);
/** @var Room&MockObject $room */
$room = $this->createMock(Room::class);
/** @var Participant&MockObject $participant */
$participant = $this->createMock(Participant::class);
/** @var IL10N&MockObject $l */
$l = $this->createMock(IL10N::class);
$chatMessage = new Message($room, $participant, $comment, $l);
$chatMessage->setMessage('Mention @me to @testUser', []);
self::invokePrivate($this->parser, 'parseMessage', [$chatMessage]);
$expectedMessageParameters = [
'mention-user1' => [
'type' => 'user',
'id' => 'testUser',
'name' => 'testUser display name',
'mention-id' => 'testUser',
]
];
$this->assertEquals('Mention @me to {mention-user1}', $chatMessage->getMessage());
$this->assertEquals($expectedMessageParameters, $chatMessage->getMessageParameters());
}
public function testGetRichMessageWhenDisplayNameCanNotBeResolved(): void {
$mentions = [
['type' => 'user', 'id' => 'testUser'],
];
$comment = $this->newComment($mentions);
$this->commentsManager->expects($this->once())
->method('resolveDisplayName')
->willThrowException(new \OutOfBoundsException());
$this->userManager->expects($this->once())
->method('getDisplayName')
->with('testUser')
->willReturn('existing user but does not resolve later');
/** @var Room&MockObject $room */
$room = $this->createMock(Room::class);
/** @var Participant&MockObject $participant */
$participant = $this->createMock(Participant::class);
/** @var IL10N&MockObject $l */
$l = $this->createMock(IL10N::class);
$chatMessage = new Message($room, $participant, $comment, $l);
$chatMessage->setMessage('Mention to @testUser', []);
self::invokePrivate($this->parser, 'parseMessage', [$chatMessage]);
$expectedMessageParameters = [
'mention-user1' => [
'type' => 'user',
'id' => 'testUser',
'name' => '',
'mention-id' => 'testUser',
]
];
$this->assertEquals('Mention to {mention-user1}', $chatMessage->getMessage());
$this->assertEquals($expectedMessageParameters, $chatMessage->getMessageParameters());
}
public function testGetRichMessageWithAtAll(): void {
$mentions = [
['type' => 'user', 'id' => 'all'],
];
$metadata = [
Message::METADATA_CAN_MENTION_ALL => true,
];
$comment = $this->newComment($mentions, $metadata);
/** @var Room&MockObject $room */
$room = $this->createMock(Room::class);
$room->expects($this->once())
->method('getType')
->willReturn(Room::TYPE_GROUP);
$room->expects($this->once())
->method('getToken')
->willReturn('token');
$room->expects($this->once())
->method('getDisplayName')
->willReturn('name');
/** @var Participant&MockObject $participant */
$participant = $this->createMock(Participant::class);
/** @var IL10N&MockObject $l */
$l = $this->createMock(IL10N::class);
$chatMessage = new Message($room, $participant, $comment, $l);
$chatMessage->setMessage('Mention to @all', []);
$this->avatarService->method('getAvatarUrl')
->with($room)
->willReturn('getAvatarUrl');
self::invokePrivate($this->parser, 'parseMessage', [$chatMessage]);
$expectedMessageParameters = [
'mention-call1' => [
'type' => 'call',
'id' => 'token',
'name' => 'name',
'call-type' => 'group',
'icon-url' => 'getAvatarUrl',
'mention-id' => 'all',
]
];
$this->assertEquals('Mention to {mention-call1}', $chatMessage->getMessage());
$this->assertEquals($expectedMessageParameters, $chatMessage->getMessageParameters());
}
public function testGetRichMessageWithFederatedUserMention(): void {
$mentions = [
['type' => 'federated_user', 'id' => 'testUser@example.tld'],
];
$comment = $this->newComment($mentions);
/** @var Room&MockObject $room */
$room = $this->createMock(Room::class);
/** @var Participant&MockObject $participant */
$participant = $this->createMock(Participant::class);
/** @var IL10N&MockObject $l */
$l = $this->createMock(IL10N::class);
$chatMessage = new Message($room, $participant, $comment, $l);
$chatMessage->setMessage('Mention to @"federated_user/testUser@example.tld"', []);
$cloudId = $this->createMock(ICloudId::class);
$cloudId->method('getUser')
->willReturn('testUser');
$cloudId->method('getRemote')
->willReturn('example.tld');
$cloudId->method('getDisplayId')
->willReturn('Display Id');
$this->cloudIdManager->method('resolveCloudId')
->with('testUser@example.tld')
->willReturn($cloudId);
self::invokePrivate($this->parser, 'parseMessage', [$chatMessage]);
$expectedMessageParameters = [
'mention-federated-user1' => [
'type' => 'user',
'id' => 'testUser',
'name' => 'Display Id',
'server' => 'example.tld',
'mention-id' => 'federated_user/testUser@example.tld',
]
];
$this->assertEquals('Mention to {mention-federated-user1}', $chatMessage->getMessage());
$this->assertEquals($expectedMessageParameters, $chatMessage->getMessageParameters());
}
public function testGetRichMessageWhenAGuestWithoutNameIsMentioned(): void {
$mentions = [
['type' => 'guest', 'id' => 'guest/123456'],
];
$comment = $this->newComment($mentions);
/** @var Room&MockObject $room */
$room = $this->createMock(Room::class);
/** @var Participant&MockObject $participant */
$participant = $this->createMock(Participant::class);
/** @var IL10N&MockObject $l */
$l = $this->createMock(IL10N::class);
$this->participantService->method('getParticipantByActor')
->with($room, Attendee::ACTOR_GUESTS, '123456')
->willThrowException(new ParticipantNotFoundException());
$this->l->expects($this->any())
->method('t')
->willReturnCallback(function ($text, $parameters = []) {
return vsprintf($text, $parameters);
});
$chatMessage = new Message($room, $participant, $comment, $l);
$chatMessage->setMessage('Mention to @"guest/123456"', []);
self::invokePrivate($this->parser, 'parseMessage', [$chatMessage]);
$expectedMessageParameters = [
'mention-guest1' => [
'type' => 'guest',
'id' => 'guest/123456',
'name' => 'Guest',
'mention-id' => 'guest/123456',
]
];
$this->assertEquals('Mention to {mention-guest1}', $chatMessage->getMessage());
$this->assertEquals($expectedMessageParameters, $chatMessage->getMessageParameters());
}
public function testGetRichMessageWhenAGuestWithoutNameIsMentionedMultipleTimes(): void {
$mentions = [
['type' => 'guest', 'id' => 'guest/123456'],
];
$comment = $this->newComment($mentions);
/** @var Room&MockObject $room */
$room = $this->createMock(Room::class);
/** @var Participant&MockObject $participant */
$participant = $this->createMock(Participant::class);
/** @var IL10N&MockObject $l */
$l = $this->createMock(IL10N::class);
$this->participantService->method('getParticipantByActor')
->with($room, Attendee::ACTOR_GUESTS, '123456')
->willThrowException(new ParticipantNotFoundException());
$this->l->expects($this->any())
->method('t')
->willReturnCallback(function ($text, $parameters = []) {
return vsprintf($text, $parameters);
});
$chatMessage = new Message($room, $participant, $comment, $l);
$chatMessage->setMessage('Mention to @"guest/123456", and again @"guest/123456"', []);
self::invokePrivate($this->parser, 'parseMessage', [$chatMessage]);
$expectedMessageParameters = [
'mention-guest1' => [
'type' => 'guest',
'id' => 'guest/123456',
'name' => 'Guest',
'mention-id' => 'guest/123456',
]
];
$this->assertEquals('Mention to {mention-guest1}, and again {mention-guest1}', $chatMessage->getMessage());
$this->assertEquals($expectedMessageParameters, $chatMessage->getMessageParameters());
}
public function testGetRichMessageWhenAGuestWithANameIsMentionedMultipleTimes(): void {
$mentions = [
['type' => 'guest', 'id' => 'guest/abcdef'],
];
$comment = $this->newComment($mentions);
/** @var Room&MockObject $room */
$room = $this->createMock(Room::class);
/** @var Participant&MockObject $participant */
$participant = $this->createMock(Participant::class);
/** @var IL10N&MockObject $l */
$l = $this->createMock(IL10N::class);
$attendee = Attendee::fromRow([
'actor_type' => 'guests',
'actor_id' => 'abcdef',
'display_name' => 'Name',
]);
$participant->method('getAttendee')
->willReturn($attendee);
$this->participantService->method('getParticipantByActor')
->with($room, Attendee::ACTOR_GUESTS, 'abcdef')
->willReturn($participant);
$this->l->expects($this->any())
->method('t')
->willReturnCallback(function ($text, $parameters = []) {
return vsprintf($text, $parameters);
});
$chatMessage = new Message($room, $participant, $comment, $l);
$chatMessage->setMessage('Mention to @"guest/abcdef", and again @"guest/abcdef"', []);
self::invokePrivate($this->parser, 'parseMessage', [$chatMessage]);
$expectedMessageParameters = [
'mention-guest1' => [
'type' => 'guest',
'id' => 'guest/abcdef',
'name' => 'Name',
'mention-id' => 'guest/abcdef',
]
];
$this->assertEquals('Mention to {mention-guest1}, and again {mention-guest1}', $chatMessage->getMessage());
$this->assertEquals($expectedMessageParameters, $chatMessage->getMessageParameters());
}
}
@@ -0,0 +1,644 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Tests\php\Chat\SystemMessage;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\Chat\SystemMessage\Listener;
use OCA\Talk\Events\AParticipantModifiedEvent;
use OCA\Talk\Events\ARoomModifiedEvent;
use OCA\Talk\Events\AttendeesAddedEvent;
use OCA\Talk\Events\ParticipantModifiedEvent;
use OCA\Talk\Events\RoomModifiedEvent;
use OCA\Talk\Manager;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\ThreadService;
use OCA\Talk\TalkSession;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUser;
use OCP\IUserSession;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;
#[Group('DB')]
class ListenerTest extends TestCase {
public const DUMMY_REFERENCE_ID = 'DUMMY_REFERENCE_ID';
protected IRequest&MockObject $request;
protected ChatManager&MockObject $chatManager;
protected IUserSession&MockObject $userSession;
protected ISession&MockObject $session;
protected TalkSession&MockObject $talkSession;
protected ITimeFactory&MockObject $timeFactory;
protected IEventDispatcher&MockObject $eventDispatcher;
protected Manager&MockObject $manager;
protected ParticipantService&MockObject $participantService;
protected MessageParser&MockObject $messageParser;
protected ThreadService&MockObject $threadService;
protected LoggerInterface&MockObject $logger;
protected ?array $handlers = null;
protected ?\DateTime $dummyTime = null;
protected ?Listener $listener = null;
protected function setUp(): void {
parent::setUp();
$this->request = $this->createMock(IRequest::class);
$this->request->expects($this->any())
->method('getParam')
->with('referenceId')
->willReturn(self::DUMMY_REFERENCE_ID);
$this->dummyTime = new \DateTime();
$this->chatManager = $this->createMock(ChatManager::class);
$this->session = $this->createMock(ISession::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->talkSession = $this->createMock(TalkSession::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->timeFactory->method('getDateTime')->willReturn($this->dummyTime);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->manager = $this->createMock(Manager::class);
$this->participantService = $this->createMock(ParticipantService::class);
$this->messageParser = $this->createMock(MessageParser::class);
$this->threadService = $this->createMock(ThreadService::class);
$this->logger = $this->createMock(LoggerInterface::class);
$l = $this->createMock(IL10N::class);
$l->expects($this->any())
->method('t')
->willReturnCallback(function ($string, $args) {
return vsprintf($string, $args);
});
$this->handlers = [];
$this->eventDispatcher->method('addListener')
->willReturnCallback(function ($eventName, $handler): void {
$this->handlers[$eventName] ??= [];
$this->handlers[$eventName][] = $handler;
});
$this->listener = new Listener(
$this->request,
$this->chatManager,
$this->talkSession,
$this->session,
$this->userSession,
$this->timeFactory,
$this->manager,
$this->participantService,
$this->messageParser,
$this->threadService,
$l,
$this->logger,
);
}
private function dispatch(string $eventName, $event): void {
$handlers = $this->handlers[$eventName];
$this->assertCount(1, $handlers);
$handlers[0]($event);
}
private function mockLoggedInUser($userId): IUser {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn($userId);
$this->userSession
->method('getUser')
->willReturn($user);
return $user;
}
public function testAfterUsersAddOneToOne(): void {
$room = $this->createMock(Room::class);
$room->expects($this->any())
->method('getType')
->willReturn(Room::TYPE_ONE_TO_ONE);
$participants = [[
'actorType' => 'users',
'actorId' => 'alice_actor',
'participantType' => Participant::USER,
]];
$attendees = array_map(static fn (array $participant) => Attendee::fromParams($participant), $participants);
$event = new AttendeesAddedEvent($room, $attendees);
$this->chatManager->expects($this->never())
->method('addSystemMessage');
self::invokePrivate($this->listener, 'handle', [$event]);
}
public static function dataRoomTypes(): array {
$expectedMessages = [
[
'actorType' => 'users',
'actorId' => 'alice_actor',
'message' => ['message' => 'user_added', 'parameters' => ['user' => 'alice_actor']],
],
[
'actorType' => 'users',
'actorId' => 'alice_actor',
'message' => ['message' => 'user_added', 'parameters' => ['user' => 'bob']],
],
[
'actorType' => 'users',
'actorId' => 'alice_actor',
'message' => ['message' => 'user_added', 'parameters' => ['user' => 'carmen']],
],
[
'actorType' => 'users',
'actorId' => 'alice_actor',
'message' => ['message' => 'user_added', 'parameters' => ['user' => 'delta']],
],
];
$allParticipants = [
// guest will be ignored
[
'actorType' => 'guests'
],
// alice_actor adding self to listed channel
[
'actorType' => 'users',
'actorId' => 'alice_actor',
'participantType' => Participant::USER,
],
// alice_actor added bob
[
'actorType' => 'users',
'actorId' => 'bob',
'participantType' => Participant::USER,
],
// empty participant type
[
'actorType' => 'users',
'actorId' => 'carmen',
],
// alice_actor adding self-joined mode
[
'actorType' => 'users',
'actorId' => 'delta',
'participantType' => Participant::USER_SELF_JOINED,
],
];
return [
[Room::TYPE_GROUP, '', $allParticipants, $expectedMessages],
[Room::TYPE_PUBLIC, '', $allParticipants, $expectedMessages],
[Room::TYPE_ONE_TO_ONE, '', $allParticipants, []],
[Room::TYPE_GROUP, 'file', $allParticipants, $expectedMessages],
[Room::TYPE_PUBLIC, 'file', $allParticipants, $expectedMessages],
];
}
#[DataProvider('dataRoomTypes')]
public function testAfterUsersAdd(int $roomType, string $objectType, array $participants, array $expectedMessages): void {
$this->mockLoggedInUser('alice_actor');
$room = $this->createMock(Room::class);
$room->method('getType')->willReturn($roomType);
$room->method('getObjectType')->willReturn($objectType);
$attendees = array_map(static fn (array $participant) => Attendee::fromParams($participant), $participants);
// TODO: add all cases
$event = new AttendeesAddedEvent($room, $attendees);
$consecutive = [];
foreach ($expectedMessages as $expectedMessage) {
$consecutive[] = [
$room,
null,
$expectedMessage['actorType'],
$expectedMessage['actorId'],
json_encode($expectedMessage['message']),
$this->dummyTime,
false,
self::DUMMY_REFERENCE_ID,
null,
false,
false,
0,
];
}
if (!empty($consecutive)) {
$i = 0;
$this->chatManager->expects($this->exactly(count($consecutive)))
->method('addSystemMessage')
->willReturnCallback(function () use ($consecutive, &$i) {
$this->assertArrayHasKey($i, $consecutive);
$this->assertSame($consecutive[$i], func_get_args());
$i++;
return $this->createMock(IComment::class);
});
} else {
$this->chatManager->expects($this->never())
->method('addSystemMessage');
}
self::invokePrivate($this->listener, 'handle', [$event]);
}
public static function dataParticipantTypeChange(): array {
return [
[
Attendee::ACTOR_GROUPS,
Participant::USER,
Participant::MODERATOR,
[],
],
[
Attendee::ACTOR_USERS,
Participant::USER,
Participant::MODERATOR,
[['message' => 'moderator_promoted', 'parameters' => ['user' => 'bob_participant']]],
],
[
Attendee::ACTOR_USERS,
Participant::MODERATOR,
Participant::USER,
[['message' => 'moderator_demoted', 'parameters' => ['user' => 'bob_participant']]],
],
[
Attendee::ACTOR_GUESTS,
Participant::GUEST,
Participant::GUEST_MODERATOR,
[['message' => 'guest_moderator_promoted', 'parameters' => ['type' => 'guests', 'id' => 'bob_participant']]],
],
[
Attendee::ACTOR_GUESTS,
Participant::GUEST_MODERATOR,
Participant::GUEST,
[['message' => 'guest_moderator_demoted', 'parameters' => ['type' => 'guests', 'id' => 'bob_participant']]],
],
[
Attendee::ACTOR_EMAILS,
Participant::GUEST,
Participant::GUEST_MODERATOR,
[['message' => 'guest_moderator_promoted', 'parameters' => ['type' => 'emails', 'id' => 'bob_participant']]],
],
[
Attendee::ACTOR_EMAILS,
Participant::GUEST_MODERATOR,
Participant::GUEST,
[['message' => 'guest_moderator_demoted', 'parameters' => ['type' => 'emails', 'id' => 'bob_participant']]],
],
[
Attendee::ACTOR_USERS,
Participant::USER_SELF_JOINED,
Participant::USER,
[['message' => 'user_added', 'parameters' => ['user' => 'bob_participant']]],
],
];
}
#[DataProvider('dataParticipantTypeChange')]
public function testAfterParticipantTypeSet(string $actorType, int $oldParticipantType, int $newParticipantType, array $expectedMessages): void {
$this->mockLoggedInUser('alice_actor');
$room = $this->createMock(Room::class);
$room->method('getType')->willReturn(Room::TYPE_GROUP);
$attendee = new Attendee();
$attendee->setActorId('bob_participant');
$attendee->setActorType($actorType);
$participant = $this->createMock(Participant::class);
$participant->method('getAttendee')->willReturn($attendee);
$event = new ParticipantModifiedEvent($room, $participant, AParticipantModifiedEvent::PROPERTY_TYPE, $newParticipantType, $oldParticipantType);
foreach ($expectedMessages as $expectedMessage) {
$consecutive[] = [
$room,
null,
Attendee::ACTOR_USERS,
'alice_actor',
json_encode($expectedMessage),
$this->dummyTime,
false,
self::DUMMY_REFERENCE_ID,
null,
false,
false,
0,
];
}
if (isset($consecutive)) {
$i = 0;
$this->chatManager->expects($this->exactly(count($consecutive)))
->method('addSystemMessage')
->willReturnCallback(function () use ($consecutive, &$i) {
$this->assertArrayHasKey($i, $consecutive);
$this->assertSame($consecutive[$i], func_get_args());
$i++;
return $this->createMock(IComment::class);
});
} else {
$this->chatManager->expects($this->never())
->method('addSystemMessage');
}
self::invokePrivate($this->listener, 'handle', [$event]);
}
public static function dataCallRecordingChange(): array {
return [
[
Room::RECORDING_VIDEO_STARTING,
Room::RECORDING_NONE,
null,
null,
null,
],
[
Room::RECORDING_VIDEO_STARTING,
Room::RECORDING_NONE,
Attendee::ACTOR_USERS,
'alice',
null,
],
[
Room::RECORDING_VIDEO,
Room::RECORDING_VIDEO_STARTING,
null,
null,
['message' => 'recording_started', 'parameters' => []],
],
[
Room::RECORDING_VIDEO,
Room::RECORDING_VIDEO_STARTING,
Attendee::ACTOR_USERS,
'alice',
['message' => 'recording_started', 'parameters' => []],
],
[
Room::RECORDING_VIDEO,
Room::RECORDING_NONE,
null,
null,
['message' => 'recording_started', 'parameters' => []],
],
[
Room::RECORDING_VIDEO,
Room::RECORDING_NONE,
Attendee::ACTOR_USERS,
'alice',
['message' => 'recording_started', 'parameters' => []],
],
[
Room::RECORDING_AUDIO_STARTING,
Room::RECORDING_NONE,
null,
null,
null,
],
[
Room::RECORDING_AUDIO_STARTING,
Room::RECORDING_NONE,
Attendee::ACTOR_USERS,
'alice',
null,
],
[
Room::RECORDING_AUDIO,
Room::RECORDING_AUDIO_STARTING,
null,
null,
['message' => 'audio_recording_started', 'parameters' => []],
],
[
Room::RECORDING_AUDIO,
Room::RECORDING_AUDIO_STARTING,
Attendee::ACTOR_USERS,
'alice',
['message' => 'audio_recording_started', 'parameters' => []],
],
[
Room::RECORDING_AUDIO,
Room::RECORDING_NONE,
null,
null,
['message' => 'audio_recording_started', 'parameters' => []],
],
[
Room::RECORDING_AUDIO,
Room::RECORDING_NONE,
Attendee::ACTOR_USERS,
'alice',
['message' => 'audio_recording_started', 'parameters' => []],
],
[
Room::RECORDING_NONE,
Room::RECORDING_VIDEO_STARTING,
null,
null,
null,
],
[
Room::RECORDING_NONE,
Room::RECORDING_VIDEO_STARTING,
Attendee::ACTOR_USERS,
'bob',
null,
],
[
Room::RECORDING_NONE,
Room::RECORDING_VIDEO,
null,
null,
null,
],
[
Room::RECORDING_NONE,
Room::RECORDING_VIDEO,
Attendee::ACTOR_USERS,
'bob',
['message' => 'recording_stopped', 'parameters' => []],
],
[
Room::RECORDING_NONE,
Room::RECORDING_AUDIO_STARTING,
null,
null,
null,
],
[
Room::RECORDING_NONE,
Room::RECORDING_AUDIO_STARTING,
Attendee::ACTOR_USERS,
'bob',
null,
],
[
Room::RECORDING_NONE,
Room::RECORDING_AUDIO,
null,
null,
null,
],
[
Room::RECORDING_NONE,
Room::RECORDING_AUDIO,
Attendee::ACTOR_USERS,
'bob',
['message' => 'audio_recording_stopped', 'parameters' => []],
],
[
Room::RECORDING_FAILED,
Room::RECORDING_VIDEO_STARTING,
null,
null,
null,
],
[
Room::RECORDING_FAILED,
Room::RECORDING_AUDIO_STARTING,
null,
null,
null,
],
[
Room::RECORDING_FAILED,
Room::RECORDING_VIDEO,
null,
null,
['message' => 'recording_failed', 'parameters' => []],
],
[
Room::RECORDING_FAILED,
Room::RECORDING_AUDIO,
null,
null,
['message' => 'recording_failed', 'parameters' => []],
],
[
Room::RECORDING_VIDEO_STARTING,
Room::RECORDING_FAILED,
null,
null,
null,
],
[
Room::RECORDING_VIDEO_STARTING,
Room::RECORDING_FAILED,
Attendee::ACTOR_USERS,
'alice',
null,
],
[
Room::RECORDING_VIDEO,
Room::RECORDING_FAILED,
null,
null,
['message' => 'recording_started', 'parameters' => []],
],
[
Room::RECORDING_VIDEO,
Room::RECORDING_FAILED,
Attendee::ACTOR_USERS,
'alice',
['message' => 'recording_started', 'parameters' => []],
],
[
Room::RECORDING_AUDIO_STARTING,
Room::RECORDING_FAILED,
null,
null,
null,
],
[
Room::RECORDING_AUDIO_STARTING,
Room::RECORDING_FAILED,
Attendee::ACTOR_USERS,
'alice',
null,
],
[
Room::RECORDING_AUDIO,
Room::RECORDING_FAILED,
null,
null,
['message' => 'audio_recording_started', 'parameters' => []],
],
[
Room::RECORDING_AUDIO,
Room::RECORDING_FAILED,
Attendee::ACTOR_USERS,
'alice',
['message' => 'audio_recording_started', 'parameters' => []],
],
];
}
#[DataProvider('dataCallRecordingChange')]
public function testAfterCallRecordingSet(int $newStatus, int $oldStatus, ?string $actorType, ?string $actorId, ?array $expectedMessage): void {
$this->mockLoggedInUser('logged_in_user');
$room = $this->createMock(Room::class);
$room->expects($this->any())
->method('getType')
->willReturn(Room::TYPE_PUBLIC);
if ($actorType !== null && $actorId !== null) {
$attendee = new Attendee();
$attendee->setActorType($actorType);
$attendee->setActorId($actorId);
$participant = $this->createMock(Participant::class);
$participant->method('getAttendee')->willReturn($attendee);
$expectedActorType = $actorType;
$expectedActorId = $actorId;
} else {
$participant = null;
$expectedActorType = Attendee::ACTOR_USERS;
$expectedActorId = 'logged_in_user';
}
$event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_CALL_RECORDING, $newStatus, $oldStatus, $participant);
if ($expectedMessage !== null) {
$this->chatManager->expects($this->once())
->method('addSystemMessage')
->with(
$room,
$participant,
$expectedActorType,
$expectedActorId,
json_encode($expectedMessage),
$this->dummyTime,
false,
self::DUMMY_REFERENCE_ID,
null,
false
);
} else {
$this->chatManager->expects($this->never())
->method('addSystemMessage');
}
self::invokePrivate($this->listener, 'handle', [$event]);
}
}