Improved Messages UI and shrinking includes/user_messages.php

This commit is contained in:
frischler 2022-04-09 19:08:53 +02:00 committed by Igor Scheller
parent 7d51953b84
commit 2c0d516578
16 changed files with 1125 additions and 177 deletions

View File

@ -50,6 +50,13 @@ $route->post('/questions', 'QuestionsController@delete');
$route->get('/questions/new', 'QuestionsController@add');
$route->post('/questions/new', 'QuestionsController@save');
// Messages
$route->get('/messages', 'MessagesController@index');
$route->post('/messages', 'MessagesController@toConversation');
$route->get('/messages/{user_id:\d+}', 'MessagesController@conversation');
$route->post('/messages/{user_id:\d+}', 'MessagesController@send');
$route->post('/messages/{user_id:\d+}/{msg_id:\d+}', 'MessagesController@delete');
// API
$route->get('/api[/{resource:.+}]', 'ApiController@index');

View File

@ -1,174 +1,14 @@
<?php
use Engelsystem\Models\Message;
use Engelsystem\Models\User\User;
/**
* @return string
*/
function messages_title()
{
return __('Messages');
}
use Engelsystem\Controllers\MessagesController;
/**
* @return string
*/
function user_unread_messages()
{
$user = auth()->user();
$count = app()->make(MessagesController::class)
->numberOfUnreadMessages();
if ($user) {
$new_messages = $user->messagesReceived()
->where('read', false)
->count();
if ($new_messages > 0) {
return ' <span class="badge bg-danger">' . $new_messages . '</span>';
}
}
return '';
}
/**
* @return string
*/
function user_messages()
{
$user = auth()->user();
$request = request();
if (!$request->has('action')) {
/** @var User[] $users */
$users = User::query()
->where('user_id', '!=', $user->id)
->leftJoin('users_personal_data', 'users.id', '=', 'users_personal_data.user_id')
->orderBy('name')
->get(['id', 'name', 'pronoun']);
$to_select_data = [
'' => __('Select recipient...')
];
foreach ($users as $u) {
$pronoun = ((config('enable_pronoun') && $u->pronoun) ? ' (' . htmlspecialchars($u->pronoun) . ')' : '');
$to_select_data[$u->id] = $u->name . $pronoun;
}
$to_select = html_select_key('to', 'to', $to_select_data, '');
$messages = $user->messages;
$messages_table = [
[
'news' => '',
'timestamp' => date(__('Y-m-d H:i')),
'from' => User_Nick_render($user),
'to' => $to_select,
'text' => form_textarea('text', '', ''),
'actions' => form_submit('submit', __('Send'))
]
];
foreach ($messages as $message) {
$sender_user_source = $message->user;
$receiver_user_source = $message->receiver;
$messages_table_entry = [
'new' => !$message->read ? icon('envelope') : '',
'timestamp' => $message->created_at->format(__('Y-m-d H:i')),
'from' => User_Nick_render($sender_user_source),
'to' => User_Nick_render($receiver_user_source),
'text' => nl2br(htmlspecialchars($message->text))
];
if ($message->receiver_id == $user->id) {
if (!$message->read) {
$messages_table_entry['actions'] = button(
page_link_to('user_messages', ['action' => 'read', 'id' => $message->id]),
__('mark as read'),
'btn-sm'
);
}
} else {
$messages_table_entry['actions'] = button(
page_link_to('user_messages', ['action' => 'delete', 'id' => $message->id]),
__('delete message'),
'btn-sm'
);
}
$messages_table[] = $messages_table_entry;
}
return page_with_title(messages_title(), [
msg(),
sprintf(__('Hello %s, here can you leave messages for other angels'), User_Nick_render($user)),
form([
table([
'new' => __('New'),
'timestamp' => __('Date'),
'from' => __('Transmitted'),
'to' => __('Recipient'),
'text' => __('Message'),
'actions' => ''
], $messages_table)
], page_link_to('user_messages', ['action' => 'send']))
]);
} else {
switch ($request->input('action')) {
case 'read':
if ($request->has('id') && preg_match('/^\d{1,11}$/', $request->input('id'))) {
$message_id = $request->input('id');
} else {
return error(__('Incomplete call, missing Message ID.'), true);
}
$message = Message::find($message_id);
if ($message !== null && $message->receiver_id == $user->id) {
$message->read = true;
$message->save();
throw_redirect(page_link_to('user_messages'));
} else {
return error(__('No Message found.'), true);
}
break;
case 'delete':
if ($request->has('id') && preg_match('/^\d{1,11}$/', $request->input('id'))) {
$message_id = $request->input('id');
} else {
return error(__('Incomplete call, missing Message ID.'), true);
}
$message = Message::find($message_id);
if ($message !== null && $message->user_id == $user->id) {
$message->delete();
throw_redirect(page_link_to('user_messages'));
} else {
return error(__('No Message found.'), true);
}
break;
case 'send':
$receiver = User::find($request->input('to'));
$text = $request->input('text');
if ($receiver !== null && !empty($text)) {
Message::create([
'user_id' => $user->id,
'receiver_id' => $request->input('to'),
'text' => $request->input('text')
]);
throw_redirect(page_link_to('user_messages'));
} else {
return error(__('Transmitting was terminated with an Error.'), true);
}
break;
default:
return error(__('Wrong action.'), true);
}
}
return '';
return $count > 0 ? ' <span class="badge bg-danger">' . $count . '</span>' : '';
}

