test: add tweet creation + comment tests (13 tests)
Covers: create, blank content validation, guest restriction, read, owner edit, non-owner edit forbidden, owner delete, non-owner delete forbidden, reply creation, comment_count aggregate, tweet owner deletes comment, third party forbidden, guest comment forbidden. Key learnings: - Tweet :destroy is not primary; use Ash.Changeset.for_destroy(:destroy) - relate_actor fails with Invalid (not Forbidden) when no actor - Ash.get returns NotFound error on miss; pass not_found_error?: false for nil Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,9 @@
|
|||||||
- [ ] No CHECK constraint on `likes >= 0` at DB level (low priority, app logic prevents it)
|
- [ ] No CHECK constraint on `likes >= 0` at DB level (low priority, app logic prevents it)
|
||||||
- [ ] `read :following_feed` — nil actor returns empty list (not a bug)
|
- [ ] `read :following_feed` — nil actor returns empty list (not a bug)
|
||||||
- [ ] No search for users or tweets
|
- [ ] No search for users or tweets
|
||||||
- [ ] Missing test coverage: comments, tweet creation, auth flows
|
- [x] Tweet creation, update, delete, comment tests (13 tests)
|
||||||
|
- [ ] Missing test coverage: auth flows
|
||||||
|
- [ ] No pagination on user list (`/users`)
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
220
test/mixer/posts/tweet_test.exs
Normal file
220
test/mixer/posts/tweet_test.exs
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
defmodule Mixer.Posts.TweetTest do
|
||||||
|
use Mixer.DataCase, async: true
|
||||||
|
|
||||||
|
require Ash.Query
|
||||||
|
|
||||||
|
alias Mixer.Accounts.User
|
||||||
|
alias Mixer.Posts.Tweet
|
||||||
|
|
||||||
|
describe "tweet creation" do
|
||||||
|
test "a user can create a tweet" do
|
||||||
|
user = user_fixture("poster@example.com", "poster")
|
||||||
|
|
||||||
|
assert {:ok, tweet} =
|
||||||
|
Tweet
|
||||||
|
|> Ash.Changeset.for_create(:create, %{content: "hello world"}, actor: user)
|
||||||
|
|> Ash.create()
|
||||||
|
|
||||||
|
assert tweet.content == "hello world"
|
||||||
|
assert tweet.user_id == user.id
|
||||||
|
assert tweet.state == :posted
|
||||||
|
assert tweet.likes == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "tweet content cannot be blank" do
|
||||||
|
user = user_fixture("blank@example.com", "blankuser")
|
||||||
|
|
||||||
|
assert {:error, error} =
|
||||||
|
Tweet
|
||||||
|
|> Ash.Changeset.for_create(:create, %{content: nil}, actor: user)
|
||||||
|
|> Ash.create()
|
||||||
|
|
||||||
|
assert Exception.message(error) =~ "content"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "guests cannot create tweets" do
|
||||||
|
assert {:error, _error} =
|
||||||
|
Tweet
|
||||||
|
|> Ash.Changeset.for_create(:create, %{content: "spam"})
|
||||||
|
|> Ash.create()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "all users can read tweets" do
|
||||||
|
user = user_fixture("readable@example.com", "readable")
|
||||||
|
|
||||||
|
Tweet
|
||||||
|
|> Ash.Changeset.for_create(:create, %{content: "public post"}, actor: user)
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
|
tweets = Tweet |> Ash.read!(authorize?: false)
|
||||||
|
assert length(tweets) >= 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "tweet update" do
|
||||||
|
test "owner can edit their tweet" do
|
||||||
|
user = user_fixture("editor@example.com", "editor")
|
||||||
|
tweet = tweet_fixture(user, "original content")
|
||||||
|
|
||||||
|
assert {:ok, updated} =
|
||||||
|
tweet
|
||||||
|
|> Ash.Changeset.for_update(:update, %{content: "edited content"}, actor: user)
|
||||||
|
|> Ash.update()
|
||||||
|
|
||||||
|
assert updated.content == "edited content"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-owner cannot edit a tweet" do
|
||||||
|
owner = user_fixture("owner@example.com", "tweetowner")
|
||||||
|
other = user_fixture("other@example.com", "otheruser")
|
||||||
|
tweet = tweet_fixture(owner, "owner's post")
|
||||||
|
|
||||||
|
assert {:error, error} =
|
||||||
|
tweet
|
||||||
|
|> Ash.Changeset.for_update(:update, %{content: "hacked"}, actor: other)
|
||||||
|
|> Ash.update()
|
||||||
|
|
||||||
|
assert Exception.message(error) =~ "forbidden"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "tweet deletion" do
|
||||||
|
test "owner can delete their tweet" do
|
||||||
|
user = user_fixture("deleter@example.com", "deleter")
|
||||||
|
tweet = tweet_fixture(user, "to be deleted")
|
||||||
|
|
||||||
|
assert :ok =
|
||||||
|
tweet
|
||||||
|
|> Ash.Changeset.for_destroy(:destroy, %{}, actor: user)
|
||||||
|
|> Ash.destroy()
|
||||||
|
|
||||||
|
assert {:ok, nil} = Tweet |> Ash.get(tweet.id, authorize?: false, not_found_error?: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-owner cannot delete a tweet" do
|
||||||
|
owner = user_fixture("owner2@example.com", "owner2")
|
||||||
|
other = user_fixture("other2@example.com", "other2")
|
||||||
|
tweet = tweet_fixture(owner, "protected post")
|
||||||
|
|
||||||
|
assert {:error, error} =
|
||||||
|
tweet
|
||||||
|
|> Ash.Changeset.for_destroy(:destroy, %{}, actor: other)
|
||||||
|
|> Ash.destroy()
|
||||||
|
|
||||||
|
assert Exception.message(error) =~ "forbidden"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "comments (replies)" do
|
||||||
|
test "a user can reply to a tweet" do
|
||||||
|
author = user_fixture("author@example.com", "author")
|
||||||
|
replier = user_fixture("replier@example.com", "replier")
|
||||||
|
parent = tweet_fixture(author, "parent post")
|
||||||
|
|
||||||
|
assert {:ok, comment} =
|
||||||
|
Tweet
|
||||||
|
|> Ash.Changeset.for_create(
|
||||||
|
:create,
|
||||||
|
%{content: "great post!", parent_tweet_id: parent.id},
|
||||||
|
actor: replier
|
||||||
|
)
|
||||||
|
|> Ash.create()
|
||||||
|
|
||||||
|
assert comment.parent_tweet_id == parent.id
|
||||||
|
assert comment.user_id == replier.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "comment_count reflects number of replies" do
|
||||||
|
author = user_fixture("countauthor@example.com", "countauthor")
|
||||||
|
replier = user_fixture("countreplier@example.com", "countreplier")
|
||||||
|
parent = tweet_fixture(author, "tweet with replies")
|
||||||
|
|
||||||
|
Tweet
|
||||||
|
|> Ash.Changeset.for_create(:create, %{content: "reply 1", parent_tweet_id: parent.id}, actor: replier)
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
|
Tweet
|
||||||
|
|> Ash.Changeset.for_create(:create, %{content: "reply 2", parent_tweet_id: parent.id}, actor: replier)
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
|
loaded = Tweet |> Ash.get!(parent.id, load: [:comment_count], authorize?: false)
|
||||||
|
assert loaded.comment_count == 2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "tweet owner can delete a comment on their tweet" do
|
||||||
|
author = user_fixture("tweetowner3@example.com", "tweetowner3")
|
||||||
|
replier = user_fixture("commenter@example.com", "commenter")
|
||||||
|
parent = tweet_fixture(author, "parent tweet")
|
||||||
|
|
||||||
|
comment =
|
||||||
|
Tweet
|
||||||
|
|> Ash.Changeset.for_create(
|
||||||
|
:create,
|
||||||
|
%{content: "a comment", parent_tweet_id: parent.id},
|
||||||
|
actor: replier
|
||||||
|
)
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
|
# Tweet owner (author) can delete someone else's comment on their post
|
||||||
|
assert :ok =
|
||||||
|
comment
|
||||||
|
|> Ash.Changeset.for_destroy(:destroy, %{}, actor: author)
|
||||||
|
|> Ash.destroy()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "a third party cannot delete a comment they don't own" do
|
||||||
|
author = user_fixture("tweetowner4@example.com", "tweetowner4")
|
||||||
|
replier = user_fixture("commenter2@example.com", "commenter2")
|
||||||
|
bystander = user_fixture("bystander@example.com", "bystander")
|
||||||
|
parent = tweet_fixture(author, "parent tweet 2")
|
||||||
|
|
||||||
|
comment =
|
||||||
|
Tweet
|
||||||
|
|> Ash.Changeset.for_create(
|
||||||
|
:create,
|
||||||
|
%{content: "a comment", parent_tweet_id: parent.id},
|
||||||
|
actor: replier
|
||||||
|
)
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
|
assert {:error, error} =
|
||||||
|
comment
|
||||||
|
|> Ash.Changeset.for_destroy(:destroy, %{}, actor: bystander)
|
||||||
|
|> Ash.destroy()
|
||||||
|
|
||||||
|
assert Exception.message(error) =~ "forbidden"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "guests cannot post comments" do
|
||||||
|
author = user_fixture("tweetowner5@example.com", "tweetowner5")
|
||||||
|
parent = tweet_fixture(author, "parent post 3")
|
||||||
|
|
||||||
|
assert {:error, _error} =
|
||||||
|
Tweet
|
||||||
|
|> Ash.Changeset.for_create(
|
||||||
|
:create,
|
||||||
|
%{content: "spam comment", parent_tweet_id: parent.id}
|
||||||
|
)
|
||||||
|
|> Ash.create()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
defp user_fixture(email, username) do
|
||||||
|
User
|
||||||
|
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||||
|
email: email,
|
||||||
|
password: "password1234",
|
||||||
|
password_confirmation: "password1234",
|
||||||
|
username: username
|
||||||
|
})
|
||||||
|
|> Ash.create!(authorize?: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp tweet_fixture(user, content) do
|
||||||
|
Tweet
|
||||||
|
|> Ash.Changeset.for_create(:create, %{content: content}, actor: user)
|
||||||
|
|> Ash.create!()
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user