diff --git a/backend/phpunit.xml b/backend/phpunit.xml index 8c6eb1b..ca91dd2 100644 --- a/backend/phpunit.xml +++ b/backend/phpunit.xml @@ -4,6 +4,7 @@ bootstrap="vendor/autoload.php" colors="true" enforceTimeLimit="true" + defaultTimeLimit="30" > diff --git a/backend/tests/Feature/AuthenticatesUsers.php b/backend/tests/Feature/AuthenticatesUsers.php new file mode 100644 index 0000000..1a74ad1 --- /dev/null +++ b/backend/tests/Feature/AuthenticatesUsers.php @@ -0,0 +1,72 @@ +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(), + )); + } +} diff --git a/backend/tests/Feature/Comment/CommentFlowTest.php b/backend/tests/Feature/Comment/CommentFlowTest.php new file mode 100644 index 0000000..cc5a37e --- /dev/null +++ b/backend/tests/Feature/Comment/CommentFlowTest.php @@ -0,0 +1,172 @@ +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); + } +} diff --git a/backend/tests/Feature/Post/PostFlowTest.php b/backend/tests/Feature/Post/PostFlowTest.php new file mode 100644 index 0000000..7d422fa --- /dev/null +++ b/backend/tests/Feature/Post/PostFlowTest.php @@ -0,0 +1,156 @@ +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); + } +}