View File

@ -348,3 +348,14 @@ code {
content: "";
}
}
.conversation {
height: 60vh;
overflow-x: hidden;
overflow-y: auto
}
.message {
max-width: 75%;
display: inline-block;
}

View File

@ -154,3 +154,6 @@ msgstr "Eine Beschreibung findest du unter %2$s"
msgid "user.edit.success"
msgstr "Benutzer erfolgreich bearbeitet."
msgid "messages.delete.success"
msgstr "Nachricht erfolgreich gelöscht."

View File

@ -1308,10 +1308,6 @@ msgstr "Zu löschende Schichten"
msgid "It's done!"
msgstr "Erledigt!"
#: includes/pages/user_messages.php:121
msgid "Message"
msgstr "Nachricht"
#: includes/pages/admin_rooms.php:202
#: includes/view/User_view.php:129
msgid "Delete"
@ -1685,10 +1681,6 @@ msgstr "Nachname"
msgid "Entry required!"
msgstr "Pflichtfeld!"
#: includes/pages/user_messages.php:11
msgid "Messages"
msgstr "Nachrichten"
#: includes/pages/user_messages.php:49
msgid "Select recipient..."
msgstr "Empfänger auswählen..."
@ -3038,3 +3030,18 @@ msgstr "Angekommen"
msgid "user.got_shirt"
msgstr "Shirt bekommen"
msgid "messages.title"
msgstr "Nachrichten"
msgid "messages.choose.an.angel"
msgstr "Wähle einen Engel"
msgid "messages.to.conversation"
msgstr "Zur Konversation"
msgid "angel"
msgstr "Engel"
msgid "message"
msgstr "Nachricht"

View File

@ -152,3 +152,6 @@ msgstr "You can find a description at %2$s"
msgid "user.edit.success"
msgstr "User edited successfully."
msgid "messages.delete.success"
msgstr "Message successfully deleted."

View File

@ -300,3 +300,18 @@ msgstr "Arrived"
msgid "user.got_shirt"
msgstr "Got shirt"
msgid "messages.title"
msgstr "Messages"
msgid "messages.choose.an.angel"
msgstr "Choose an Angel"
msgid "messages.to.conversation"
msgstr "To Conversation"
msgid "angel"
msgstr "Angel"
msgid "message"
msgstr "Message"

View File

@ -44,7 +44,7 @@
{% endif %}
{% if is_user() and has_permission_to('user_messages') %}
{{ _self.toolbar_item(menuUserMessages(), url('user-messages'), 'user-messages', 'envelope') }}
{{ _self.toolbar_item(menuUserMessages(), url('messages'), 'messages', 'envelope') }}
{% endif %}
{{ menuUserHints() }}

View File

