merge post-comment-controllers
This commit is contained in:
commit
ec4f729c63
8 changed files with 743 additions and 0 deletions
124
backend/app/Controllers/CommentController.php
Normal file
124
backend/app/Controllers/CommentController.php
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Comment\Comment;
|
||||||
|
use App\Comment\UseCases\CreateComment\CreateComment;
|
||||||
|
use App\Comment\UseCases\CreateComment\CreateCommentRequest;
|
||||||
|
use App\Comment\UseCases\DeleteComment\DeleteComment;
|
||||||
|
use App\Comment\UseCases\DeleteComment\DeleteCommentRequest;
|
||||||
|
use App\Comment\UseCases\ListCommentsForPost\ListCommentsForPost;
|
||||||
|
use App\Comment\UseCases\ListCommentsForPost\ListCommentsForPostRequest;
|
||||||
|
use App\Exceptions\BadRequestException;
|
||||||
|
use App\Exceptions\ForbiddenException;
|
||||||
|
use App\User\User;
|
||||||
|
use App\User\UserRepository;
|
||||||
|
use DomainException;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class CommentController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private CreateComment $createComment,
|
||||||
|
private DeleteComment $deleteComment,
|
||||||
|
private ListCommentsForPost $listCommentsForPost,
|
||||||
|
private UserRepository $userRepo,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function listForPost(Request $request, int $postId): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$comments = $this->listCommentsForPost->execute(
|
||||||
|
new ListCommentsForPostRequest(postId: $postId),
|
||||||
|
);
|
||||||
|
} catch (BadRequestException $exception) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['error' => $exception->getMessage()], 400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'comments' => array_map(
|
||||||
|
function (Comment $comment) {
|
||||||
|
return $this->serialize($comment);
|
||||||
|
},
|
||||||
|
$comments,
|
||||||
|
),
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Request $request, int $postId): JsonResponse
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $request->attributes->get('user');
|
||||||
|
try {
|
||||||
|
$comment = $this->createComment->execute(new CreateCommentRequest(
|
||||||
|
postId: $postId,
|
||||||
|
userId: $user->getId(),
|
||||||
|
body: $request->input('body'),
|
||||||
|
));
|
||||||
|
} catch (BadRequestException $exception) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['error' => $exception->getMessage()], 400,
|
||||||
|
);
|
||||||
|
} catch (DomainException $exception) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['error' => $exception->getMessage()], 404,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'comment' => $this->serialize($comment),
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $request->attributes->get('user');
|
||||||
|
try {
|
||||||
|
$this->deleteComment->execute(new DeleteCommentRequest(
|
||||||
|
commentId: $id,
|
||||||
|
requesterId: $user->getId(),
|
||||||
|
requesterIsAdmin: $user->isAdmin(),
|
||||||
|
));
|
||||||
|
} catch (BadRequestException $exception) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['error' => $exception->getMessage()], 400,
|
||||||
|
);
|
||||||
|
} catch (ForbiddenException $exception) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['error' => $exception->getMessage()], 403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse(null, 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* id: int,
|
||||||
|
* postId: int,
|
||||||
|
* userId: int,
|
||||||
|
* authorDisplayName: string,
|
||||||
|
* body: string,
|
||||||
|
* createdAt: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private function serialize(Comment $comment): array
|
||||||
|
{
|
||||||
|
$author = $this->userRepo->find($comment->getUserId());
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $comment->getId(),
|
||||||
|
'postId' => $comment->getPostId(),
|
||||||
|
'userId' => $comment->getUserId(),
|
||||||
|
'authorDisplayName' => $author === null
|
||||||
|
? ''
|
||||||
|
: $author->getDisplayName(),
|
||||||
|
'body' => $comment->getBody(),
|
||||||
|
'createdAt' => $comment->getCreatedAt()->format(DATE_ATOM),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
181
backend/app/Controllers/PostController.php
Normal file
181
backend/app/Controllers/PostController.php
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Exceptions\BadRequestException;
|
||||||
|
use App\Exceptions\ForbiddenException;
|
||||||
|
use App\Post\Post;
|
||||||
|
use App\Post\UseCases\CreatePost\CreatePost;
|
||||||
|
use App\Post\UseCases\CreatePost\CreatePostRequest;
|
||||||
|
use App\Post\UseCases\DeletePost\DeletePost;
|
||||||
|
use App\Post\UseCases\DeletePost\DeletePostRequest;
|
||||||
|
use App\Post\UseCases\GetPost\GetPost;
|
||||||
|
use App\Post\UseCases\ListRecentPosts\ListRecentPosts;
|
||||||
|
use App\Post\UseCases\ListRecentPosts\ListRecentPostsRequest;
|
||||||
|
use App\Post\UseCases\ListUserPosts\ListUserPosts;
|
||||||
|
use App\Post\UseCases\ListUserPosts\ListUserPostsRequest;
|
||||||
|
use App\User\User;
|
||||||
|
use App\User\UserRepository;
|
||||||
|
use DomainException;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class PostController
|
||||||
|
{
|
||||||
|
private const RECENT_LIMIT = 20;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private CreatePost $createPost,
|
||||||
|
private DeletePost $deletePost,
|
||||||
|
private GetPost $getPost,
|
||||||
|
private ListRecentPosts $listRecentPosts,
|
||||||
|
private ListUserPosts $listUserPosts,
|
||||||
|
private UserRepository $userRepo,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function recent(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$posts = $this->listRecentPosts->execute(
|
||||||
|
new ListRecentPostsRequest(limit: self::RECENT_LIMIT),
|
||||||
|
);
|
||||||
|
} catch (BadRequestException $exception) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['error' => $exception->getMessage()], 400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'posts' => array_map(
|
||||||
|
function (Post $post) {
|
||||||
|
return $this->serialize($post);
|
||||||
|
},
|
||||||
|
$posts,
|
||||||
|
),
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$post = $this->getPost->execute($id);
|
||||||
|
} catch (BadRequestException $exception) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['error' => $exception->getMessage()], 400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($post === null) {
|
||||||
|
return new JsonResponse(['error' => 'post not found'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'post' => $this->serialize($post),
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listByUser(
|
||||||
|
Request $request,
|
||||||
|
string $displayName,
|
||||||
|
): JsonResponse {
|
||||||
|
$user = $this->userRepo->findByDisplayName($displayName);
|
||||||
|
if ($user === null) {
|
||||||
|
return new JsonResponse(['error' => 'user not found'], 404);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$posts = $this->listUserPosts->execute(
|
||||||
|
new ListUserPostsRequest(userId: $user->getId()),
|
||||||
|
);
|
||||||
|
} catch (BadRequestException $exception) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['error' => $exception->getMessage()], 400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'user' => [
|
||||||
|
'id' => $user->getId(),
|
||||||
|
'displayName' => $user->getDisplayName(),
|
||||||
|
],
|
||||||
|
'posts' => array_map(
|
||||||
|
function (Post $post) {
|
||||||
|
return $this->serialize($post);
|
||||||
|
},
|
||||||
|
$posts,
|
||||||
|
),
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $request->attributes->get('user');
|
||||||
|
try {
|
||||||
|
$post = $this->createPost->execute(new CreatePostRequest(
|
||||||
|
userId: $user->getId(),
|
||||||
|
title: $request->input('title'),
|
||||||
|
body: $request->input('body'),
|
||||||
|
));
|
||||||
|
} catch (BadRequestException $exception) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['error' => $exception->getMessage()], 400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'post' => $this->serialize($post),
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $request->attributes->get('user');
|
||||||
|
try {
|
||||||
|
$this->deletePost->execute(new DeletePostRequest(
|
||||||
|
postId: $id,
|
||||||
|
requesterId: $user->getId(),
|
||||||
|
requesterIsAdmin: $user->isAdmin(),
|
||||||
|
));
|
||||||
|
} catch (BadRequestException $exception) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['error' => $exception->getMessage()], 400,
|
||||||
|
);
|
||||||
|
} catch (ForbiddenException $exception) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['error' => $exception->getMessage()], 403,
|
||||||
|
);
|
||||||
|
} catch (DomainException $exception) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['error' => $exception->getMessage()], 409,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse(null, 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* id: int,
|
||||||
|
* userId: int,
|
||||||
|
* authorDisplayName: string,
|
||||||
|
* title: string,
|
||||||
|
* body: string,
|
||||||
|
* createdAt: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private function serialize(Post $post): array
|
||||||
|
{
|
||||||
|
$author = $this->userRepo->find($post->getUserId());
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $post->getId(),
|
||||||
|
'userId' => $post->getUserId(),
|
||||||
|
'authorDisplayName' => $author === null
|
||||||
|
? ''
|
||||||
|
: $author->getDisplayName(),
|
||||||
|
'title' => $post->getTitle(),
|
||||||
|
'body' => $post->getBody(),
|
||||||
|
'createdAt' => $post->getCreatedAt()->format(DATE_ATOM),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ namespace App\Providers;
|
||||||
|
|
||||||
use App\Auth\EloquentSessionRepository;
|
use App\Auth\EloquentSessionRepository;
|
||||||
use App\Auth\SessionRepository;
|
use App\Auth\SessionRepository;
|
||||||
|
use App\Comment\CommentRepository;
|
||||||
|
use App\Comment\EloquentCommentRepository;
|
||||||
use App\Email\EmailConfirmationToken\EloquentEmailConfirmationTokenRepository;
|
use App\Email\EmailConfirmationToken\EloquentEmailConfirmationTokenRepository;
|
||||||
use App\Email\EmailConfirmationToken\EmailConfirmationTokenRepository;
|
use App\Email\EmailConfirmationToken\EmailConfirmationTokenRepository;
|
||||||
use App\Post\EloquentPostRepository;
|
use App\Post\EloquentPostRepository;
|
||||||
|
|
@ -32,5 +34,9 @@ class RepositoryServiceProvider extends ServiceProvider
|
||||||
PostRepository::class,
|
PostRepository::class,
|
||||||
EloquentPostRepository::class,
|
EloquentPostRepository::class,
|
||||||
);
|
);
|
||||||
|
$this->app->bind(
|
||||||
|
CommentRepository::class,
|
||||||
|
EloquentCommentRepository::class,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
bootstrap="vendor/autoload.php"
|
bootstrap="vendor/autoload.php"
|
||||||
colors="true"
|
colors="true"
|
||||||
enforceTimeLimit="true"
|
enforceTimeLimit="true"
|
||||||
|
defaultTimeLimit="30"
|
||||||
>
|
>
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="Unit">
|
<testsuite name="Unit">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Controllers\AuthController;
|
use App\Controllers\AuthController;
|
||||||
|
use App\Controllers\CommentController;
|
||||||
|
use App\Controllers\PostController;
|
||||||
use App\Http\Middleware\AuthMiddleware;
|
use App\Http\Middleware\AuthMiddleware;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
|
@ -15,3 +17,32 @@ Route::post('/logout', [AuthController::class, 'logout'])
|
||||||
->middleware(AuthMiddleware::class);
|
->middleware(AuthMiddleware::class);
|
||||||
Route::get('/me', [AuthController::class, 'me'])
|
Route::get('/me', [AuthController::class, 'me'])
|
||||||
->middleware(AuthMiddleware::class);
|
->middleware(AuthMiddleware::class);
|
||||||
|
|
||||||
|
Route::get('/posts', [PostController::class, 'recent']);
|
||||||
|
Route::get('/posts/{id}', [PostController::class, 'show'])
|
||||||
|
->whereNumber('id');
|
||||||
|
Route::post('/posts', [PostController::class, 'create'])
|
||||||
|
->middleware(AuthMiddleware::class);
|
||||||
|
Route::delete('/posts/{id}', [PostController::class, 'delete'])
|
||||||
|
->whereNumber('id')
|
||||||
|
->middleware(AuthMiddleware::class);
|
||||||
|
|
||||||
|
Route::get(
|
||||||
|
'/users/{displayName}/posts',
|
||||||
|
[PostController::class, 'listByUser'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Route::get(
|
||||||
|
'/posts/{postId}/comments',
|
||||||
|
[CommentController::class, 'listForPost'],
|
||||||
|
)->whereNumber('postId');
|
||||||
|
Route::post(
|
||||||
|
'/posts/{postId}/comments',
|
||||||
|
[CommentController::class, 'create'],
|
||||||
|
)->whereNumber('postId')
|
||||||
|
->middleware(AuthMiddleware::class);
|
||||||
|
Route::delete(
|
||||||
|
'/comments/{id}',
|
||||||
|
[CommentController::class, 'delete'],
|
||||||
|
)->whereNumber('id')
|
||||||
|
->middleware(AuthMiddleware::class);
|
||||||
|
|
|
||||||
72
backend/tests/Feature/AuthenticatesUsers.php
Normal file
72
backend/tests/Feature/AuthenticatesUsers.php
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Email\EmailConfirmationToken\EmailConfirmationTokenRepository;
|
||||||
|
use App\Shared\ValueObject\EmailAddress;
|
||||||
|
use App\User\User;
|
||||||
|
use App\User\UserRepository;
|
||||||
|
|
||||||
|
trait AuthenticatesUsers
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array{user: User, cookie: string}
|
||||||
|
*/
|
||||||
|
private function signupAndLogin(
|
||||||
|
string $email,
|
||||||
|
string $displayName,
|
||||||
|
string $password,
|
||||||
|
): array {
|
||||||
|
$this->postJson('/api/signup', [
|
||||||
|
'email' => $email,
|
||||||
|
'displayName' => $displayName,
|
||||||
|
])->assertStatus(201);
|
||||||
|
|
||||||
|
$userRepo = $this->app->make(UserRepository::class);
|
||||||
|
$user = $userRepo->findByEmail(new EmailAddress($email));
|
||||||
|
$tokenRepo = $this->app->make(
|
||||||
|
EmailConfirmationTokenRepository::class,
|
||||||
|
);
|
||||||
|
$token = $tokenRepo->findByUser($user);
|
||||||
|
|
||||||
|
$this->postJson('/api/confirm-email', [
|
||||||
|
'token' => $token->getToken(),
|
||||||
|
'password' => $password,
|
||||||
|
])->assertStatus(200);
|
||||||
|
|
||||||
|
$loginResponse = $this->postJson('/api/login', [
|
||||||
|
'email' => $email,
|
||||||
|
'password' => $password,
|
||||||
|
]);
|
||||||
|
$loginResponse->assertStatus(200);
|
||||||
|
$cookie = $loginResponse->getCookie('auth_token', false);
|
||||||
|
|
||||||
|
$reloaded = $userRepo->findByEmail(new EmailAddress($email));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'user' => $reloaded,
|
||||||
|
'cookie' => $cookie->getValue(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resetClientState(): void
|
||||||
|
{
|
||||||
|
$this->defaultCookies = [];
|
||||||
|
$this->unencryptedCookies = [];
|
||||||
|
$this->withCredentials = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function promoteToAdmin(int $userId): void
|
||||||
|
{
|
||||||
|
$userRepo = $this->app->make(UserRepository::class);
|
||||||
|
$user = $userRepo->find($userId);
|
||||||
|
$userRepo->update(new User(
|
||||||
|
id: $user->getId(),
|
||||||
|
email: $user->getEmail(),
|
||||||
|
displayName: $user->getDisplayName(),
|
||||||
|
passwordHash: $user->getPasswordHash(),
|
||||||
|
isAdmin: true,
|
||||||
|
emailConfirmedAt: $user->getEmailConfirmedAt(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
172
backend/tests/Feature/Comment/CommentFlowTest.php
Normal file
172
backend/tests/Feature/Comment/CommentFlowTest.php
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Comment;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\Feature\AuthenticatesUsers;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class CommentFlowTest extends TestCase
|
||||||
|
{
|
||||||
|
use AuthenticatesUsers;
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private function createPost(string $cookie, string $title): int
|
||||||
|
{
|
||||||
|
$response = $this->withCredentials()->withUnencryptedCookie('auth_token', $cookie)
|
||||||
|
->postJson('/api/posts', [
|
||||||
|
'title' => $title,
|
||||||
|
'body' => 'b',
|
||||||
|
]);
|
||||||
|
$response->assertStatus(201);
|
||||||
|
|
||||||
|
return $response->json('post.id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_anonymous_can_list_comments(): void
|
||||||
|
{
|
||||||
|
$alice = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$postId = $this->createPost($alice['cookie'], 'P1');
|
||||||
|
|
||||||
|
$response = $this->getJson("/api/posts/{$postId}/comments");
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('comments', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_authenticated_user_creates_comment(): void
|
||||||
|
{
|
||||||
|
$alice = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$postId = $this->createPost($alice['cookie'], 'P1');
|
||||||
|
|
||||||
|
$bob = $this->signupAndLogin(
|
||||||
|
email: 'bob@example.com',
|
||||||
|
displayName: 'bob',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$response = $this->withCredentials()->withUnencryptedCookie('auth_token', $bob['cookie'])
|
||||||
|
->postJson("/api/posts/{$postId}/comments", [
|
||||||
|
'body' => 'nice post',
|
||||||
|
]);
|
||||||
|
$response->assertStatus(201);
|
||||||
|
$response->assertJsonPath('comment.body', 'nice post');
|
||||||
|
$response->assertJsonPath('comment.authorDisplayName', 'bob');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_anonymous_cannot_create_comment(): void
|
||||||
|
{
|
||||||
|
$alice = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$postId = $this->createPost($alice['cookie'], 'P1');
|
||||||
|
|
||||||
|
$this->resetClientState();
|
||||||
|
$this->postJson("/api/posts/{$postId}/comments", [
|
||||||
|
'body' => 'hi',
|
||||||
|
])->assertStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_on_missing_post_returns_404(): void
|
||||||
|
{
|
||||||
|
$alice = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$this->withCredentials()->withUnencryptedCookie('auth_token', $alice['cookie'])
|
||||||
|
->postJson('/api/posts/9999/comments', [
|
||||||
|
'body' => 'hi',
|
||||||
|
])->assertStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_author_deletes_own_comment(): void
|
||||||
|
{
|
||||||
|
$alice = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$postId = $this->createPost($alice['cookie'], 'P1');
|
||||||
|
|
||||||
|
$bob = $this->signupAndLogin(
|
||||||
|
email: 'bob@example.com',
|
||||||
|
displayName: 'bob',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$createResponse = $this->withCredentials()->withUnencryptedCookie('auth_token', $bob['cookie'])
|
||||||
|
->postJson("/api/posts/{$postId}/comments", [
|
||||||
|
'body' => 'nice',
|
||||||
|
]);
|
||||||
|
$commentId = $createResponse->json('comment.id');
|
||||||
|
|
||||||
|
$this->withCredentials()->withUnencryptedCookie('auth_token', $bob['cookie'])
|
||||||
|
->deleteJson("/api/comments/{$commentId}")
|
||||||
|
->assertStatus(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_other_user_cannot_delete_comment(): void
|
||||||
|
{
|
||||||
|
$alice = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$postId = $this->createPost($alice['cookie'], 'P1');
|
||||||
|
|
||||||
|
$bob = $this->signupAndLogin(
|
||||||
|
email: 'bob@example.com',
|
||||||
|
displayName: 'bob',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$createResponse = $this->withCredentials()->withUnencryptedCookie('auth_token', $bob['cookie'])
|
||||||
|
->postJson("/api/posts/{$postId}/comments", [
|
||||||
|
'body' => 'nice',
|
||||||
|
]);
|
||||||
|
$commentId = $createResponse->json('comment.id');
|
||||||
|
|
||||||
|
$this->withCredentials()->withUnencryptedCookie('auth_token', $alice['cookie'])
|
||||||
|
->deleteJson("/api/comments/{$commentId}")
|
||||||
|
->assertStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_admin_deletes_any_comment(): void
|
||||||
|
{
|
||||||
|
$alice = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$postId = $this->createPost($alice['cookie'], 'P1');
|
||||||
|
|
||||||
|
$bob = $this->signupAndLogin(
|
||||||
|
email: 'bob@example.com',
|
||||||
|
displayName: 'bob',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$createResponse = $this->withCredentials()->withUnencryptedCookie('auth_token', $bob['cookie'])
|
||||||
|
->postJson("/api/posts/{$postId}/comments", [
|
||||||
|
'body' => 'nice',
|
||||||
|
]);
|
||||||
|
$commentId = $createResponse->json('comment.id');
|
||||||
|
|
||||||
|
$this->promoteToAdmin($alice['user']->getId());
|
||||||
|
$loginResponse = $this->postJson('/api/login', [
|
||||||
|
'email' => 'alice@example.com',
|
||||||
|
'password' => 'longenoughpassword',
|
||||||
|
]);
|
||||||
|
$aliceCookie = $loginResponse->getCookie('auth_token', false);
|
||||||
|
|
||||||
|
$this->withCredentials()->withUnencryptedCookie('auth_token', $aliceCookie->getValue())
|
||||||
|
->deleteJson("/api/comments/{$commentId}")
|
||||||
|
->assertStatus(204);
|
||||||
|
}
|
||||||
|
}
|
||||||
156
backend/tests/Feature/Post/PostFlowTest.php
Normal file
156
backend/tests/Feature/Post/PostFlowTest.php
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Post;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\Feature\AuthenticatesUsers;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class PostFlowTest extends TestCase
|
||||||
|
{
|
||||||
|
use AuthenticatesUsers;
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_authenticated_user_creates_post(): void
|
||||||
|
{
|
||||||
|
$session = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->withCredentials()->withUnencryptedCookie('auth_token', $session['cookie'])
|
||||||
|
->postJson('/api/posts', [
|
||||||
|
'title' => 'My Post',
|
||||||
|
'body' => 'Hello world',
|
||||||
|
]);
|
||||||
|
$response->assertStatus(201);
|
||||||
|
$response->assertJsonPath('post.title', 'My Post');
|
||||||
|
$response->assertJsonPath('post.authorDisplayName', 'alice');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_anonymous_create_post_returns_401(): void
|
||||||
|
{
|
||||||
|
$this->postJson('/api/posts', [
|
||||||
|
'title' => 'My Post',
|
||||||
|
'body' => 'Hello world',
|
||||||
|
])->assertStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_recent_posts_are_public(): void
|
||||||
|
{
|
||||||
|
$session = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$this->withCredentials()->withUnencryptedCookie('auth_token', $session['cookie'])
|
||||||
|
->postJson('/api/posts', [
|
||||||
|
'title' => 'P1',
|
||||||
|
'body' => 'B1',
|
||||||
|
])->assertStatus(201);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/posts');
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('posts.0.title', 'P1');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_show_returns_404_when_missing(): void
|
||||||
|
{
|
||||||
|
$this->getJson('/api/posts/9999')->assertStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_user_posts_listed_by_display_name(): void
|
||||||
|
{
|
||||||
|
$session = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$this->withCredentials()->withUnencryptedCookie('auth_token', $session['cookie'])
|
||||||
|
->postJson('/api/posts', [
|
||||||
|
'title' => 'A1',
|
||||||
|
'body' => 'b',
|
||||||
|
])->assertStatus(201);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/users/alice/posts');
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('user.displayName', 'alice');
|
||||||
|
$response->assertJsonPath('posts.0.title', 'A1');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_other_user_cannot_delete_post(): void
|
||||||
|
{
|
||||||
|
$alice = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$createResponse = $this->withCredentials()->withUnencryptedCookie('auth_token', $alice['cookie'])
|
||||||
|
->postJson('/api/posts', [
|
||||||
|
'title' => 'A1',
|
||||||
|
'body' => 'b',
|
||||||
|
]);
|
||||||
|
$postId = $createResponse->json('post.id');
|
||||||
|
|
||||||
|
$bob = $this->signupAndLogin(
|
||||||
|
email: 'bob@example.com',
|
||||||
|
displayName: 'bob',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$this->withCredentials()->withUnencryptedCookie('auth_token', $bob['cookie'])
|
||||||
|
->deleteJson("/api/posts/{$postId}")
|
||||||
|
->assertStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_author_deletes_own_post(): void
|
||||||
|
{
|
||||||
|
$alice = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$createResponse = $this->withCredentials()->withUnencryptedCookie('auth_token', $alice['cookie'])
|
||||||
|
->postJson('/api/posts', [
|
||||||
|
'title' => 'A1',
|
||||||
|
'body' => 'b',
|
||||||
|
]);
|
||||||
|
$postId = $createResponse->json('post.id');
|
||||||
|
|
||||||
|
$this->withCredentials()->withUnencryptedCookie('auth_token', $alice['cookie'])
|
||||||
|
->deleteJson("/api/posts/{$postId}")
|
||||||
|
->assertStatus(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_admin_deletes_anyones_post(): void
|
||||||
|
{
|
||||||
|
$alice = $this->signupAndLogin(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
displayName: 'alice',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$createResponse = $this->withCredentials()->withUnencryptedCookie('auth_token', $alice['cookie'])
|
||||||
|
->postJson('/api/posts', [
|
||||||
|
'title' => 'A1',
|
||||||
|
'body' => 'b',
|
||||||
|
]);
|
||||||
|
$postId = $createResponse->json('post.id');
|
||||||
|
|
||||||
|
$bob = $this->signupAndLogin(
|
||||||
|
email: 'bob@example.com',
|
||||||
|
displayName: 'bob',
|
||||||
|
password: 'longenoughpassword',
|
||||||
|
);
|
||||||
|
$this->promoteToAdmin($bob['user']->getId());
|
||||||
|
// Re-login bob to get a fresh cookie/payload
|
||||||
|
$loginResponse = $this->postJson('/api/login', [
|
||||||
|
'email' => 'bob@example.com',
|
||||||
|
'password' => 'longenoughpassword',
|
||||||
|
]);
|
||||||
|
$bobCookie = $loginResponse->getCookie('auth_token', false);
|
||||||
|
|
||||||
|
$this->withCredentials()->withUnencryptedCookie('auth_token', $bobCookie->getValue())
|
||||||
|
->deleteJson("/api/posts/{$postId}")
|
||||||
|
->assertStatus(204);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue