diff --git a/realworld/accounts/tests.py b/realworld/accounts/tests.py index 073e947..b8c394d 100644 --- a/realworld/accounts/tests.py +++ b/realworld/accounts/tests.py @@ -3,6 +3,8 @@ from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse, reverse_lazy +from realworld.articles.models import Article +from realworld.comments.models import Comment from .forms import UserCreationForm @@ -133,3 +135,371 @@ def test_exists(self): response = self.client.get(self.url, {"email": "tester@gmail.com"}) self.assertEqual(response.status_code, http.HTTPStatus.OK) self.assertContains(response, "This email is in use") + + +class TestSettingsView(TestCase): + password = "testpass" + url = reverse_lazy("settings") + + @classmethod + def setUpTestData(cls): + cls.user = User( + email="tester@gmail.com", + name="tester", + bio="Original bio", + ) + cls.user.set_password(cls.password) + cls.user.save() + + def setUp(self): + self.client.force_login(self.user) + + def test_get(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertContains(response, "tester") + self.assertContains(response, "Original bio") + + def test_get_not_authenticated(self): + self.client.logout() + response = self.client.get(self.url) + self.assertEqual(response.status_code, http.HTTPStatus.FOUND) + + def test_post_valid(self): + response = self.client.post(self.url, { + "name": "Updated Name", + "email": "updated@gmail.com", + "bio": "Updated bio", + "image": "", + }) + + self.assertEqual(response.headers["HX-Redirect"], self.user.get_absolute_url()) + self.user.refresh_from_db() + self.assertEqual(self.user.name, "Updated Name") + self.assertEqual(self.user.email, "updated@gmail.com") + self.assertEqual(self.user.bio, "Updated bio") + + def test_post_invalid_email(self): + response = self.client.post(self.url, { + "name": "Updated Name", + "email": "invalid-email", + "bio": "Updated bio", + "image": "", + }) + + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertContains(response, "Enter a valid email address") + self.user.refresh_from_db() + self.assertEqual(self.user.email, "tester@gmail.com") + + +class TestLoginEdgeCases(TestCase): + password = "testpass" + + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user( + email="tester@gmail.com", + name="tester", + password=cls.password + ) + + def test_login_invalid_credentials(self): + response = self.client.post(reverse("login"), { + "username": "tester@gmail.com", + "password": "wrongpassword" + }) + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertContains(response, "Please enter a correct") + + def test_login_nonexistent_user(self): + response = self.client.post(reverse("login"), { + "username": "nonexistent@gmail.com", + "password": "testpass" + }) + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertContains(response, "Please enter a correct") + + def test_login_empty_credentials(self): + response = self.client.post(reverse("login"), { + "username": "", + "password": "" + }) + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertContains(response, "This field is required") + + +class TestProfileView(TestCase): + password = "testpass" + + @classmethod + def setUpTestData(cls): + cls.user = User( + email="tester@gmail.com", + name="tester", + bio="Test bio", + ) + cls.user.set_password(cls.password) + cls.user.save() + + cls.other_user = User( + email="other@gmail.com", + name="other", + ) + cls.other_user.set_password(cls.password) + cls.other_user.save() + + cls.article = Article.objects.create( + title="Test Article", + summary="Test summary", + content="Test content", + author=cls.user, + ) + + cls.url = reverse("profile", args=[cls.user.id]) + + def test_get_own_profile(self): + self.client.force_login(self.user) + response = self.client.get(self.url) + + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertEqual(response.context["profile"], self.user) + self.assertContains(response, "tester") + self.assertContains(response, "Test bio") + + def test_get_other_profile(self): + self.client.force_login(self.other_user) + response = self.client.get(self.url) + + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertEqual(response.context["profile"], self.user) + self.assertContains(response, "tester") + + def test_get_profile_anonymous(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertEqual(response.context["profile"], self.user) + + def test_get_nonexistent_profile(self): + url = reverse("profile", args=[99999]) + response = self.client.get(url) + self.assertEqual(response.status_code, http.HTTPStatus.NOT_FOUND) + + +class TestHTMXIntegration(TestCase): + password = "testpass" + + @classmethod + def setUpTestData(cls): + cls.author = User( + email="author@gmail.com", + name="author", + ) + cls.author.set_password(cls.password) + cls.author.save() + + cls.user = User( + email="user@gmail.com", + name="user", + ) + cls.user.set_password(cls.password) + cls.user.save() + + cls.article = Article.objects.create( + title="Test Article", + summary="Test summary", + content="Test content", + author=cls.author, + ) + + def test_favorite_htmx_target_specific(self): + self.client.force_login(self.user) + url = reverse("favorite", args=[self.article.id]) + + response = self.client.post( + url, + HTTP_HX_TARGET=f"favorite-{self.article.id}" + ) + + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertTrue(response.context["is_favorite"]) + self.assertFalse(response.context["is_detail"]) + + def test_favorite_htmx_out_of_band(self): + self.client.force_login(self.user) + url = reverse("favorite", args=[self.article.id]) + + response = self.client.post(url) + + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertTrue(response.context["is_favorite"]) + self.assertTrue(response.context["is_detail"]) + + def test_register_htmx_redirect(self): + response = self.client.post(reverse("register"), { + "name": "New User", + "email": "newuser@gmail.com", + "password": "testpass1", + }) + + self.assertEqual(response.headers["HX-Redirect"], reverse("home")) + + def test_article_creation_htmx_redirect(self): + self.client.force_login(self.user) + response = self.client.post(reverse("create_article"), { + "title": "HTMX Test Article", + "summary": "test summary", + "content": "test content", + "tags": "htmx test" + }) + + article = Article.objects.get(title="HTMX Test Article") + self.assertEqual(response.headers["HX-Redirect"], article.get_absolute_url()) + + +class TestIntegrationWorkflows(TestCase): + password = "testpass" + + def test_complete_user_journey(self): + register_response = self.client.post(reverse("register"), { + "name": "Journey User", + "email": "journey@gmail.com", + "password": "testpass1", + }) + self.assertEqual(register_response.headers["HX-Redirect"], reverse("home")) + + user = User.objects.get(email="journey@gmail.com") + self.client.force_login(user) + + create_response = self.client.post(reverse("create_article"), { + "title": "My First Article", + "summary": "This is my first article", + "content": "# Hello World\n\nThis is my first article content.", + "tags": "first blog" + }) + + article = Article.objects.get(title="My First Article") + self.assertEqual(create_response.headers["HX-Redirect"], article.get_absolute_url()) + + other_user = User.objects.create_user( + email="commenter@gmail.com", + name="Commenter", + password="testpass1" + ) + self.client.force_login(other_user) + + comment_response = self.client.post( + reverse("add_comment", args=[article.id]), + {"content": "Great article!"} + ) + self.assertEqual(comment_response.status_code, http.HTTPStatus.OK) + + comment = Comment.objects.get(content="Great article!") + self.assertEqual(comment.author, other_user) + self.assertEqual(comment.article, article) + + self.client.force_login(user) + edit_response = self.client.post( + reverse("edit_article", args=[article.id]), + { + "title": "My Updated First Article", + "summary": "This is my updated first article", + "content": "# Hello Updated World\n\nThis is my updated article content.", + "tags": "first blog updated" + } + ) + + article.refresh_from_db() + self.assertEqual(article.title, "My Updated First Article") + self.assertEqual(edit_response.headers["HX-Redirect"], article.get_absolute_url()) + + def test_permission_edge_cases(self): + author = User.objects.create_user( + email="author@gmail.com", + name="Author", + password="testpass1" + ) + + unauthorized_user = User.objects.create_user( + email="unauthorized@gmail.com", + name="Unauthorized", + password="testpass1" + ) + + article = Article.objects.create( + title="Protected Article", + summary="Protected summary", + content="Protected content", + author=author, + ) + + comment = Comment.objects.create( + content="Protected comment", + author=author, + article=article, + ) + + self.client.force_login(unauthorized_user) + + edit_article_response = self.client.get(reverse("edit_article", args=[article.id])) + self.assertEqual(edit_article_response.status_code, http.HTTPStatus.NOT_FOUND) + + delete_article_response = self.client.delete(reverse("delete_article", args=[article.id])) + self.assertEqual(delete_article_response.status_code, http.HTTPStatus.NOT_FOUND) + + edit_comment_response = self.client.get(reverse("edit_comment", args=[comment.id])) + self.assertEqual(edit_comment_response.status_code, http.HTTPStatus.NOT_FOUND) + + delete_comment_response = self.client.delete(reverse("delete_comment", args=[comment.id])) + self.assertEqual(delete_comment_response.status_code, http.HTTPStatus.NOT_FOUND) + + favorite_own_article_response = self.client.post(reverse("favorite", args=[article.id])) + self.assertEqual(favorite_own_article_response.status_code, http.HTTPStatus.OK) + + def test_cross_model_interactions(self): + user1 = User.objects.create_user( + email="user1@gmail.com", + name="User One", + password="testpass1" + ) + + user2 = User.objects.create_user( + email="user2@gmail.com", + name="User Two", + password="testpass1" + ) + + article = Article.objects.create( + title="Interactive Article", + summary="Test summary", + content="Test content", + author=user1, + ) + + self.client.force_login(user2) + + follow_response = self.client.post(reverse("follow", args=[user1.id])) + self.assertEqual(follow_response.status_code, http.HTTPStatus.OK) + self.assertTrue(user1.followers.filter(pk=user2.id).exists()) + + favorite_response = self.client.post(reverse("favorite", args=[article.id])) + self.assertEqual(favorite_response.status_code, http.HTTPStatus.OK) + self.assertTrue(article.favorites.filter(pk=user2.id).exists()) + + comment_response = self.client.post( + reverse("add_comment", args=[article.id]), + {"content": "Great work from someone I follow!"} + ) + self.assertEqual(comment_response.status_code, http.HTTPStatus.OK) + + comment = Comment.objects.get(content="Great work from someone I follow!") + self.assertEqual(comment.author, user2) + self.assertEqual(comment.article, article) + + unfollow_response = self.client.delete(reverse("follow", args=[user1.id])) + self.assertEqual(unfollow_response.status_code, http.HTTPStatus.OK) + self.assertFalse(user1.followers.filter(pk=user2.id).exists()) + + unfavorite_response = self.client.delete(reverse("favorite", args=[article.id])) + self.assertEqual(unfavorite_response.status_code, http.HTTPStatus.OK) + self.assertFalse(article.favorites.filter(pk=user2.id).exists()) diff --git a/realworld/articles/tests.py b/realworld/articles/tests.py index d72163e..d2e01fa 100644 --- a/realworld/articles/tests.py +++ b/realworld/articles/tests.py @@ -99,6 +99,39 @@ def test_post_invalid(self): response = self.client.post(self.url, {}) self.assertEqual(response.status_code, http.HTTPStatus.OK) + def test_post_invalid_missing_title(self): + response = self.client.post(self.url, { + "summary": "test summary", + "content": "test content", + "tags": "python" + }) + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertContains(response, "This field is required") + + def test_post_valid_missing_content(self): + response = self.client.post(self.url, { + "title": "Test Title", + "summary": "test summary", + "tags": "python" + }) + self.assertEqual(response.status_code, http.HTTPStatus.FOUND) + + article = Article.objects.get(title="Test Title") + self.assertEqual(article.author, self.author) + self.assertEqual(article.summary, "test summary") + self.assertEqual(article.content, "") + + def test_post_invalid_title_too_long(self): + long_title = "x" * 121 + response = self.client.post(self.url, { + "title": long_title, + "summary": "test summary", + "content": "test content", + "tags": "python" + }) + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertContains(response, "Ensure this value has at most 120 characters") + def test_post_valid(self): response = self.client.post( self.url, @@ -231,6 +264,162 @@ def test_remove_favorite(self): self.assertTrue(response.context["is_detail"]) +class TestEditArticleView(TestCase): + password = "testpass" + + @classmethod + def setUpTestData(cls): + cls.author = User( + email="tester@gmail.com", + name="tester", + ) + cls.author.set_password(cls.password) + cls.author.save() + + cls.other_user = User( + email="other@gmail.com", + name="other", + ) + cls.other_user.set_password(cls.password) + cls.other_user.save() + + cls.article = Article.objects.create( + title="Test Article", + summary="Test summary", + content="Test content", + author=cls.author, + ) + cls.article.tags.add("python", "django") + + cls.url = reverse("edit_article", args=[cls.article.id]) + + def setUp(self): + self.client.force_login(self.author) + + def test_get(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertEqual(response.context["article"], self.article) + form = response.context["form"] + self.assertEqual(form.instance, self.article) + self.assertEqual(form.initial["title"], "Test Article") + + def test_get_permission_denied(self): + self.client.force_login(self.other_user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, http.HTTPStatus.NOT_FOUND) + + def test_get_not_authenticated(self): + self.client.logout() + response = self.client.get(self.url) + self.assertEqual(response.status_code, http.HTTPStatus.FOUND) + + def test_get_nonexistent_article(self): + url = reverse("edit_article", args=[99999]) + response = self.client.get(url) + self.assertEqual(response.status_code, http.HTTPStatus.NOT_FOUND) + + def test_post_valid(self): + response = self.client.post(self.url, { + "title": "Updated Title", + "summary": "Updated summary", + "content": "Updated content", + "tags": "python django updated" + }) + + self.article.refresh_from_db() + self.assertEqual(response.headers["HX-Redirect"], self.article.get_absolute_url()) + self.assertEqual(self.article.title, "Updated Title") + self.assertEqual(self.article.summary, "Updated summary") + self.assertEqual(self.article.content, "Updated content") + self.assertEqual(set(self.article.tags.names()), {"python", "django", "updated"}) + + def test_post_invalid(self): + response = self.client.post(self.url, { + "title": "", + "summary": "Updated summary", + "content": "Updated content", + "tags": "python" + }) + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertContains(response, "This field is required") + + self.article.refresh_from_db() + self.assertEqual(self.article.title, "Test Article") + + def test_post_permission_denied(self): + self.client.force_login(self.other_user) + response = self.client.post(self.url, { + "title": "Hacked Title", + "summary": "Hacked summary", + "content": "Hacked content", + "tags": "hacked" + }) + self.assertEqual(response.status_code, http.HTTPStatus.NOT_FOUND) + + self.article.refresh_from_db() + self.assertEqual(self.article.title, "Test Article") + + +class TestDeleteArticleView(TestCase): + password = "testpass" + + @classmethod + def setUpTestData(cls): + cls.author = User( + email="tester@gmail.com", + name="tester", + ) + cls.author.set_password(cls.password) + cls.author.save() + + cls.other_user = User( + email="other@gmail.com", + name="other", + ) + cls.other_user.set_password(cls.password) + cls.other_user.save() + + cls.article = Article.objects.create( + title="Test Article", + summary="Test summary", + content="Test content", + author=cls.author, + ) + + cls.url = reverse("delete_article", args=[cls.article.id]) + + def setUp(self): + self.client.force_login(self.author) + + def test_delete_success(self): + article_id = self.article.id + response = self.client.delete(self.url) + + self.assertEqual(response.status_code, http.HTTPStatus.FOUND) + self.assertEqual(response.url, reverse("home")) + self.assertFalse(Article.objects.filter(id=article_id).exists()) + + def test_delete_permission_denied(self): + self.client.force_login(self.other_user) + response = self.client.delete(self.url) + + self.assertEqual(response.status_code, http.HTTPStatus.NOT_FOUND) + self.assertTrue(Article.objects.filter(id=self.article.id).exists()) + + def test_delete_not_authenticated(self): + self.client.logout() + response = self.client.delete(self.url) + + self.assertEqual(response.status_code, http.HTTPStatus.FOUND) + self.assertTrue(Article.objects.filter(id=self.article.id).exists()) + + def test_delete_nonexistent_article(self): + url = reverse("delete_article", args=[99999]) + response = self.client.delete(url) + self.assertEqual(response.status_code, http.HTTPStatus.NOT_FOUND) + + class TestTagsAutocomplete(TestCase): url = reverse_lazy("tags_autocomplete") diff --git a/realworld/comments/tests.py b/realworld/comments/tests.py index e477576..802fa46 100644 --- a/realworld/comments/tests.py +++ b/realworld/comments/tests.py @@ -40,3 +40,205 @@ def test_add_comment(self): self.assertEqual(comment.article, self.article) self.assertEqual(comment.author, self.author) + + def test_add_comment_invalid(self): + self.client.force_login(self.author) + + response = self.client.post(self.url, {"content": ""}) + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertContains(response, "content") + self.assertEqual(Comment.objects.count(), 0) + + def test_add_comment_not_authenticated(self): + response = self.client.post(self.url, {"content": "test"}) + self.assertEqual(response.status_code, http.HTTPStatus.FOUND) + self.assertEqual(Comment.objects.count(), 0) + + +class TestCommentModel(TestCase): + @classmethod + def setUpTestData(cls): + cls.author = User.objects.create( + email="tester@gmail.com", + name="tester", + password="testpass" + ) + + cls.article = Article.objects.create( + title="test", + summary="test", + content="test", + author=cls.author, + ) + + cls.comment = Comment.objects.create( + content="Test comment content", + author=cls.author, + article=cls.article, + ) + + def test_comment_creation(self): + self.assertEqual(self.comment.content, "Test comment content") + self.assertEqual(self.comment.author, self.author) + self.assertEqual(self.comment.article, self.article) + self.assertIsNotNone(self.comment.created) + self.assertIsNotNone(self.comment.updated) + + def test_comment_relationships(self): + self.assertEqual(self.comment.author.email, "tester@gmail.com") + self.assertEqual(self.comment.article.title, "test") + + def test_comment_timestamps(self): + original_updated = self.comment.updated + self.comment.content = "Updated content" + self.comment.save() + self.comment.refresh_from_db() + self.assertGreater(self.comment.updated, original_updated) + + +class TestEditCommentView(TestCase): + password = "testpass" + + @classmethod + def setUpTestData(cls): + cls.author = User( + email="tester@gmail.com", + name="tester", + ) + cls.author.set_password(cls.password) + cls.author.save() + + cls.other_user = User( + email="other@gmail.com", + name="other", + ) + cls.other_user.set_password(cls.password) + cls.other_user.save() + + cls.article = Article.objects.create( + title="test", + summary="test", + content="test", + author=cls.author, + ) + + cls.comment = Comment.objects.create( + content="Original comment", + author=cls.author, + article=cls.article, + ) + + cls.url = reverse("edit_comment", args=[cls.comment.id]) + + def setUp(self): + self.client.force_login(self.author) + + def test_get(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertEqual(response.context["comment"], self.comment) + form = response.context["form"] + self.assertEqual(form.instance, self.comment) + + def test_get_permission_denied(self): + self.client.force_login(self.other_user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, http.HTTPStatus.NOT_FOUND) + + def test_get_not_authenticated(self): + self.client.logout() + response = self.client.get(self.url) + self.assertEqual(response.status_code, http.HTTPStatus.FOUND) + + def test_post_valid(self): + response = self.client.post(self.url, { + "content": "Updated comment content" + }) + + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.comment.refresh_from_db() + self.assertEqual(self.comment.content, "Updated comment content") + + def test_post_invalid(self): + response = self.client.post(self.url, { + "content": "" + }) + + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertContains(response, "content") + self.comment.refresh_from_db() + self.assertEqual(self.comment.content, "Original comment") + + def test_post_permission_denied(self): + self.client.force_login(self.other_user) + response = self.client.post(self.url, { + "content": "Hacked content" + }) + + self.assertEqual(response.status_code, http.HTTPStatus.NOT_FOUND) + self.comment.refresh_from_db() + self.assertEqual(self.comment.content, "Original comment") + + +class TestDeleteCommentView(TestCase): + password = "testpass" + + @classmethod + def setUpTestData(cls): + cls.author = User( + email="tester@gmail.com", + name="tester", + ) + cls.author.set_password(cls.password) + cls.author.save() + + cls.other_user = User( + email="other@gmail.com", + name="other", + ) + cls.other_user.set_password(cls.password) + cls.other_user.save() + + cls.article = Article.objects.create( + title="test", + summary="test", + content="test", + author=cls.author, + ) + + cls.comment = Comment.objects.create( + content="Test comment", + author=cls.author, + article=cls.article, + ) + + cls.url = reverse("delete_comment", args=[cls.comment.id]) + + def setUp(self): + self.client.force_login(self.author) + + def test_delete_success(self): + comment_id = self.comment.id + response = self.client.delete(self.url) + + self.assertEqual(response.status_code, http.HTTPStatus.OK) + self.assertFalse(Comment.objects.filter(id=comment_id).exists()) + + def test_delete_permission_denied(self): + self.client.force_login(self.other_user) + response = self.client.delete(self.url) + + self.assertEqual(response.status_code, http.HTTPStatus.NOT_FOUND) + self.assertTrue(Comment.objects.filter(id=self.comment.id).exists()) + + def test_delete_not_authenticated(self): + self.client.logout() + response = self.client.delete(self.url) + + self.assertEqual(response.status_code, http.HTTPStatus.FOUND) + self.assertTrue(Comment.objects.filter(id=self.comment.id).exists()) + + def test_delete_nonexistent_comment(self): + url = reverse("delete_comment", args=[99999]) + response = self.client.delete(url) + self.assertEqual(response.status_code, http.HTTPStatus.NOT_FOUND)