@ -0,0 +1,66 @@
{% extends "layouts/app.twig" %}
{% import 'macros/base.twig' as m %}
{% import 'macros/form.twig' as f %}
{% block title %}{{ __('messages.title') }}: {{ other_user.nameWithPronoun() }}{% endblock %}
{% block content %}
<div class="container">
<div class="page-header">
<h1>
{{ __('messages.title') }}: <span class="icon-icon_angel"></span> {{ other_user.nameWithPronoun() }}
</h1>
</div>
<div class="row">
<div class="col-12 col-lg-8 offset-lg-2">
<div class="row conversation">
{% for msg in messages %}
{% if msg.user_id == other_user.id %}
<div class="col-12">
<div>
<div class="message alert alert-secondary position-relative">
<div>{{ msg.text | nl2br }}</div>
<div class="text-end">
<small class="opacity-75">{{ msg.created_at }}</small>
</div>
{% if msg.read == false %}
<span class="position-absolute top-0 start-100 translate-middle-x p-2 bg-danger rounded-circle">
<span class="visually-hidden">New alerts</span>
</span>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="d-flex justify-content-end">
<div class="message alert alert-primary">
<div>{{ msg.text | nl2br }}</div>
<div class="text-end">
<form action="{{ url('/messages/' ~ other_user.id ~ '/' ~ msg.id) }}"
enctype="multipart/form-data" method="post">
{{ csrf() }}
<small class="opacity-75">{{ msg.created_at }}</small>
{{ f.submit(m.icon('trash'), {'btn_type': 'primary', 'size': 'sm'}) }}
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
<form action="" enctype="multipart/form-data" method="post">
{{ csrf() }}
<div class="input-group">
<textarea class="form-control" id="text" name="text" required="" rows="1"></textarea>
{{ f.submit(m.icon('send-fill')) }}
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,64 @@
{% extends "layouts/app.twig" %}
{% import 'macros/base.twig' as m %}
{% import 'macros/form.twig' as f %}
{% block title %}{{ __('messages.title') }}{% endblock %}
{% block content %}
<div class="container">
<div class="page-header">
<h1>
{{ block('title') }}
</h1>
</div>
<form action="" enctype="multipart/form-data" method="post">
{{ csrf() }}
<div class="row gx-2 mb-3">
<div class="col-auto">
<select id="user_id" name="user_id" class="form-control pe-5" required>
<option value="">{{ __('messages.choose.an.angel') }}</option>
{% for value,decription in users -%}
<option value="{{ value }}">{{ decription }}</option>
{% endfor %}
</select>
</div>
<div class="col">
{{ f.submit(__('messages.to.conversation'), {'btn_type': 'secondary'}) }}
</div>
</div>
</form>
<table class="table table-striped data">
<thead>
<tr>
<th>{{ __('angel') }}</th>
<th>{{ __('message') }}</th>
<th>{{ __('Date') }}</th>
</tr>
</thead>
<tbody>
{% for c in conversations %}
<tr>
<td>
<span class="icon-icon_angel"></span>
{{ c.other_user.nameWithPronoun() }}
{% if c.unread_messages > 0 %}
<span class="badge bg-danger">{{ c.unread_messages }}</span>
{% endif %}
</td>
<td>
<a href="{{ url('messages/' ~ c.other_user.id) }}">
{{ c.latest_message.text|length > 100 ? c.latest_message.text|slice(0, 100) ~ '...' : c.latest_message.text }}
</a>
</td>
<td>
{{ c.latest_message.created_at }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,285 @@
<?php
namespace Engelsystem\Controllers;
use Engelsystem\Database\Database;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Redirector;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Models\Message;
use Engelsystem\Models\User\User;
use Illuminate\Database\Query\Expression as QueryExpression;
use Psr\Log\LoggerInterface;
use Illuminate\Support\Collection;
use Engelsystem\Http\Exceptions\HttpForbidden;
class MessagesController extends BaseController
{
/** @var Authenticator */
protected $auth;
/** @var LoggerInterface */
protected $log;
/** @var Redirector */
protected $redirect;
/** @var Response */
protected $response;
/** @var Response */
protected $request;
/** @var Database */
protected $db;
/** @var Message */
protected $message;
/** @var User */
protected $user;
/** @var string[] */
protected $permissions = [
'user_messages',
];
/**
* @param Authenticator $auth
* @param LoggerInterface $log
* @param Redirector $redirect
* @param Response $response
* @param Request $request
* @param Database $db
* @param Message $message
* @param User $user
*/
public function __construct(
Authenticator $auth,
LoggerInterface $log,
Redirector $redirect,
Response $response,
Request $request,
Database $db,
Message $message,
User $user
) {
$this->auth = $auth;
$this->log = $log;
$this->redirect = $redirect;
$this->response = $response;
$this->request = $request;
$this->db = $db;
$this->message = $message;
$this->user = $user;
}
public function index(): Response
{
return $this->listConversations();
}
/**
* Returns a list of conversations of the current user, each containing the other participant,
* the most recent message, and the number of unread messages.
*/
public function listConversations(): Response
{
$current_user = $this->auth->user();
$latest_messages = $this->latestMessagePerConversation($current_user);
$numberOfUnreadMessages = $this->numberOfUnreadMessagesPerConversation($current_user);
$conversations = [];
foreach ($latest_messages as $msg) {
$other_user = $msg->user_id == $current_user->id ? $msg->receiver : $msg->sender;
$unread_messages = $numberOfUnreadMessages[$other_user->id] ?? 0;
array_push($conversations, [
'other_user' => $other_user,
'latest_message' => $msg,
'unread_messages' => $unread_messages
]);
}
$users = $this->user->orderBy('name')->get()
->except($current_user->id)
->mapWithKeys(function ($u) {
return [ $u->id => $u->nameWithPronoun() ];
});
return $this->response->withView(
'pages/messages/overview.twig',
[
'conversations' => $conversations,
'users' => $users
]
);
}
/**
* Forwards to the conversation with the user of the given id.
*/
public function toConversation(Request $request): Response
{
$data = $this->validate($request, [ 'user_id' => 'required|int' ]);
return $this->redirect->to('/messages/' . $data['user_id']);
}
/**
* Returns a list of messages between the current user and a user with the given id. The ids shall not be the same.
* Unread messages will be marked as read during this call. Still, they will be shown as unread in the frontend to
* highlight them to the user as new.
*/
public function conversation(Request $request): Response
{
$current_user = $this->auth->user();
$other_user = $this->user->findOrFail($request->getAttribute('user_id'));
if ($current_user->id == $other_user->id) {
throw new HttpForbidden('You can not start a conversation with yourself.');
}
$messages = $this->message
->where(function ($q) use ($current_user, $other_user) {
$q->whereUserId($current_user->id)
->whereReceiverId($other_user->id);
})
->orWhere(function ($q) use ($current_user, $other_user) {
$q->whereUserId($other_user->id)
->whereReceiverId($current_user->id);
})
->orderBy('created_at')
->get();
$unread_messages = $messages->filter(function ($m) use ($other_user) {
return $m->user_id == $other_user->id && !$m->read;
});
foreach ($unread_messages as $msg) {
$msg->read = true;
$msg->save();
$msg->read = false; // change back to true to display it to the frontend one more time.
}
return $this->response->withView(
'pages/messages/conversation.twig',
['messages' => $messages, 'other_user' => $other_user]
);
}
/**
* Sends a message to another user.
*/
public function send(Request $request): Response
{
$current_user = $this->auth->user();
$data = $this->validate($request, [ 'text' => 'required' ]);
$other_user = $this->user->findOrFail($request->getAttribute('user_id'));
if ($other_user->id == $current_user->id) {
throw new HttpForbidden('You can not send a message to yourself.');
}
$new_message = new Message();
$new_message->sender()->associate($current_user);
$new_message->receiver()->associate($other_user);
$new_message->text = $data['text'];
$new_message->read = false;
$new_message->save();
$this->log->info(
'User {from} has written a message to user {to}',
[
'from' => $current_user->id,
'to' => $other_user->id
]
);
return $this->redirect->to('/messages/' . $other_user->id);
}
/**
* Deletes a message from a given id, as long as this message was send by the current user. The given user_id
* The given user_id is used to redirect back to the conversation with that user.
*/
public function delete(Request $request): Response
{
$current_user = $this->auth->user();
$other_user_id = $request->getAttribute('user_id');
$msg_id = $request->getAttribute('msg_id');
$msg = $this->message->findOrFail($msg_id);
if ($msg->user_id == $current_user->id) {
$msg->delete();
$this->log->info(
'User {from} deleted message {msg} in a conversation with user {to}',
[
'from' => $current_user->id,
'to' => $other_user_id,
'msg' => $msg_id
]
);
} else {
$this->log->warning(
'User {from} tried to delete message {msg} which was not written by them, ' .
'in a conversation with user {to}',
[
'from' => $current_user->id,
'to' => $other_user_id,
'msg' => $msg_id
]
);
throw new HttpForbidden('You can not delete a message you haven\'t send');
}
return $this->redirect->to('/messages/' . $other_user_id);
}
public function numberOfUnreadMessages(): int
{
return $this->auth->user()
->messagesReceived()
->where('read', false)
->count();
}
protected function numberOfUnreadMessagesPerConversation($current_user): Collection
{
return $current_user->messagesReceived()
->select('user_id', $this->raw('count(*) as amount'))
->where('read', false)
->groupBy('user_id')
->get(['user_id', 'amount'])
->mapWithKeys(function ($unread) {
return [ $unread->user_id => $unread->amount ];
});
}
protected function latestMessagePerConversation($current_user): Collection
{
$latest_message_ids = $this->message
->select($this->raw('max(id) as last_id'))
->where('user_id', "=", $current_user->id)
->orWhere('receiver_id', "=", $current_user->id)
->groupBy($this->raw(
'(CASE WHEN user_id = ' . $current_user->id .
' THEN receiver_id ELSE user_id END)'
));
return $this->message
->joinSub($latest_message_ids, 'conversations', function ($join) {
$join->on('messages.id', '=', 'conversations.last_id');
})
->orderBy('created_at', 'DESC')
->get();
}
protected function raw($value): QueryExpression
{
return $this->db->getConnection()->raw($value);
}
}

View File

@ -140,10 +140,6 @@ class LegacyMiddleware implements MiddlewareInterface
return [$title, $content];
case 'user_worklog':
return user_worklog_controller();
case 'user_messages':
$title = messages_title();
$content = user_messages();
return [$title, $content];
case 'user_settings':
$title = settings_title();
$content = user_settings();

View File

@ -20,8 +20,10 @@ use Illuminate\Support\Carbon;
* @property string $text
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property-read User $sender
* @property-read User $receiver
* @method static Builder|Message whereId($value)
* @method static Builder|Message whereUserId($value)
* @method static Builder|Message whereReceiverId($value)
* @method static Builder|Message whereRead($value)
* @method static Builder|Message whereText($value)
@ -56,6 +58,14 @@ class Message extends BaseModel
'read' => false,
];
/**
* @return BelongsTo
*/
public function sender(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* @return BelongsTo
*/

View File

@ -225,4 +225,18 @@ class User extends BaseModel
->orderBy('read')
->orderBy('id', 'DESC');
}
/**
* Return either just the user name or the name alongside with the pronoun.
* @return string
*/
public function nameWithPronoun(): string
{
if (config('enable_pronoun')) {
$pronoun = $this->personalData->pronoun;
return $pronoun ? $this->name . ' (' . $pronoun . ')' : $this->name;
} else {
return $this->name;
}
}
}

