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:
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