diff --git a/config/routes.php b/config/routes.php index 2f903922..91fb30ab 100644 --- a/config/routes.php +++ b/config/routes.php @@ -39,6 +39,7 @@ $route->get('/news', 'NewsController@index'); $route->get('/meetings', 'NewsController@meetings'); $route->get('/news/{id:\d+}', 'NewsController@show'); $route->post('/news/{id:\d+}', 'NewsController@comment'); +$route->post('/news/comment/{id:\d+}', 'NewsController@deleteComment'); // FAQ $route->get('/faq', 'FaqController@index'); diff --git a/resources/lang/de_DE/additional.po b/resources/lang/de_DE/additional.po index 875c7f0b..c2507d80 100644 --- a/resources/lang/de_DE/additional.po +++ b/resources/lang/de_DE/additional.po @@ -74,6 +74,9 @@ msgstr "Die Minuten nach dem Talk müssen eine Zahl sein." msgid "news.comment.success" msgstr "Kommentar gespeichert." +msgid "news.comment-delete.success" +msgstr "Kommentar erfolgreich gelöscht." + msgid "news.edit.success" msgstr "News erfolgreich aktualisiert." diff --git a/resources/lang/en_US/additional.po b/resources/lang/en_US/additional.po index db0e33ea..0a828419 100644 --- a/resources/lang/en_US/additional.po +++ b/resources/lang/en_US/additional.po @@ -72,6 +72,9 @@ msgstr "The minutes after the talk have to be an integer." msgid "news.comment.success" msgstr "Comment saved." +msgid "news.comment-delete.success" +msgstr "Comment successfully deleted." + msgid "news.edit.success" msgstr "News successfully updated." diff --git a/resources/views/pages/news/news.twig b/resources/views/pages/news/news.twig index 93d21628..8bc3225d 100644 --- a/resources/views/pages/news/news.twig +++ b/resources/views/pages/news/news.twig @@ -22,6 +22,17 @@ {{ comment.created_at.format(__('Y-m-d H:i')) }} {{ m.user(comment.user) }} + + {% if comment.user.id == user.id or has_permission_to('admin_news') or has_permission_to('comment.delete') %} +
+
+ {{ csrf() }} + {{ f.submit(m.glyphicon('trash'), {'name': 'delete', 'btn_type': 'danger', 'btn_size': 'xs', 'title': __('form.delete')}) }} +
+
+ {% endif %} {% endfor %} diff --git a/src/Controllers/NewsController.php b/src/Controllers/NewsController.php index 2d9b540d..c42a16e1 100644 --- a/src/Controllers/NewsController.php +++ b/src/Controllers/NewsController.php @@ -4,6 +4,7 @@ namespace Engelsystem\Controllers; use Engelsystem\Config\Config; use Engelsystem\Helpers\Authenticator; +use Engelsystem\Http\Exceptions\HttpForbidden; use Engelsystem\Http\Redirector; use Engelsystem\Http\Request; use Engelsystem\Http\Response; @@ -18,6 +19,9 @@ class NewsController extends BaseController /** @var Authenticator */ protected $auth; + /** @var NewsComment */ + protected $comment; + /** @var Config */ protected $config; @@ -39,13 +43,15 @@ class NewsController extends BaseController /** @var array */ protected $permissions = [ 'news', - 'meetings' => 'user_meetings', - 'comment' => 'news_comments', + 'meetings' => 'user_meetings', + 'comment' => 'news_comments', + 'deleteComment' => 'news_comments', ]; /** * @param Authenticator $auth * @param Config $config + * @param NewsComment $comment * @param LoggerInterface $log * @param News $news * @param Redirector $redirector @@ -54,6 +60,7 @@ class NewsController extends BaseController */ public function __construct( Authenticator $auth, + NewsComment $comment, Config $config, LoggerInterface $log, News $news, @@ -62,6 +69,7 @@ class NewsController extends BaseController Request $request ) { $this->auth = $auth; + $this->comment = $comment; $this->config = $config; $this->log = $log; $this->news = $news; @@ -132,6 +140,41 @@ class NewsController extends BaseController return $this->redirect->back(); } + /** + * @param Request $request + * + * @return Response + */ + public function deleteComment(Request $request): Response + { + $id = $request->getAttribute('id'); + $this->validate( + $request, + [ + 'delete' => 'checked', + ] + ); + + $comment = $this->comment->findOrFail($id); + if ( + $comment->user->id != $this->auth->user()->id + && !$this->auth->can('admin_news') + && !$this->auth->can('comment.delete') + ) { + throw new HttpForbidden(); + } + + $comment->delete(); + + $this->log->info( + 'Deleted comment "{comment}" of news "{news}"', + ['comment' => $comment->text, 'news' => $comment->news->title] + ); + $this->addNotification('news.comment-delete.success'); + + return $this->redirect->to('/news/' . $comment->news->id); + } + /** * @param bool $onlyMeetings * @return Response diff --git a/tests/Unit/Controllers/ControllerTest.php b/tests/Unit/Controllers/ControllerTest.php index 1d2f35eb..a0b76339 100644 --- a/tests/Unit/Controllers/ControllerTest.php +++ b/tests/Unit/Controllers/ControllerTest.php @@ -72,5 +72,6 @@ abstract class ControllerTest extends TestCase $this->config = new Config(); $this->app->instance('config', $this->config); + $this->app->instance(Config::class, $this->config); } } diff --git a/tests/Unit/Controllers/NewsControllerTest.php b/tests/Unit/Controllers/NewsControllerTest.php index df0c977a..1e2a26dd 100644 --- a/tests/Unit/Controllers/NewsControllerTest.php +++ b/tests/Unit/Controllers/NewsControllerTest.php @@ -2,30 +2,20 @@ namespace Engelsystem\Test\Unit\Controllers; -use Engelsystem\Config\Config; use Engelsystem\Controllers\NewsController; use Engelsystem\Helpers\Authenticator; +use Engelsystem\Http\Exceptions\HttpForbidden; use Engelsystem\Http\Exceptions\ValidationException; -use Engelsystem\Http\Request; -use Engelsystem\Http\Response; -use Engelsystem\Http\UrlGenerator; -use Engelsystem\Http\UrlGeneratorInterface; use Engelsystem\Http\Validation\Validator; use Engelsystem\Models\News; use Engelsystem\Models\NewsComment; use Engelsystem\Models\User\User; use Engelsystem\Test\Unit\HasDatabase; -use Engelsystem\Test\Unit\TestCase; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Collection; use PHPUnit\Framework\MockObject\MockObject; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Log\LoggerInterface; -use Psr\Log\Test\TestLogger; -use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; -class NewsControllerTest extends TestCase +class NewsControllerTest extends ControllerTest { use HasDatabase; @@ -75,15 +65,6 @@ class NewsControllerTest extends TestCase ], ]; - /** @var TestLogger */ - protected $log; - - /** @var Response|MockObject */ - protected $response; - - /** @var Request */ - protected $request; - /** * @covers \Engelsystem\Controllers\NewsController::__construct * @covers \Engelsystem\Controllers\NewsController::index @@ -225,51 +206,108 @@ class NewsControllerTest extends TestCase $this->log->hasInfoThatContains('Created news comment'); /** @var NewsComment $comment */ - $comment = NewsComment::whereNewsId(1)->first(); + $comment = NewsComment::whereNewsId(1)->get()[2]; $this->assertEquals('Foo bar!', $comment->text); } + /** + * @covers \Engelsystem\Controllers\NewsController::deleteComment + */ + public function testDeleteCommentInvalidRequest() + { + /** @var NewsController $controller */ + $controller = $this->app->get(NewsController::class); + $controller->setValidator($this->app->get(Validator::class)); + + $this->expectException(ValidationException::class); + $controller->deleteComment($this->request); + } + + /** + * @covers \Engelsystem\Controllers\NewsController::deleteComment + */ + public function testDeleteCommentNotFound() + { + $this->request = $this->request->withAttribute('id', 42)->withParsedBody(['delete' => '1']); + + /** @var NewsController $controller */ + $controller = $this->app->get(NewsController::class); + $controller->setValidator($this->app->get(Validator::class)); + + $this->expectException(ModelNotFoundException::class); + $controller->deleteComment($this->request); + } + + /** + * @covers \Engelsystem\Controllers\NewsController::deleteComment + */ + public function testDeleteCommentNotAllowed() + { + $this->request = $this->request->withAttribute('id', 2)->withParsedBody(['delete' => '1']); + + $this->addUser(1); + $this->addUser(2); + + /** @var NewsController $controller */ + $controller = $this->app->get(NewsController::class); + $controller->setValidator($this->app->get(Validator::class)); + + $this->expectException(HttpForbidden::class); + $controller->deleteComment($this->request); + } + + /** + * @covers \Engelsystem\Controllers\NewsController::deleteComment + */ + public function testDeleteComment() + { + $this->request = $this->request->withAttribute('id', 1)->withParsedBody(['delete' => '1']); + $this->setExpects($this->response, 'redirectTo', ['http://localhost/news/1'], $this->response); + + $this->addUser(1); + + /** @var NewsController $controller */ + $controller = $this->app->get(NewsController::class); + $controller->setValidator($this->app->get(Validator::class)); + + $controller->deleteComment($this->request); + + $this->assertCount(1, NewsComment::all()); + $this->assertTrue($this->log->hasInfoThatContains('Deleted comment')); + $this->assertHasNotification('news.comment-delete.success'); + } + /** * Setup environment */ public function setUp(): void { parent::setUp(); - $this->initDatabase(); - $this->request = new Request(); - $this->app->instance('request', $this->request); - $this->app->instance(Request::class, $this->request); - $this->app->instance(ServerRequestInterface::class, $this->request); - - $this->response = $this->createMock(Response::class); - $this->app->instance(Response::class, $this->response); - - $this->app->instance(Config::class, new Config(['display_news' => 2])); - - $this->log = new TestLogger(); - $this->app->instance(LoggerInterface::class, $this->log); - - $this->app->instance('session', new Session(new MockArraySessionStorage())); + $this->config->set(['display_news' => 2]); $this->auth = $this->createMock(Authenticator::class); $this->app->instance(Authenticator::class, $this->auth); - $this->app->bind(UrlGeneratorInterface::class, UrlGenerator::class); - - $this->app->instance('config', new Config()); - foreach ($this->data as $news) { (new News($news))->save(); } + + foreach ([1, 2] as $i) { + NewsComment::create([ + 'news_id' => 1, + 'text' => 'test comment ' . $i, + 'user_id' => $i, + ]); + } } /** * Creates a new user */ - protected function addUser() + protected function addUser(int $id = 42) { - $user = User::factory()->create(['id' => 42]); + $user = User::factory()->create(['id' => $id]); $this->auth->expects($this->any()) ->method('user')