View File

@ -0,0 +1,585 @@
<?php
namespace Engelsystem\Test\Unit\Controllers;
use Carbon\Carbon;
use Engelsystem\Controllers\MessagesController;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\HttpForbidden;
use Engelsystem\Http\Exceptions\ValidationException;
use Engelsystem\Http\UrlGenerator;
use Engelsystem\Http\UrlGeneratorInterface;
use Engelsystem\Http\Validation\Validator;
use Engelsystem\Models\Message;
use Engelsystem\Models\Question;
use Engelsystem\Models\User\PersonalData;
use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\HasDatabase;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Collection;
use PHPUnit\Framework\MockObject\MockObject;
class MessagesControllerTest extends ControllerTest
{
use HasDatabase;
/** @var MessagesController */
protected $controller;
/** @var Authenticator|MockObject */
protected $auth;
/** @var User */
protected $user_a;
/** @var User */
protected $user_b;
/** @var Carbon */
protected $now;
/** @var Carbon */
protected $one_minute_ago;
/** @var Carbon */
protected $two_minutes_ago;
/**
* @testdox index: underNormalConditions -> returnsCorrectViewAndData
*/
public function testIndexUnderNormalConditionsReturnsCorrectViewAndData()
{
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$this->assertEquals('pages/messages/overview.twig', $view);
$this->assertArrayHasKey('conversations', $data);
$this->assertArrayHasKey('users', $data);
$this->assertArrayOrCollection($data['conversations']);
$this->assertArrayOrCollection($data['users']);
return $this->response;
});
$this->controller->index();
}
/**
* @testdox index: otherUsersExist -> returnsUsersWithoutMeOrderedByName
*/
public function testIndexOtherUsersExistReturnsUsersWithoutMeOrderedByName()
{
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$users = $data['users'];
$this->assertEquals(1, count($users));
$this->assertEquals('b', $users[$this->user_b->id]);
return $this->response;
});
$this->controller->index();
}
/**
* @testdox index: pronounsDeactivated -> userListHasNoPronouns
*/
public function testIndexPronounsDeactivatedUserListHasNoPronouns()
{
$this->user_with_pronoun = User::factory(['name' => 'x'])
->has(PersonalData::factory(['pronoun' => 'X']))->create();
$this->user_without_pronoun = $this->user_b;
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$users = $data['users'];
$this->assertEquals('x', $users[$this->user_with_pronoun->id]);
$this->assertEquals('b', $users[$this->user_without_pronoun->id]);
return $this->response;
});
$this->controller->index();
}
/**
* @testdox index: pronounsActivated -> userListHasPronouns
*/
public function testIndexPronounsActivatedUserListHasPronouns()
{
config(['enable_pronoun' => true]);
$this->user_with_pronoun = User::factory(['name' => 'x'])
->has(PersonalData::factory(['pronoun' => 'X']))->create();
$this->user_without_pronoun = $this->user_b;
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$users = $data['users'];
$this->assertEquals('x (X)', $users[$this->user_with_pronoun->id]);
$this->assertEquals('b', $users[$this->user_without_pronoun->id]);
return $this->response;
});
$this->controller->index();
}
/**
* @testdox index: withNoConversation -> returnsEmptyConversationList
*/
public function testIndexWithNoConversationReturnsEmptyConversationList()
{
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$this->assertEquals(0, count($data['conversations']));
return $this->response;
});
$this->controller->index();
}
/**
* @testdox index: withConversation -> conversationContainsCorrectData
*/
public function testIndexWithConversationConversationContainsCorrectData()
{
// save messages in wrong order to ensure latest message considers creation date, not id.
$this->createMessage($this->user_a, $this->user_b, 'a>b', $this->now);
$this->createMessage($this->user_b, $this->user_a, 'b>a', $this->two_minutes_ago);
$this->createMessage($this->user_b, $this->user_a, 'b>a', $this->one_minute_ago);
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$conversations = $data['conversations'];
$this->assertEquals(1, count($conversations));
$c = $conversations[0];
$this->assertArrayHasKey('other_user', $c);
$this->assertArrayHasKey('latest_message', $c);
$this->assertArrayHasKey('unread_messages', $c);
$this->assertTrue($c['other_user'] instanceof User);
$this->assertTrue($c['latest_message'] instanceof Message);
$this->assertEquals('string', gettype($c['unread_messages']));
$this->assertEquals('b', $c['other_user']->name);
$this->assertEquals('b>a', $c['latest_message']->text);
$this->assertEquals(2, $c['unread_messages']);
return $this->response;
});
$this->controller->index();
}
/**
* @testdox index: withConversations -> onlyContainsConversationsWithMe
*/
public function testIndexWithConversationsOnlyContainsConversationsWithMe()
{
$user_c = User::factory(['name' => 'c'])->create();
// save messages in wrong order to ensure latest message considers creation date, not id.
$this->createMessage($this->user_a, $this->user_b, 'a>b', $this->now);
$this->createMessage($this->user_b, $user_c, 'b>c', $this->now);
$this->createMessage($user_c, $this->user_a, 'c>a', $this->now);
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$conversations = $data['conversations'];
$this->assertEquals(2, count($conversations));
$msg0 = $conversations[0]['latest_message']->text;
$msg1 = $conversations[1]['latest_message']->text;
$this->assertTrue(($msg0 == 'a>b' && $msg1 == 'c>a') || ($msg1 == 'c>a' && $msg0 == 'a>b'));
return $this->response;
});
$this->controller->index();
}
/**
* @testdox index: withConversations -> conversationsOrderedByDate
*/
public function testIndexWithConversationsConversationsOrderedByDate()
{
$user_c = User::factory(['name' => 'c'])->create();
$user_d = User::factory(['name' => 'd'])->create();
$this->createMessage($this->user_a, $this->user_b, 'a>b', $this->now);
$this->createMessage($user_d, $this->user_a, 'd>a', $this->two_minutes_ago);
$this->createMessage($this->user_a, $user_c, 'a>c', $this->one_minute_ago);
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$conversations = $data['conversations'];
$this->assertEquals('a>b', $conversations[0]['latest_message']->text);
$this->assertEquals('a>c', $conversations[1]['latest_message']->text);
$this->assertEquals('d>a', $conversations[2]['latest_message']->text);
return $this->response;
});
$this->controller->index();
}
/**
* @testdox ToConversation: withNoUserIdGiven -> throwsException
*/
public function testToConversationWithNoUserIdGivenThrowsException()
{
$this->expectException(ValidationException::class);
$this->controller->toConversation($this->request);
}
/**
* @testdox ToConversation: withUserIdGiven -> redirect
*/
public function testToConversationWithUserIdGivenRedirect()
{
$this->request = $this->request->withParsedBody([
'user_id' => '1',
]);
$this->response->expects($this->once())
->method('redirectTo')
->with('http://localhost/messages/1')
->willReturn($this->response);
$this->controller->toConversation($this->request);
}
/**
* @testdox conversation: withNoUserIdGiven -> throwsException
*/
public function testConversationWithNoUserIdGivenThrowsException()
{
$this->expectException(ModelNotFoundException::class);
$this->controller->conversation($this->request);
}
/**
* @testdox conversation: withMyUserIdGiven -> throwsException
*/
public function testConversationWithMyUserIdGivenThrowsException()
{
$this->request->attributes->set('user_id', $this->user_a->id);
$this->expectException(HttpForbidden::class);
$this->controller->conversation($this->request);
}
/**
* @testdox conversation: withUnknownUserIdGiven -> throwsException
*/
public function testConversationWithUnknownUserIdGivenThrowsException()
{
$this->request->attributes->set('user_id', '1234');
$this->expectException(ModelNotFoundException::class);
$this->controller->conversation($this->request);
}
/**
* @testdox conversation: underNormalConditions -> returnsCorrectViewAndData
*/
public function testConversationUnderNormalConditionsReturnsCorrectViewAndData()
{
$this->request->attributes->set('user_id', $this->user_b->id);
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$this->assertEquals('pages/messages/conversation.twig', $view);
$this->assertArrayHasKey('messages', $data);
$this->assertArrayHasKey('other_user', $data);
$this->assertArrayOrCollection($data['messages']);
$this->assertTrue($data['other_user'] instanceof User);
return $this->response;
});
$this->controller->conversation($this->request);
}
/**
* @testdox conversation: withNoMessages -> returnsEmptyMessageList
*/
public function testConversationWithNoMessagesReturnsEmptyMessageList()
{
$this->request->attributes->set('user_id', $this->user_b->id);
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$this->assertEquals(0, count($data['messages']));
return $this->response;
});
$this->controller->conversation($this->request);
}
/**
* @testdox conversation: withMessages -> messagesOnlyWithThatUserOrderedByDate
*/
public function testConversationWithMessagesMessagesOnlyWithThatUserOrderedByDate()
{
$this->request->attributes->set('user_id', $this->user_b->id);
$user_c = User::factory(['name' => 'c'])->create();
// to be listed
$this->createMessage($this->user_a, $this->user_b, 'a>b', $this->now);
$this->createMessage($this->user_b, $this->user_a, 'b>a', $this->two_minutes_ago);
$this->createMessage($this->user_b, $this->user_a, 'b>a2', $this->one_minute_ago);
// not to be listed
$this->createMessage($this->user_a, $user_c, 'a>c', $this->now);
$this->createMessage($user_c, $this->user_b, 'b>c', $this->now);
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$messages = $data['messages'];
$this->assertEquals(3, count($messages));
$this->assertEquals('b>a', $messages[0]->text);
$this->assertEquals('b>a2', $messages[1]->text);
$this->assertEquals('a>b', $messages[2]->text);
return $this->response;
});
$this->controller->conversation($this->request);
}
/**
* @testdox conversation: withUnreadMessages -> messagesToMeWillStillBeReturnedAsUnread
*/
public function testConversationWithUnreadMessagesMessagesToMeWillStillBeReturnedAsUnread()
{
$this->request->attributes->set('user_id', $this->user_b->id);
$this->createMessage($this->user_b, $this->user_a, 'b>a', $this->now);
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
$this->assertFalse($data['messages'][0]->read);
return $this->response;
});
$this->controller->conversation($this->request);
}
/**
* @testdox conversation: withUnreadMessages -> messagesToMeWillBeMarkedAsRead
*/
public function testConversationWithUnreadMessagesMessagesToMeWillBeMarkedAsRead()
{
$this->request->attributes->set('user_id', $this->user_b->id);
$this->response->expects($this->once())
->method('withView')
->willReturnCallback(function (string $view, array $data) {
return $this->response;
});
$msg = $this->createMessage($this->user_b, $this->user_a, 'b>a', $this->now);
$this->controller->conversation($this->request);
$this->assertTrue(Message::whereId($msg->id)->first()->read);
}
/**
* @testdox send: withNoTextGiven -> throwsException
*/
public function testSendWithNoTextGivenThrowsException()
{
$this->expectException(ValidationException::class);
$this->controller->send($this->request);
}
/**
* @testdox send: withNoUserIdGiven -> throwsException
*/
public function testSendWithNoUserIdGivenThrowsException()
{
$this->request = $this->request->withParsedBody([
'text' => 'a',
]);
$this->expectException(ModelNotFoundException::class);
$this->controller->send($this->request);
}
/**
* @testdox send: withMyUserIdGiven -> throwsException
*/
public function testSendWithMyUserIdGivenThrowsException()
{
$this->request = $this->request->withParsedBody([
'text' => 'a',
]);
$this->request->attributes->set('user_id', $this->user_a->id);
$this->expectException(HttpForbidden::class);
$this->controller->send($this->request);
}
/**
* @testdox send: withUnknownUserIdGiven -> throwsException
*/
public function testSendWithUnknownUserIdGivenThrowsException()
{
$this->request = $this->request->withParsedBody([
'text' => 'a',
]);
$this->request->attributes->set('user_id', '1234');
$this->expectException(ModelNotFoundException::class);
$this->controller->send($this->request);
}
/**
* @testdox send: withUserAndTextGiven -> savesMessage
*/
public function testSendWithUserAndTextGivenSavesMessage()
{
$this->request = $this->request->withParsedBody([
'text' => 'a',
]);
$this->request->attributes->set('user_id', $this->user_b->id);
$this->response->expects($this->once())
->method('redirectTo')
->with('http://localhost/messages/' . $this->user_b->id)
->willReturn($this->response);
$this->controller->send($this->request);
$msg = Message::whereText('a')->first();
$this->assertEquals($this->user_a->id, $msg->user_id);
$this->assertEquals($this->user_b->id, $msg->receiver_id);
$this->assertFalse($msg->read);
}
/**
* @testdox delete: withNoMsgIdGiven -> throwsException
*/
public function testDeleteWithNoMsgIdGivenThrowsException()
{
$this->expectException(ModelNotFoundException::class);
$this->controller->delete($this->request);
}
/**
* @testdox delete: tryingToDeleteSomeonesMessage -> throwsException
*/
public function testDeleteTryingToDeleteSomeonesMessageThrowsException()
{
$this->expectException(HttpForbidden::class);
$msg = $this->createMessage($this->user_b, $this->user_a, 'a>b', $this->now);
$this->request->attributes->set('msg_id', $msg->id);
$this->controller->delete($this->request);
}
/**
* @testdox delete: tryingToDeleteMyMessage -> deletesItAndRedirect
*/
public function testDeleteTryingToDeleteMyMessageDeletesItAndRedirect()
{
$msg = $this->createMessage($this->user_a, $this->user_b, 'a>b', $this->now);
$this->request->attributes->set('msg_id', $msg->id);
$this->request->attributes->set('user_id', '1');
$this->response->expects($this->once())
->method('redirectTo')
->with('http://localhost/messages/1')
->willReturn($this->response);
$this->controller->delete($this->request);
$this->assertEquals(0, count(Message::whereId($msg->id)->get()));
}
/**
* @testdox NumberOfUnreadMessages: withNoMessages -> returns0
*/
public function testNumberOfUnreadMessagesWithNoMessagesReturns0()
{
$this->assertEquals(0, $this->controller->numberOfUnreadMessages());
}
/**
* @testdox NumberOfUnreadMessages: withMessagesNotToMe -> messagesNotToMeAreIgnored
*/
public function testNumberOfUnreadMessagesWithMessagesNotToMeMessagesNotToMeAreIgnored()
{
$user_c = User::factory(['name' => 'c'])->create();
$this->createMessage($this->user_a, $this->user_b, 'a>b', $this->now);
$this->createMessage($this->user_b, $user_c, 'b>c', $this->now);
$this->assertEquals(0, $this->controller->numberOfUnreadMessages());
}
/**
* @testdox NumberOfUnreadMessages: withMessages -> returnsSumOfUnreadMessagesSentToMe
*/
public function testNumberOfUnreadMessagesWithMessagesReturnsSumOfUnreadMessagesSentToMe()
{
$user_c = User::factory(['name' => 'c'])->create();
$this->createMessage($this->user_b, $this->user_a, 'b>a1', $this->now);
$this->createMessage($this->user_b, $this->user_a, 'b>a2', $this->now);
$this->createMessage($user_c, $this->user_a, 'c>a', $this->now);
$this->assertEquals(3, $this->controller->numberOfUnreadMessages());
}
/**
* Setup environment
*/
public function setUp(): void
{
parent::setUp();
$this->auth = $this->createMock(Authenticator::class);
$this->app->instance(Authenticator::class, $this->auth);
$this->app->bind(UrlGeneratorInterface::class, UrlGenerator::class);
$this->user_a = User::factory(['name' => 'a'])->create();
$this->user_b = User::factory(['name' => 'b'])->create();
$this->setExpects($this->auth, 'user', null, $this->user_a, $this->any());
$this->now = Carbon::now();
$this->one_minute_ago = Carbon::now()->subMinute();
$this->two_minutes_ago = Carbon::now()->subMinutes(2);
$this->controller = $this->app->get(MessagesController::class);
$this->controller->setValidator(new Validator());
}
protected function assertArrayOrCollection($obj)
{
$this->assertTrue(gettype($obj) == 'array' || $obj instanceof Collection);
}
protected function createMessage(User $from, User $to, string $text, Carbon $at): Message
{
Message::unguard(); // unguard temporarily to save custom creation dates.
$msg = new Message([
'user_id' => $from->id,
'receiver_id' => $to->id,
'text' => $text,
'created_at' => $at,
'updated_at' => $at,
]);
$msg->save();
Message::reguard();
return $msg;
}
}

