Browse Source

Merge pull request #7306 from nextcloud/feature/noid/simple-polls

📊 Simple polls API
pull/7484/head
Joas Schilling 3 years ago
committed by GitHub
parent
commit
2f16ea57ae
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      appinfo/info.xml
  2. 1
      appinfo/routes.php
  3. 48
      appinfo/routes/routesPollController.php
  4. 10
      docs/constants.md
  5. 114
      docs/poll.md
  6. 6
      lib/Chat/ChatManager.php
  7. 12
      lib/Chat/Parser/SystemMessage.php
  8. 260
      lib/Controller/PollController.php
  9. 19
      lib/GuestManager.php
  10. 12
      lib/Listener/UserDisplayNameListener.php
  11. 145
      lib/Migration/Version15000Date20220503121308.php
  12. 108
      lib/Model/Poll.php
  13. 70
      lib/Model/PollMapper.php
  14. 77
      lib/Model/Vote.php
  15. 91
      lib/Model/VoteMapper.php
  16. 281
      lib/Service/PollService.php
  17. 1
      mkdocs.yml
  18. 181
      tests/integration/features/bootstrap/FeatureContext.php
  19. 525
      tests/integration/features/chat/poll.feature
  20. 6
      tests/integration/spreedcheats/lib/Controller/ApiController.php
  21. 7
      tests/php/Chat/ChatManagerTest.php

2
appinfo/info.xml

@ -16,7 +16,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m
]]></description>
<version>15.0.0-dev.3</version>
<version>15.0.0-dev.4</version>
<licence>agpl</licence>
<author>Aleksandra Lazarević</author>

1
appinfo/routes.php

