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->get('/questions/new', 'QuestionsController@add');
$route->post('/questions/new', 'QuestionsController@save'); $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 // API
$route->get('/api[/{resource:.+}]', 'ApiController@index'); $route->get('/api[/{resource:.+}]', 'ApiController@index');

View File

@ -1,174 +1,14 @@
<?php <?php
use Engelsystem\Models\Message; use Engelsystem\Controllers\MessagesController;
use Engelsystem\Models\User\User;
/**
* @return string
*/
function messages_title()
{
return __('Messages');
}
/** /**
* @return string * @return string
*/ */
function user_unread_messages() function user_unread_messages()
{ {
$user = auth()->user(); $count = app()->make(MessagesController::class)
->numberOfUnreadMessages();
if ($user) { return $count > 0 ? ' <span class="badge bg-danger">' . $count . '</span>' : '';
$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 '';
} }

View File

@ -348,3 +348,14 @@ code {
content: ""; 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" msgid "user.edit.success"
msgstr "Benutzer erfolgreich bearbeitet." 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!" msgid "It's done!"
msgstr "Erledigt!" msgstr "Erledigt!"
#: includes/pages/user_messages.php:121
msgid "Message"
msgstr "Nachricht"
#: includes/pages/admin_rooms.php:202 #: includes/pages/admin_rooms.php:202
#: includes/view/User_view.php:129 #: includes/view/User_view.php:129
msgid "Delete" msgid "Delete"
@ -1685,10 +1681,6 @@ msgstr "Nachname"
msgid "Entry required!" msgid "Entry required!"
msgstr "Pflichtfeld!" msgstr "Pflichtfeld!"
#: includes/pages/user_messages.php:11
msgid "Messages"
msgstr "Nachrichten"
#: includes/pages/user_messages.php:49 #: includes/pages/user_messages.php:49
msgid "Select recipient..." msgid "Select recipient..."
msgstr "Empfänger auswählen..." msgstr "Empfänger auswählen..."
@ -3038,3 +3030,18 @@ msgstr "Angekommen"
msgid "user.got_shirt" msgid "user.got_shirt"
msgstr "Shirt bekommen" 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" msgid "user.edit.success"
msgstr "User edited successfully." msgstr "User edited successfully."
msgid "messages.delete.success"
msgstr "Message successfully deleted."

View File

@ -300,3 +300,18 @@ msgstr "Arrived"
msgid "user.got_shirt" msgid "user.got_shirt"
msgstr "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 %} {% endif %}
{% if is_user() and has_permission_to('user_messages') %} {% 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 %} {% endif %}
{{ menuUserHints() }} {{ 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]; return [$title, $content];
case 'user_worklog': case 'user_worklog':
return user_worklog_controller(); return user_worklog_controller();
case 'user_messages':
$title = messages_title();
$content = user_messages();
return [$title, $content];
case 'user_settings': case 'user_settings':
$title = settings_title(); $title = settings_title();
$content = user_settings(); $content = user_settings();

View File

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

View File

@ -225,4 +225,18 @@ class User extends BaseModel
->orderBy('read') ->orderBy('read')
->orderBy('id', 'DESC'); ->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 Carbon\Carbon;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use Engelsystem\Config\Config;
use Engelsystem\Models\BaseModel; use Engelsystem\Models\BaseModel;
use Engelsystem\Models\News; use Engelsystem\Models\News;
use Engelsystem\Models\NewsComment; use Engelsystem\Models\NewsComment;
@ -276,4 +277,45 @@ class UserTest extends ModelTest
$this->assertContains($question1->id, $answers); $this->assertContains($question1->id, $answers);
$this->assertContains($question2->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);
}
} }