View File

@ -4,6 +4,7 @@ namespace Engelsystem\Test\Unit\Models\User;
use Carbon\Carbon;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use Engelsystem\Config\Config;
use Engelsystem\Models\BaseModel;
use Engelsystem\Models\News;
use Engelsystem\Models\NewsComment;
@ -276,4 +277,45 @@ class UserTest extends ModelTest
$this->assertContains($question1->id, $answers);
$this->assertContains($question2->id, $answers);
}
/**
* @testdox nameWithPronoun: pronounsDeactivated -> returnNameOnly
*/
public function testNameWithPronounPronounsDeactivatedReturnNameOnly()
{
$user_with_pronoun = User::factory(['name' => 'x'])
->has(PersonalData::factory(['pronoun' => 'X']))->create();
$user_without_pronoun = User::factory(['name' => 'y'])->create();
$this->assertEquals('x', $user_with_pronoun->nameWithPronoun());
$this->assertEquals('y', $user_without_pronoun->nameWithPronoun());
}
/**
* @testdox nameWithPronoun: pronounsActivated -> returnNameAndPronoun
*/
public function testNameWithPronounPronounsActivatedReturnNameAndPronoun()
{
config(['enable_pronoun' => true]);
$user_with_pronoun = User::factory(['name' => 'x'])
->has(PersonalData::factory(['pronoun' => 'X']))->create();
$user_without_pronoun = User::factory(['name' => 'y'])->create();
$this->assertEquals('x (X)', $user_with_pronoun->nameWithPronoun());
$this->assertEquals('y', $user_without_pronoun->nameWithPronoun());
}
/**
* Prepare test
*/
protected function setUp(): void
{
parent::setUp();
// config needed for checking if pronouns are activated.
$config = new Config();
$this->app->instance('config', $config);
$this->app->instance(Config::class, $config);
}
}