@ -35,6 +35,7 @@ return array_merge_recursive(
include(__DIR__ . '/routes/routesMatterbridgeController.php'),
include(__DIR__ . '/routes/routesMatterbridgeSettingsController.php'),
include(__DIR__ . '/routes/routesPageController.php'),
include(__DIR__ . '/routes/routesPollController.php'),
include(__DIR__ . '/routes/routesPublicShareAuthController.php'),
include(__DIR__ . '/routes/routesReactionController.php'),
include(__DIR__ . '/routes/routesRoomController.php'),

48
appinfo/routes/routesPollController.php

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
$requirements = [
'apiVersion' => 'v1',
'token' => '^[a-z0-9]{4,30}$',
];
$requirementsWithPollId = [
'apiVersion' => 'v1',
'token' => '^[a-z0-9]{4,30}$',
'pollId' => '^[0-9]+$',
];
return [
'ocs' => [
/** @see \OCA\Talk\Controller\PollController::createPoll() */
['name' => 'Poll#createPoll', 'url' => '/api/{apiVersion}/poll/{token}', 'verb' => 'POST', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\PollController::showPoll() */
['name' => 'Poll#showPoll', 'url' => '/api/{apiVersion}/poll/{token}/{pollId}', 'verb' => 'GET', 'requirements' => $requirementsWithPollId],
/** @see \OCA\Talk\Controller\PollController::votePoll() */
['name' => 'Poll#votePoll', 'url' => '/api/{apiVersion}/poll/{token}/{pollId}', 'verb' => 'POST', 'requirements' => $requirementsWithPollId],
/** @see \OCA\Talk\Controller\PollController::closePoll() */
['name' => 'Poll#closePoll', 'url' => '/api/{apiVersion}/poll/{token}/{pollId}', 'verb' => 'DELETE', 'requirements' => $requirementsWithPollId],
],
];

10
docs/constants.md

@ -99,6 +99,16 @@ title: Constants
* `other` - Shared objects not falling into any other category
* `voice` - Voice messages
## Poll
### Poll status
* `0` - Open: Participants can cast votes
* `1` - Closed: Participants can no longer cast votes and the result is displayed
### Poll mode
* `0` - Public: Participants can see the result immediately and also who voted for which option
* `1` - Hidden: The result is hidden until the poll is closed and then only the number of votes for each option are displayed
## Signaling modes
* `internal` - No external signaling server is used
* `external` - A single external signaling server is used

114
docs/poll.md

@ -0,0 +1,114 @@
# Poll API
Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
## Create a poll in a conversation
* Method: `POST`
* Endpoint: `/poll/{token}`
* Data:
| field | type | Description |
|--------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `question` | string | The question of the poll |
| `options` | string[] | Array of strings with the voting options |
| `resultMode` | int | The result and voting mode of the poll, `0` means participants can immediatelly see the result and who voted for which option. `1` means the result is hidden until the poll is closed and then only the summary is published. |
| `maxVotes` | int | Maximum amount of options a participant can vote for |
* Response:
- Status code:
+ `201 Created`
+ `400 Bad Request` When the question or the options were too long
+ `403 Forbidden` When the conversation is read-only
+ `403 Forbidden` When the actor does not have chat permissions
+ `404 Not Found` When the conversation could not be found for the participant
+ `412 Precondition Failed` When the lobby is active and the user is not a moderator
- Data:
See [Poll data](#poll-data)
## Get state or result of a poll
* Method: `GET`
* Endpoint: `/poll/{token}/{pollId}`
* Response:
- Status code:
+ `201 Created`
+ `400 Bad Request` In case of any other error
+ `404 Not Found` When the conversation could not be found for the participant
+ `404 Not Found` When the poll id could not be found in the conversation
+ `412 Precondition Failed` When the lobby is active and the user is not a moderator
- Data:
See [Poll data](#poll-data)
## Vote on a poll
* Method: `POST`
* Endpoint: `/poll/{token}/{pollId}`
* Data:
| field | type | Description |
|--------------|-------|--------------------------------------------------|
| `optionIds` | int[] | The option IDs the participant wants to vote for |
* Response:
- Status code:
+ `201 Created`
+ `400 Bad Request` When an option id is invalid
+ `400 Bad Request` When too many options were voted
+ `404 Not Found` When the conversation could not be found for the participant
+ `404 Not Found` When the poll id could not be found in the conversation
+ `412 Precondition Failed` When the lobby is active and the user is not a moderator
- Data:
See [Poll data](#poll-data)
## Close a poll
* Method: `DELETE`
* Endpoint: `/poll/{token}/{pollId}`
* Response:
- Status code:
+ `200 OK`
+ `403 Forbidden` When the participant is not the author of the poll or a moderator
+ `404 Not Found` When the conversation could not be found for the participant
+ `404 Not Found` When the poll id could not be found in the conversation
+ `412 Precondition Failed` When the lobby is active and the user is not a moderator
- Data:
See [Poll data](#poll-data)
## Poll data
| field | type | Description |
|--------------------|----------|------------------------------------------------------------------------------------------------------------------|
| `id` | int | ID of the poll |
| `question` | string | The question of the poll |
| `options` | string[] | The options participants can vote for |
| `votes` | int[] | Map with optionId => number of votes (only available for when the actor voted on public poll or the poll is closed) |
| `actorType` | string | Actor type of the poll author (see [Constants - Attendee types](constants.md#attendee-types)) |
| `actorId` | string | Actor ID identifying the poll author |
| `actorDisplayName` | string | Display name of the poll author |
| `status` | int | Status of the poll (see [Constants - Poll status](constants.md#poll-status)) |
| `resultMode` | int | Result mode of the poll (see [Constants - Poll mode](constants.md#poll-mode)) |
| `maxVotes` | int | Maximum amount of options a user can vote for, `0` means unlimited |
| `votedSelf` | int[] | Array of option ids the participant voted for |
| `numVoters` | int | The number of unique voters that (only available for when the actor voted on public poll or the poll is closed) |
| `details` | array[] | Detailed list who voted for which option (only available for public closed polls), see [Details](#details) below |
### Details
| field | type | Description |
|------------------|--------|--------------------------------------------------------------------------------------------------------------|
| actorType | string | The actor type of the participant that voted (see [Constants - Attendee types](constants.md#attendee-types)) |
| actorId | string | The actor id of the participant that voted |
| actorDisplayName | string | The display name of the participant that voted |
| optionId | int | The option that was voted for |

6
lib/Chat/ChatManager.php

@ -33,6 +33,7 @@ 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\Share\RoomShareProvider;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
@ -85,6 +86,7 @@ class ChatManager {
private IManager $shareManager;
private RoomShareProvider $shareProvider;
private ParticipantService $participantService;
private PollService $pollService;
private Notifier $notifier;
protected ITimeFactory $timeFactory;
protected ICache $cache;
@ -98,6 +100,7 @@ class ChatManager {
IManager $shareManager,
RoomShareProvider $shareProvider,
ParticipantService $participantService,
PollService $pollService,
Notifier $notifier,
ICacheFactory $cacheFactory,
ITimeFactory $timeFactory,
@ -109,6 +112,7 @@ class ChatManager {
$this->shareManager = $shareManager;
$this->shareProvider = $shareProvider;
$this->participantService = $participantService;
$this->pollService = $pollService;
$this->notifier = $notifier;
$this->cache = $cacheFactory->createDistributed('talk/lastmsgid');
$this->unreadCountCache = $cacheFactory->createDistributed('talk/unreadcount');
@ -621,6 +625,8 @@ class ChatManager {
$this->notifier->removePendingNotificationsForRoom($chat);
$this->attachmentService->deleteAttachmentsForRoom($chat);
$this->pollService->deleteByRoomId($chat->getId());
}
/**

12
lib/Chat/Parser/SystemMessage.php

@ -461,6 +461,18 @@ class SystemMessage {
if ($currentUserIsActor) {
$parsedMessage = $this->l->t('You cleared the history of the conversation');
}
} elseif ($message === 'poll_closed') {
$parsedParameters['poll'] = $parameters['poll'];
$parsedMessage = $this->l->t('{actor} closed the poll {poll}');
if ($currentUserIsActor) {
$parsedMessage = $this->l->t('You closed the poll {poll}');
}
} elseif ($message === 'poll_voted') {
$parsedParameters['poll'] = $parameters['poll'];
$parsedMessage = $this->l->t('{actor} voted on the poll {poll}');
if ($currentUserIsActor) {
$parsedMessage = $this->l->t('You voted on the poll {poll}');
}
} else {
throw new \OutOfBoundsException('Unknown subject');
}

260
lib/Controller/PollController.php

@ -0,0 +1,260 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Talk\Controller;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Exceptions\WrongPermissionsException;
use OCA\Talk\Model\Poll;
use OCA\Talk\Model\Vote;
use OCA\Talk\Service\AttachmentService;
use OCA\Talk\Service\PollService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\Exception;
use OCP\IRequest;
use Psr\Log\LoggerInterface;
class PollController extends AEnvironmentAwareController {
protected ChatManager $chatManager;
protected PollService $pollService;
protected AttachmentService $attachmentService;
protected ITimeFactory $timeFactory;
protected LoggerInterface $logger;
public function __construct(string $appName,
IRequest $request,
ChatManager $chatManager,
PollService $pollService,
AttachmentService $attachmentService,
ITimeFactory $timeFactory,
LoggerInterface $logger) {
parent::__construct($appName, $request);
$this->pollService = $pollService;
$this->attachmentService = $attachmentService;
$this->chatManager = $chatManager;
$this->timeFactory = $timeFactory;
$this->logger = $logger;
}
/**
* @PublicPage
* @RequireParticipant
* @RequireReadWriteConversation
* @RequirePermissions(permissions=chat)
* @RequireModeratorOrNoLobby
*
* @param string $question
* @param array $options
* @param int $resultMode
* @param int $maxVotes
* @return DataResponse
*/
public function createPoll(string $question, array $options, int $resultMode, int $maxVotes): DataResponse {
$attendee = $this->participant->getAttendee();
try {
$poll = $this->pollService->createPoll(
$this->room->getId(),
$attendee->getActorType(),
$attendee->getActorId(),
$attendee->getDisplayName(),
$question,
$options,
$resultMode,
$maxVotes
);
} catch (\Exception $e) {
$this->logger->error('Error creating poll', ['exception' => $e]);
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
$message = json_encode([
'message' => 'object_shared',
'parameters' => [
'objectType' => 'talk-poll',
'objectId' => $poll->getId(),
'metaData' => [
'type' => 'talk-poll',
'id' => $poll->getId(),
'name' => $question,
]
],
], JSON_THROW_ON_ERROR);
try {
$this->chatManager->addSystemMessage($this->room, $attendee->getActorType(), $attendee->getActorId(), $message, $this->timeFactory->getDateTime(), true);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
return new DataResponse($this->renderPoll($poll, []), Http::STATUS_CREATED);
}
/**
* @PublicPage
* @RequireParticipant
* @RequireModeratorOrNoLobby
*
* @param int $pollId
* @return DataResponse
*/
public function showPoll(int $pollId): DataResponse {
try {
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
} catch (DoesNotExistException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$votedSelf = $this->pollService->getVotesForActor($this->participant, $poll);
$detailedVotes = [];
if ($poll->getResultMode() === Poll::MODE_PUBLIC && $poll->getStatus() === Poll::STATUS_CLOSED) {
$detailedVotes = $this->pollService->getVotes($poll);
}
return new DataResponse($this->renderPoll($poll, $votedSelf, $detailedVotes));
}
/**
* @PublicPage
* @RequireParticipant
* @RequireModeratorOrNoLobby
*
* @param int $pollId
* @param int[] $optionIds
* @return DataResponse
*/
public function votePoll(int $pollId, array $optionIds): DataResponse {
try {
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
} catch (\Exception $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
if ($poll->getStatus() === Poll::STATUS_CLOSED) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
try {
$votedSelf = $this->pollService->votePoll($this->participant, $poll, $optionIds);
} catch (\RuntimeException $e) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
if ($poll->getResultMode() === Poll::MODE_PUBLIC) {
$attendee = $this->participant->getAttendee();
try {
$message = json_encode([
'message' => 'poll_voted',
'parameters' => [
'poll' => [
'type' => 'talk-poll',
'id' => $poll->getId(),
'name' => $poll->getQuestion(),
],
],
], JSON_THROW_ON_ERROR);
$this->chatManager->addSystemMessage($this->room, $attendee->getActorType(), $attendee->getActorId(), $message, $this->timeFactory->getDateTime(), true);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
}
return new DataResponse($this->renderPoll($poll, $votedSelf));
}
/**
* @PublicPage
* @RequireParticipant
* @RequireModeratorOrNoLobby
*
* @param int $pollId
* @return DataResponse
*/
public function closePoll(int $pollId): DataResponse {
try {
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
} catch (\Exception $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$poll->setStatus(Poll::STATUS_CLOSED);
try {
$this->pollService->updatePoll($this->participant, $poll);
} catch (WrongPermissionsException $e) {
return new DataResponse([], Http::STATUS_FORBIDDEN);
} catch (Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
}
$attendee = $this->participant->getAttendee();
try {
$message = json_encode([
'message' => 'poll_closed',
'parameters' => [
'poll' => [
'type' => 'talk-poll',
'id' => $poll->getId(),
'name' => $poll->getQuestion(),
],
],
], JSON_THROW_ON_ERROR);
$this->chatManager->addSystemMessage($this->room, $attendee->getActorType(), $attendee->getActorId(), $message, $this->timeFactory->getDateTime(), true);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
$detailedVotes = [];
if ($poll->getResultMode() === Poll::MODE_PUBLIC) {
$detailedVotes = $this->pollService->getVotes($poll);
}
$votedSelf = $this->pollService->getVotesForActor($this->participant, $poll);
return new DataResponse($this->renderPoll($poll, $votedSelf, $detailedVotes));
}
protected function renderPoll(Poll $poll, array $votedSelf = [], array $detailedVotes = []): array {
$data = $poll->asArray();
unset($data['roomId']);
$canSeeSummary = !empty($votedSelf) && $poll->getResultMode() === Poll::MODE_PUBLIC;
if (!$canSeeSummary && $poll->getStatus() === Poll::STATUS_OPEN) {
$data['votes'] = [];
$data['numVoters'] = 0;
} elseif ($poll->getResultMode() === Poll::MODE_PUBLIC && $poll->getStatus() === Poll::STATUS_CLOSED) {
$data['details'] = array_map(static fn (Vote $vote) => $vote->asArray(), $detailedVotes);
}
$data['votedSelf'] = array_map(static fn (Vote $vote) => $vote->getOptionId(), $votedSelf);
return $data;
}
}

19
lib/GuestManager.php

@ -27,6 +27,7 @@ use OCA\Talk\Events\AddEmailEvent;
use OCA\Talk\Events\ModifyParticipantEvent;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\PollService;
use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IL10N;
@ -42,19 +43,13 @@ class GuestManager {
public const EVENT_AFTER_NAME_UPDATE = self::class . '::updateName';
protected Config $talkConfig;
protected IMailer $mailer;
protected Defaults $defaults;
protected IUserSession $userSession;
private ParticipantService $participantService;
protected ParticipantService $participantService;
protected PollService $pollService;
protected IURLGenerator $url;
protected IL10N $l;
protected IEventDispatcher $dispatcher;
public function __construct(Config $talkConfig,
@ -62,6 +57,7 @@ class GuestManager {
Defaults $defaults,
IUserSession $userSession,
ParticipantService $participantService,
PollService $pollService,
IURLGenerator $url,
IL10N $l,
IEventDispatcher $dispatcher) {
@ -70,6 +66,7 @@ class GuestManager {
$this->defaults = $defaults;
$this->userSession = $userSession;
$this->participantService = $participantService;
$this->pollService = $pollService;
$this->url = $url;
$this->l = $l;
$this->dispatcher = $dispatcher;
@ -89,6 +86,12 @@ class GuestManager {
$displayName
);
$this->pollService->updateDisplayNameForActor(
$attendee->getActorType(),
$attendee->getActorId(),
$displayName
);
$event = new ModifyParticipantEvent($room, $participant, 'name', $displayName);
$this->dispatcher->dispatch(self::EVENT_AFTER_NAME_UPDATE, $event);
}

12
lib/Listener/UserDisplayNameListener.php

@ -25,15 +25,19 @@ namespace OCA\Talk\Listener;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\PollService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\User\Events\UserChangedEvent;
class UserDisplayNameListener implements IEventListener {
private ParticipantService $participantService;
private PollService $pollService;
public function __construct(ParticipantService $participantService) {
public function __construct(ParticipantService $participantService,
PollService $pollService) {
$this->participantService = $participantService;
$this->pollService = $pollService;
}
public function handle(Event $event): void {
@ -52,5 +56,11 @@ class UserDisplayNameListener implements IEventListener {
$event->getUser()->getUID(),
(string) $event->getValue()
);
$this->pollService->updateDisplayNameForActor(
Attendee::ACTOR_USERS,
$event->getUser()->getUID(),
(string) $event->getValue()
);
}
}

145
lib/Migration/Version15000Date20220503121308.php

@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Talk\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version15000Date20220503121308 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('talk_polls')) {
$table = $schema->createTable('talk_polls');
$table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 20,
]);
$table->addColumn('room_id', Types::BIGINT, [
'notnull' => true,
'length' => 20,
]);
$table->addColumn('question', Types::TEXT, [
'notnull' => false,
'length' => null,
]);
$table->addColumn('options', Types::TEXT, [
'notnull' => false,
'length' => null,
]);
$table->addColumn('votes', Types::TEXT, [
'notnull' => false,
'length' => null,
]);
$table->addColumn('num_voters', Types::BIGINT, [
'notnull' => false,
'length' => 20,
'default' => 0,
]);
$table->addColumn('actor_type', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('actor_id', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('display_name', Types::STRING, [
'notnull' => false,
'length' => 255,
]);
$table->addColumn('status', Types::SMALLINT, [
'notnull' => false,
'default' => 0,
]);
$table->addColumn('result_mode', Types::SMALLINT, [
'notnull' => false,
'default' => 0,
]);
$table->addColumn('max_votes', Types::INTEGER, [
'notnull' => true,
'default' => 0,
]);
$table->setPrimaryKey(['id']);
$table->addIndex(['room_id'], 'talk_poll_room');
}
if (!$schema->hasTable('talk_poll_votes')) {
$table = $schema->createTable('talk_poll_votes');
$table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 20,
]);
$table->addColumn('poll_id', Types::BIGINT, [
'notnull' => true,
'length' => 20,
]);
$table->addColumn('room_id', Types::BIGINT, [
'notnull' => true,
'length' => 20,
]);
$table->addColumn('actor_type', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('actor_id', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('display_name', Types::STRING, [
'notnull' => false,
'length' => 255,
]);
$table->addColumn('option_id', Types::INTEGER, [
'notnull' => true,
'length' => 6,
'default' => 1,
]);
$table->setPrimaryKey(['id']);
$table->addIndex(['poll_id', 'actor_type', 'actor_id'], 'talk_poll_vote');
$table->addIndex(['room_id'], 'talk_vote_room');
}
return $schema;
}
}

108
lib/Model/Poll.php

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Talk\Model;
use OCP\AppFramework\Db\Entity;
/**
* @method void setRoomId(int $roomId)
* @method int getRoomId()
* @method void setQuestion(string $question)
* @method string getQuestion()
* @method void setOptions(string $options)
* @method string getOptions()
* @method void setVotes(string $votes)
* @method string getVotes()
* @method void setNumVoters(int $numVoters)
* @method int getNumVoters()
* @method void setActorType(string $actorType)
* @method string getActorType()
* @method void setActorId(string $actorId)
* @method string getActorId()
* @method void setDisplayName(string $displayName)
* @method string getDisplayName()
* @method void setStatus(int $status)
* @method int getStatus()
* @method void setResultMode(int $resultMode)
* @method int getResultMode()
* @method void setMaxVotes(int $maxVotes)
* @method int getMaxVotes()
*/
class Poll extends Entity {
public const STATUS_OPEN = 0;
public const STATUS_CLOSED = 1;
public const MODE_PUBLIC = 0;
public const MODE_HIDDEN = 1;
public const MAX_VOTES_UNLIMITED = 0;
protected int $roomId = 0;
protected string $question = '';
protected string $options = '';
protected string $votes = '';
protected int $numVoters = 0;
protected string $actorType = '';
protected string $actorId = '';
protected ?string $displayName = null;
protected int $status = self::STATUS_OPEN;
protected int $resultMode = self::MODE_PUBLIC;
protected int $maxVotes = self::MAX_VOTES_UNLIMITED;
public function __construct() {
$this->addType('roomId', 'int');
$this->addType('question', 'string');
$this->addType('options', 'string');
$this->addType('votes', 'string');
$this->addType('numVoters', 'int');
$this->addType('actorType', 'string');
$this->addType('actorId', 'string');
$this->addType('displayName', 'string');
$this->addType('status', 'int');
$this->addType('resultMode', 'int');
$this->addType('maxVotes', 'int');
}
/**
* @return array
*/
public function asArray(): array {
return [
'id' => $this->getId(),
// The room id is not needed on the API level but only internally for optimising database queries
// 'roomId' => $this->getRoomId(),
'question' => $this->getQuestion(),
'options' => json_decode($this->getOptions(), true, 512, JSON_THROW_ON_ERROR),
'votes' => json_decode($this->getVotes(), true, 512, JSON_THROW_ON_ERROR),
'numVoters' => $this->getNumVoters(),
'actorType' => $this->getActorType(),
'actorId' => $this->getActorId(),
'actorDisplayName' => $this->getDisplayName(),
'status' => $this->getStatus(),
'resultMode' => $this->getResultMode(),
'maxVotes' => $this->getMaxVotes(),
];
}
}

70
lib/Model/PollMapper.php

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Talk\Model;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
class PollMapper extends QBMapper {
/**
* @param IDBConnection $db
*/
public function __construct(IDBConnection $db) {
parent::__construct($db, 'talk_polls', Poll::class);
}
/**
* @param int $pollId
* @return Poll
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws Exception
*/
public function getByPollId(int $pollId): Poll {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from($this->getTableName())
->where($query->expr()->eq('id', $query->createNamedParameter($pollId, IQueryBuilder::PARAM_INT)));
return $this->findEntity($query);
}
public function deleteByRoomId(int $roomId): void {
$query = $this->db->getQueryBuilder();
$query->delete($this->getTableName())
->where($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT)));
$query->executeStatement();
}
}

77
lib/Model/Vote.php

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Talk\Model;
use OCP\AppFramework\Db\Entity;
/**
* @method void setPollId(int $pollId)
* @method int getPollId()
* @method void setRoomId(int $roomId)
* @method int getRoomId()
* @method void setActorType(string $actorType)
* @method string getActorType()
* @method void setActorId(string $actorId)
* @method string getActorId()
* @method void setDisplayName(string $displayName)
* @method string getDisplayName()
* @method void setOptionId(int $optionId)
* @method int getOptionId()
*/
class Vote extends Entity {
protected int $pollId = 0;
protected int $roomId = 0;
protected string $actorType = '';
protected string $actorId = '';
protected ?string $displayName = null;
protected int $optionId = 0;
public function __construct() {
$this->addType('pollId', 'int');
$this->addType('roomId', 'int');
$this->addType('actorType', 'string');
$this->addType('actorId', 'string');
$this->addType('displayName', 'string');
$this->addType('optionId', 'int');
}
/**
* @return array
*/
public function asArray(): array {
return [
// The ids are not needed on the API level but only internally for optimising database queries
// 'id' => $this->getId(),
// 'pollId' => $this->getPollId(),
// 'roomId' => $this->getRoomId(),
'actorType' => $this->getActorType(),
'actorId' => $this->getActorId(),
'actorDisplayName' => $this->getDisplayName(),
'optionId' => $this->getOptionId(),
];
}
}

91
lib/Model/VoteMapper.php

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Talk\Model;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
class VoteMapper extends QBMapper {
/**
* @param IDBConnection $db
*/
public function __construct(IDBConnection $db) {
parent::__construct($db, 'talk_poll_votes', Vote::class);
}
/**
* @param int $pollId
* @return Vote[]
*/
public function findByPollId(int $pollId): array {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from($this->getTableName())
->where($query->expr()->eq('poll_id', $query->createNamedParameter($pollId)));
return $this->findEntities($query);
}
/**
* @param int $pollId
* @param string $actorType
* @param string $actorId
* @return Vote[]
*/
public function findByPollIdForActor(int $pollId, string $actorType, string $actorId): array {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from($this->getTableName())
->where($query->expr()->eq('poll_id', $query->createNamedParameter($pollId)))
->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter($actorType)))
->andWhere($query->expr()->eq('actor_id', $query->createNamedParameter($actorId)));
return $this->findEntities($query);
}
public function deleteByRoomId(int $roomId): void {
$query = $this->db->getQueryBuilder();
$query->delete($this->getTableName())
->where($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT)));
$query->executeStatement();
}
public function deleteVotesByActor(int $pollId, string $actorType, string $actorId): void {
$query = $this->db->getQueryBuilder();
$query->delete($this->getTableName())
->where($query->expr()->eq('poll_id', $query->createNamedParameter($pollId)))
->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter($actorType)))
->andWhere($query->expr()->eq('actor_id', $query->createNamedParameter($actorId)));
$query->executeStatement();
}
}

281
lib/Service/PollService.php

@ -0,0 +1,281 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Talk\Service;
use OCA\Talk\Exceptions\WrongPermissionsException;
use OCA\Talk\Model\Poll;
use OCA\Talk\Model\PollMapper;
use OCA\Talk\Model\Vote;
use OCA\Talk\Model\VoteMapper;
use OCA\Talk\Participant;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
class PollService {
protected IDBConnection $connection;
protected PollMapper $pollMapper;
protected VoteMapper $voteMapper;
public function __construct(IDBConnection $connection,
PollMapper $pollMapper,
VoteMapper $voteMapper) {
$this->connection = $connection;
$this->pollMapper = $pollMapper;
$this->voteMapper = $voteMapper;
}
public function createPoll(int $roomId, string $actorType, string $actorId, string $displayName, string $question, array $options, int $resultMode, int $maxVotes): Poll {
$poll = new Poll();
$poll->setRoomId($roomId);
$poll->setActorType($actorType);
$poll->setActorId($actorId);
$poll->setDisplayName($displayName);
$poll->setQuestion($question);
$poll->setOptions(json_encode($options));
$poll->setVotes(json_encode([]));
$poll->setResultMode($resultMode);
$poll->setMaxVotes($maxVotes);
$this->pollMapper->insert($poll);
return $poll;
}
/**
* @param int $roomId
* @param int $pollId
* @return Poll
* @throws DoesNotExistException
*/
public function getPoll(int $roomId, int $pollId): Poll {
$poll = $this->pollMapper->getByPollId($pollId);
if ($poll->getRoomId() !== $roomId) {
throw new DoesNotExistException('Room id mismatch');
}
return $poll;
}
/**
* @param Participant $participant
* @param Poll $poll
* @throws WrongPermissionsException
* @throws Exception
*/
public function updatePoll(Participant $participant, Poll $poll): void {
if (!$participant->hasModeratorPermissions()
&& ($poll->getActorType() !== $participant->getAttendee()->getActorType()
|| $poll->getActorId() !== $participant->getAttendee()->getActorId())) {
// Only moderators and the author of the poll can update it
throw new WrongPermissionsException();
}
$this->pollMapper->update($poll);
}
/**
* @param Participant $participant
* @param Poll $poll
* @return Vote[]
*/
public function getVotesForActor(Participant $participant, Poll $poll): array {
return $this->voteMapper->findByPollIdForActor(
$poll->getId(),
$participant->getAttendee()->getActorType(),
$participant->getAttendee()->getActorId()
);
}
/**
* @param Poll $poll
* @return Vote[]
*/
public function getVotes(Poll $poll): array {
return $this->voteMapper->findByPollId($poll->getId());
}
/**
* @param Participant $participant
* @param Poll $poll
* @param int[] $optionIds Options the user voted for
* @return Vote[]
* @throws \RuntimeException
*/
public function votePoll(Participant $participant, Poll $poll, array $optionIds): array {
$numVotes = count($optionIds);
if ($numVotes !== count(array_unique($optionIds))) {
throw new \UnexpectedValueException();
}
if ($poll->getMaxVotes() !== Poll::MAX_VOTES_UNLIMITED
&& $poll->getMaxVotes() < $numVotes) {
throw new \OverflowException();
}
$maxOptionId = max(array_keys(json_decode($poll->getOptions(), true, 512, JSON_THROW_ON_ERROR)));
$maxVotedId = max($optionIds);
$minVotedId = min($optionIds);
if ($minVotedId < 0 || $maxVotedId > $maxOptionId) {
throw new \RangeException();
}
$votes = [];
$result = json_decode($poll->getVotes(), true);
$previousVotes = $this->voteMapper->findByPollIdForActor(
$poll->getId(),
$participant->getAttendee()->getActorType(),
$participant->getAttendee()->getActorId()
);
$numVoters = $poll->getNumVoters();
if ($previousVotes && $numVoters > 0) {
$numVoters--;
}
foreach ($previousVotes as $vote) {
$result[$vote->getOptionId()] ??= 1;
$result[$vote->getOptionId()] -= 1;
}
$this->connection->beginTransaction();
try {
$this->voteMapper->deleteVotesByActor(
$poll->getId(),
$participant->getAttendee()->getActorType(),
$participant->getAttendee()->getActorId()
);
if (!empty($optionIds)) {
$numVoters++;
}
foreach ($optionIds as $optionId) {
$vote = new Vote();
$vote->setPollId($poll->getId());
$vote->setRoomId($poll->getRoomId());
$vote->setActorType($participant->getAttendee()->getActorType());
$vote->setActorId($participant->getAttendee()->getActorId());
$vote->setDisplayName($participant->getAttendee()->getDisplayName());
$vote->setOptionId($optionId);
$this->voteMapper->insert($vote);
$result[$optionId] ??= 0;
$result[$optionId] += 1;
$votes[] = $vote;
}
} catch (\Exception $e) {
$this->connection->rollBack();
throw $e;
}
$this->connection->commit();
$this->updateResultCache($poll->getId());
$result = array_filter($result);
$poll->setVotes(json_encode($result));
$poll->setNumVoters($numVoters);
return $votes;
}
public function updateResultCache(int $pollId): void {
$resultQuery = $this->connection->getQueryBuilder();
$resultQuery->selectAlias(
$resultQuery->func()->concat(
$resultQuery->expr()->literal('"'),
'option_id',
$resultQuery->expr()->literal('":'),
$resultQuery->func()->count('id')
),
'colonseparatedvalue'
)
->from('talk_poll_votes')
->where($resultQuery->expr()->eq('poll_id', $resultQuery->createNamedParameter($pollId)))
->groupBy('option_id')
->orderBy('option_id', 'ASC');
$jsonQuery = $this->connection->getQueryBuilder();
$jsonQuery
->selectAlias(
$jsonQuery->func()->concat(
$jsonQuery->expr()->literal('{'),
$jsonQuery->func()->groupConcat('colonseparatedvalue'),
$jsonQuery->expr()->literal('}')
),
'json'
)
->from($jsonQuery->createFunction('(' . $resultQuery->getSQL() . ')'), 'json');
$subQuery = $this->connection->getQueryBuilder();
$subQuery->select('actor_type', 'actor_id')
->from('talk_poll_votes')
->where($subQuery->expr()->eq('poll_id', $subQuery->createNamedParameter($pollId)))
->groupBy('actor_type', 'actor_id');
$votersQuery = $this->connection->getQueryBuilder();
$votersQuery->select($votersQuery->func()->count('*'))
->from($votersQuery->createFunction('(' . $subQuery->getSQL() . ')'), 'voters');
$update = $this->connection->getQueryBuilder();
$update->update('talk_polls')
->set('votes', $jsonQuery->createFunction('(' . $jsonQuery->getSQL() . ')'))
->set('num_voters', $jsonQuery->createFunction('(' . $votersQuery->getSQL() . ')'))
->where($update->expr()->eq('id', $update->createNamedParameter($pollId, IQueryBuilder::PARAM_INT)));
$this->connection->beginTransaction();
try {
$update->executeStatement();
} catch (\Exception $e) {
$this->connection->rollBack();
throw $e;
}
$this->connection->commit();
}
public function deleteByRoomId(int $roomId): void {
$this->voteMapper->deleteByRoomId($roomId);
$this->pollMapper->deleteByRoomId($roomId);
}
public function updateDisplayNameForActor(string $actorType, string $actorId, string $displayName): void {
$update = $this->connection->getQueryBuilder();
$update->update('talk_polls')
->set('display_name', $update->createNamedParameter($displayName))
->where($update->expr()->eq('actor_type', $update->createNamedParameter($actorType)))
->andWhere($update->expr()->eq('actor_id', $update->createNamedParameter($actorId)));
$update->executeStatement();
$update = $this->connection->getQueryBuilder();
$update->update('talk_poll_votes')
->set('display_name', $update->createNamedParameter($displayName))
->where($update->expr()->eq('actor_type', $update->createNamedParameter($actorType)))
->andWhere($update->expr()->eq('actor_id', $update->createNamedParameter($actorId)));
$update->executeStatement();
}
}

1
mkdocs.yml

@ -27,6 +27,7 @@ nav:
- 'Call management': 'call.md'
- 'Chat management': 'chat.md'
- 'Reaction management': 'reaction.md'
- 'Poll management': 'poll.md'
- 'Webinar management': 'webinar.md'
- 'Settings': 'settings.md'
- 'Integration by other apps': 'integration.md'

181
tests/integration/features/bootstrap/FeatureContext.php

@ -56,6 +56,8 @@ class FeatureContext implements Context, SnippetAcceptingContext {
protected static $remoteToInviteId;
/** @var string[] */
protected static $inviteIdToRemote;
/** @var int[] */
protected static $questionToPollId;
protected static $permissionsMap = [
@ -148,6 +150,7 @@ class FeatureContext implements Context, SnippetAcceptingContext {
self::$userToAttendeeId = [];
self::$textToMessageId = [];
self::$messageIdToText = [];
self::$questionToPollId = [];
$this->createdUsers = [];
$this->createdGroups = [];
@ -1511,6 +1514,171 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
}
/**
* @Then /^user "([^"]*)" creates a poll in room "([^"]*)" with (\d+)(?: \((v1)\))?$/
*
* @param string $user
* @param string $identifier
* @param string $statusCode
* @param string $apiVersion
*/
public function createPoll(string $user, string $identifier, string $statusCode, string $apiVersion = 'v1', TableNode $formData = null): void {
$data = $formData->getRowsHash();
$data['options'] = json_decode($data['options'], true);
if ($data['resultMode'] === 'public') {
$data['resultMode'] = 0;
} elseif ($data['resultMode'] === 'hidden') {
$data['resultMode'] = 1;
} else {
throw new \Exception('Invalid result mode');
}
if ($data['maxVotes'] === 'unlimited') {
$data['maxVotes'] = 0;
}
$this->setCurrentUser($user);
$this->sendRequest(
'POST', '/apps/spreed/api/' . $apiVersion . '/poll/' . self::$identifierToToken[$identifier],
$data
);
$this->assertStatusCode($this->response, $statusCode);
if ($statusCode !== '201') {
return;
}
$response = $this->getDataFromResponse($this->response);
if (isset($response['id'])) {
self::$questionToPollId[$data['question']] = $response['id'];
}
}
/**
* @Then /^user "([^"]*)" sees poll "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v1)\))?$/
*
* @param string $user
* @param string $question
* @param string $identifier
* @param string $statusCode
* @param string $apiVersion
* @param ?TableNode $formData
*/
public function userSeesPollInRoom(string $user, string $question, string $identifier, string $statusCode, string $apiVersion = 'v1', TableNode $formData = null): void {
$this->setCurrentUser($user);
$this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/poll/' . self::$identifierToToken[$identifier] . '/' . self::$questionToPollId[$question]);
$this->assertStatusCode($this->response, $statusCode);
$expected = $this->preparePollExpectedData($formData->getRowsHash());
$response = $this->getDataFromResponse($this->response);
$this->assertPollEquals($expected, $response);
}
/**
* @Then /^user "([^"]*)" closes poll "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v1)\))?$/
*
* @param string $user
* @param string $question
* @param string $identifier
* @param string $statusCode
* @param string $apiVersion
* @param ?TableNode $formData
*/
public function userClosesPollInRoom(string $user, string $question, string $identifier, string $statusCode, string $apiVersion = 'v1', TableNode $formData = null): void {
$this->setCurrentUser($user);
$this->sendRequest('DELETE', '/apps/spreed/api/' . $apiVersion . '/poll/' . self::$identifierToToken[$identifier] . '/' . self::$questionToPollId[$question]);
$this->assertStatusCode($this->response, $statusCode);
if ($statusCode !== '200') {
return;
}
$expected = $this->preparePollExpectedData($formData->getRowsHash());
$response = $this->getDataFromResponse($this->response);
$this->assertPollEquals($expected, $response);
}
/**
* @Then /^user "([^"]*)" votes for options "([^"]*)" on poll "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v1)\))?$/
*
* @param string $user
* @param string $options
* @param string $question
* @param string $identifier
* @param string $statusCode
* @param string $apiVersion
* @param ?TableNode $formData
*/
public function userVotesPollInRoom(string $user, string $options, string $question, string $identifier, string $statusCode, string $apiVersion = 'v1', TableNode $formData = null): void {
$data = [
'optionIds' => json_decode($options, true),
];
$this->setCurrentUser($user);
$this->sendRequest(
'POST', '/apps/spreed/api/' . $apiVersion . '/poll/' . self::$identifierToToken[$identifier] . '/' . self::$questionToPollId[$question],
$data
);
$this->assertStatusCode($this->response, $statusCode);
if ($statusCode !== '200' && $statusCode !== '201') {
return;
}
$expected = $this->preparePollExpectedData($formData->getRowsHash());
$response = $this->getDataFromResponse($this->response);
$this->assertPollEquals($expected, $response);
}
protected function assertPollEquals(array $expected, array $response): void {
if (isset($expected['details'])) {
$response['details'] = array_map(static function (array $detail): array {
unset($detail['id']);
return $detail;
}, $response['details']);
}
Assert::assertEquals($expected, $response);
}
protected function preparePollExpectedData(array $expected): array {
if ($expected['resultMode'] === 'public') {
$expected['resultMode'] = 0;
} elseif ($expected['resultMode'] === 'hidden') {
$expected['resultMode'] = 1;
}
if ($expected['maxVotes'] === 'unlimited') {
$expected['maxVotes'] = 0;
}
if ($expected['status'] === 'open') {
$expected['status'] = 0;
} elseif ($expected['status'] === 'closed') {
$expected['status'] = 1;
}
if ($expected['votedSelf'] === 'not voted') {
$expected['votedSelf'] = [];
} else {
$expected['votedSelf'] = json_decode($expected['votedSelf'], true);
}
if (isset($expected['votes'])) {
$expected['votes'] = json_decode($expected['votes'], true);
}
if (isset($expected['details'])) {
$expected['details'] = json_decode($expected['details'], true);
}
$expected['numVoters'] = (int) $expected['numVoters'];
$expected['options'] = json_decode($expected['options'], true);
$result = preg_match('/POLL_ID\(([^)]+)\)/', $expected['id'], $matches);
if ($result) {
$expected['id'] = self::$questionToPollId[$matches[1]];
}
return $expected;
}
/**
* @Then /^user "([^"]*)" deletes message "([^"]*)" from room "([^"]*)" with (\d+)(?: \((v1)\))?$/
*
@ -1741,14 +1909,21 @@ class FeatureContext implements Context, SnippetAcceptingContext {
$includeReactions = in_array('reactions', $formData->getRow(0), true);
$includeReactionsSelf = in_array('reactionsSelf', $formData->getRow(0), true);
$count = count($formData->getHash());
$expected = $formData->getHash();
$count = count($expected);
Assert::assertCount($count, $messages, 'Message count does not match');
for ($i = 0; $i < $count; $i++) {
if ($formData->getHash()[$i]['messageParameters'] === '"IGNORE"') {
if ($expected[$i]['messageParameters'] === '"IGNORE"') {
$messages[$i]['messageParameters'] = 'IGNORE';
}
$result = preg_match('/POLL_ID\(([^)]+)\)/', $expected[$i]['messageParameters'], $matches);
if ($result) {
$expected[$i]['messageParameters'] = str_replace($matches[0], self::$questionToPollId[$matches[1]], $expected[$i]['messageParameters']);
}
}
Assert::assertEquals($formData->getHash(), array_map(function ($message) use ($includeParents, $includeReferenceId, $includeReactions, $includeReactionsSelf) {
Assert::assertEquals($expected, array_map(function ($message) use ($includeParents, $includeReferenceId, $includeReactions, $includeReactionsSelf) {
$data = [
'room' => self::$tokenToIdentifier[$message['token']],
'actorType' => $message['actorType'],

525
tests/integration/features/chat/poll.feature

@ -0,0 +1,525 @@
Feature: chat/poll
Background:
Given user "participant1" exists
Given user "participant2" exists
Scenario: Create a public poll without max votes limit
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" adds user "participant2" to room "room" with 200 (v4)
When user "participant1" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | public |
| maxVotes | unlimited |
Then user "participant1" sees the following messages in room "room" with 200
| room | actorType | actorId | actorDisplayName | message | messageParameters |
| room | users | participant1 | participant1-displayname | {object} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"object":{"type":"talk-poll","id":POLL_ID(What is the question?),"name":"What is the question?"}} |
Then user "participant1" sees poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | [] |
| numVoters | 0 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | not voted |
Then user "participant1" votes for options "[1]" on poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"1":1} |
| numVoters | 1 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | [1] |
Then user "participant1" sees poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"1":1} |
| numVoters | 1 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | [1] |
Then user "participant2" sees poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | [] |
| numVoters | 0 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | not voted |
Then user "participant1" closes poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"1":1} |
| numVoters | 1 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | closed |
| votedSelf | [1] |
| details | [{"actorType":"users","actorId":"participant1","actorDisplayName":"participant1-displayname","optionId":1}] |
Then user "participant2" sees poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"1":1} |
| numVoters | 1 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | closed |
| votedSelf | not voted |
| details | [{"actorType":"users","actorId":"participant1","actorDisplayName":"participant1-displayname","optionId":1}] |
Scenario: Participants can update their votes but only while open
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | public |
| maxVotes | unlimited |
Then user "participant1" votes for options "[0]" on poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"0":1} |
| numVoters | 1 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | [0] |
Then user "participant1" votes for options "[1]" on poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"1":1} |
| numVoters | 1 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | [1] |
Then user "participant1" closes poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"1":1} |
| numVoters | 1 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | closed |
| votedSelf | [1] |
| details | [{"actorType":"users","actorId":"participant1","actorDisplayName":"participant1-displayname","optionId":1}] |
Then user "participant1" votes for options "[0]" on poll "What is the question?" in room "room" with 400
Scenario: Participants can only vote for valid options
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | public |
| maxVotes | unlimited |
Then user "participant1" votes for options "[-1]" on poll "What is the question?" in room "room" with 400
Then user "participant1" votes for options "[2]" on poll "What is the question?" in room "room" with 400
Scenario: Participants can not exceed the maxVotes
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | public |
| maxVotes | 1 |
Then user "participant1" votes for options "[0,1]" on poll "What is the question?" in room "room" with 400
Scenario: Participants can vote for multiple options
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | public |
| maxVotes | unlimited |
Then user "participant1" votes for options "[0,1]" on poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"0":1,"1":1} |
| numVoters | 1 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | [0,1] |
Scenario: Participants can not vote for the same option multiple times
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | public |
| maxVotes | unlimited |
Then user "participant1" votes for options "[1,1]" on poll "What is the question?" in room "room" with 400
Scenario: Non-moderators can also create polls and close it themselves
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" adds user "participant2" to room "room" with 200 (v4)
When user "participant2" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | public |
| maxVotes | unlimited |
Then user "participant1" sees the following messages in room "room" with 200
| room | actorType | actorId | actorDisplayName | message | messageParameters |
| room | users | participant2 | participant2-displayname | {object} | {"actor":{"type":"user","id":"participant2","name":"participant2-displayname"},"object":{"type":"talk-poll","id":POLL_ID(What is the question?),"name":"What is the question?"}} |
Then user "participant2" closes poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | [] |
| numVoters | 0 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant2 |
| actorDisplayName | participant2-displayname |
| status | closed |
| votedSelf | not voted |
| details | {} |
Scenario: Non-moderators can note create polls without chat permission
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" adds user "participant2" to room "room" with 200 (v4)
# Removing chat permission only
Then user "participant1" sets permissions for "participant2" in room "room" to "CSJLAVP" with 200 (v4)
When user "participant2" creates a poll in room "room" with 403
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | public |
| maxVotes | unlimited |
Scenario: Moderators can close polls of others
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" adds user "participant2" to room "room" with 200 (v4)
When user "participant2" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | public |
| maxVotes | unlimited |
Then user "participant1" closes poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | [] |
| numVoters | 0 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant2 |
| actorDisplayName | participant2-displayname |
| status | closed |
| votedSelf | not voted |
| details | {} |
Scenario: There are system messages for opening, voting and closing on public polls
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" adds user "participant2" to room "room" with 200 (v4)
When user "participant1" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | public |
| maxVotes | unlimited |
Then user "participant1" votes for options "[0]" on poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"0":1} |
| numVoters | 1 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | [0] |
Then user "participant2" votes for options "[1]" on poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"0":1,"1":1} |
| numVoters | 2 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | [1] |
Then user "participant1" closes poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"0":1,"1":1} |
| numVoters | 2 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | closed |
| votedSelf | [0] |
| details | [{"actorType":"users","actorId":"participant1","actorDisplayName":"participant1-displayname","optionId":0},{"actorType":"users","actorId":"participant2","actorDisplayName":"participant2-displayname","optionId":1}] |
Then user "participant1" sees the following system messages in room "room" with 200 (v1)
| room | actorType | actorId | actorDisplayName | systemMessage |
| room | users | participant1 | participant1-displayname | poll_closed |
| room | users | participant2 | participant2-displayname | poll_voted |
| room | users | participant1 | participant1-displayname | poll_voted |
| room | users | participant1 | participant1-displayname | user_added |
| room | users | participant1 | participant1-displayname | conversation_created |
Then user "participant1" sees the following messages in room "room" with 200 (v1)
| room | actorType | actorId | actorDisplayName | message | messageParameters |
| room | users | participant1 | participant1-displayname | {object} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"object":{"type":"talk-poll","id":POLL_ID(What is the question?),"name":"What is the question?"}} |
Scenario: There are only system messages for opening and closing on hidden polls
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" adds user "participant2" to room "room" with 200 (v4)
When user "participant1" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | hidden |
| maxVotes | unlimited |
Then user "participant1" votes for options "[0]" on poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | [] |
| numVoters | 0 |
| resultMode | hidden |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | [0] |
Then user "participant2" votes for options "[1]" on poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | [] |
| numVoters | 0 |
| resultMode | hidden |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | [1] |
Then user "participant1" closes poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"0":1,"1":1} |
| numVoters | 2 |
| resultMode | hidden |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | closed |
| votedSelf | [0] |
Then user "participant1" sees the following system messages in room "room" with 200 (v1)
| room | actorType | actorId | actorDisplayName | systemMessage |
| room | users | participant1 | participant1-displayname | poll_closed |
| room | users | participant1 | participant1-displayname | user_added |
| room | users | participant1 | participant1-displayname | conversation_created |
Then user "participant1" sees the following messages in room "room" with 200 (v1)
| room | actorType | actorId | actorDisplayName | message | messageParameters |
| room | users | participant1 | participant1-displayname | {object} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"object":{"type":"talk-poll","id":POLL_ID(What is the question?),"name":"What is the question?"}} |
Scenario: Non-moderators can not close polls of others
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" adds user "participant2" to room "room" with 200 (v4)
When user "participant1" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | public |
| maxVotes | unlimited |
Then user "participant2" closes poll "What is the question?" in room "room" with 403
Scenario: Votes and details are not accessible in hidden result mode
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" adds user "participant2" to room "room" with 200 (v4)
When user "participant1" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | hidden |
| maxVotes | unlimited |
Then user "participant1" sees the following messages in room "room" with 200
| room | actorType | actorId | actorDisplayName | message | messageParameters |
| room | users | participant1 | participant1-displayname | {object} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"object":{"type":"talk-poll","id":POLL_ID(What is the question?),"name":"What is the question?"}} |
Then user "participant2" votes for options "[1]" on poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | [] |
| numVoters | 0 |
| resultMode | hidden |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | [1] |
Then user "participant1" sees poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | [] |
| numVoters | 0 |
| resultMode | hidden |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | not voted |
Then user "participant2" sees poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | [] |
| numVoters | 0 |
| resultMode | hidden |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | [1] |
Then user "participant1" closes poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"1":1} |
| numVoters | 1 |
| resultMode | hidden |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | closed |
| votedSelf | not voted |
Scenario: Number of voters and votes are restricted to the very same poll
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" adds user "participant2" to room "room" with 200 (v4)
When user "participant1" creates a poll in room "room" with 201
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| resultMode | public |
| maxVotes | unlimited |
Then user "participant1" votes for options "[0]" on poll "What is the question?" in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question? |
| options | ["Where are you?","How much is the fish?"] |
| votes | {"0":1} |
| numVoters | 1 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant1 |
| actorDisplayName | participant1-displayname |
| status | open |
| votedSelf | [0] |
When user "participant2" creates a poll in room "room" with 201
| question | Another one ... |
| options | ["... bites the dust!","... bites de_dust!"] |
| resultMode | public |
| maxVotes | unlimited |
Then user "participant2" votes for options "[1]" on poll "Another one ..." in room "room" with 200
| id | POLL_ID(Another one ...) |
| question | Another one ... |
| options | ["... bites the dust!","... bites de_dust!"] |
| votes | {"1":1} |
| numVoters | 1 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant2 |
| actorDisplayName | participant2-displayname |
| status | open |
| votedSelf | [1] |
Then user "participant2" sees poll "Another one ..." in room "room" with 200
| id | POLL_ID(Another one ...) |
| question | Another one ... |
| options | ["... bites the dust!","... bites de_dust!"] |
| votes | {"1":1} |
| numVoters | 1 |
| resultMode | public |
| maxVotes | unlimited |
| actorType | users |
| actorId | participant2 |
| actorDisplayName | participant2-displayname |
| status | open |
| votedSelf | [1] |

6
tests/integration/spreedcheats/lib/Controller/ApiController.php

@ -74,6 +74,12 @@ class ApiController extends OCSController {
$delete = $this->db->getQueryBuilder();
$delete->delete('talk_rooms')->executeStatement();
$delete = $this->db->getQueryBuilder();
$delete->delete('talk_polls')->executeStatement();
$delete = $this->db->getQueryBuilder();
$delete->delete('talk_poll_votes')->executeStatement();
$delete = $this->db->getQueryBuilder();
$delete->delete('talk_sessions')->executeStatement();

7
tests/php/Chat/ChatManagerTest.php

@ -33,6 +33,7 @@ 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\Share\RoomShareProvider;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
@ -64,6 +65,8 @@ class ChatManagerTest extends TestCase {
protected $shareProvider;
/** @var ParticipantService|MockObject */
protected $participantService;
/** @var PollService|MockObject */
protected $pollService;
/** @var Notifier|MockObject */
protected $notifier;
/** @var ITimeFactory|MockObject */
@ -81,6 +84,7 @@ class ChatManagerTest extends TestCase {
$this->shareManager = $this->createMock(IManager::class);
$this->shareProvider = $this->createMock(RoomShareProvider::class);
$this->participantService = $this->createMock(ParticipantService::class);
$this->pollService = $this->createMock(PollService::class);
$this->notifier = $this->createMock(Notifier::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
@ -94,6 +98,7 @@ class ChatManagerTest extends TestCase {
$this->shareManager,
$this->shareProvider,
$this->participantService,
$this->pollService,
$this->notifier,
$cacheFactory,
$this->timeFactory,
@ -118,6 +123,7 @@ class ChatManagerTest extends TestCase {
$this->shareManager,
$this->shareProvider,
$this->participantService,
$this->pollService,
$this->notifier,
$cacheFactory,
$this->timeFactory,
@ -135,6 +141,7 @@ class ChatManagerTest extends TestCase {
$this->shareManager,
$this->shareProvider,
$this->participantService,
$this->pollService,
$this->notifier,
$cacheFactory,
$this->timeFactory,

Loading…
Cancel
Save