Self-follow check used get_attribute(:follower_id) which is nil at validation time because relate_actor runs after validations in Ash. Fixed to use context.actor.id directly. Added 9 tests covering: follow, follow idempotency, self-follow prevention, guest restriction, unfollow, unfollow noop, guest unfollow, follower/following counts, and am_i_following. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
173 lines
5.4 KiB
Elixir
173 lines
5.4 KiB
Elixir
defmodule Mixer.Accounts.FollowTest do
|
|
use Mixer.DataCase, async: true
|
|
|
|
require Ash.Query
|
|
|
|
alias Mixer.Accounts.Follow
|
|
alias Mixer.Accounts.User
|
|
|
|
describe "follow" do
|
|
test "a user can follow another user" do
|
|
alice = user_fixture("alice@example.com", "alice")
|
|
bob = user_fixture("bob@example.com", "bob")
|
|
|
|
assert {:ok, follow} =
|
|
Follow
|
|
|> Ash.Changeset.for_create(:follow, %{following_id: bob.id}, actor: alice)
|
|
|> Ash.create()
|
|
|
|
assert follow.follower_id == alice.id
|
|
assert follow.following_id == bob.id
|
|
end
|
|
|
|
test "following the same user twice is a noop (upsert)" do
|
|
alice = user_fixture("alice2@example.com", "alice2")
|
|
bob = user_fixture("bob2@example.com", "bob2")
|
|
|
|
assert {:ok, _} =
|
|
Follow
|
|
|> Ash.Changeset.for_create(:follow, %{following_id: bob.id}, actor: alice)
|
|
|> Ash.create()
|
|
|
|
assert {:ok, _} =
|
|
Follow
|
|
|> Ash.Changeset.for_create(:follow, %{following_id: bob.id}, actor: alice)
|
|
|> Ash.create()
|
|
|
|
assert count_follows(alice.id, bob.id) == 1
|
|
end
|
|
|
|
test "a user cannot follow themselves" do
|
|
alice = user_fixture("alice3@example.com", "alice3")
|
|
|
|
assert {:error, error} =
|
|
Follow
|
|
|> Ash.Changeset.for_create(:follow, %{following_id: alice.id}, actor: alice)
|
|
|> Ash.create()
|
|
|
|
assert Exception.message(error) =~ "cannot follow yourself"
|
|
end
|
|
|
|
test "guests cannot follow" do
|
|
bob = user_fixture("bob3@example.com", "bob3")
|
|
|
|
assert {:error, _error} =
|
|
Follow
|
|
|> Ash.Changeset.for_create(:follow, %{following_id: bob.id})
|
|
|> Ash.create()
|
|
end
|
|
end
|
|
|
|
describe "unfollow" do
|
|
test "a user can unfollow someone they follow" do
|
|
alice = user_fixture("alice4@example.com", "alice4")
|
|
bob = user_fixture("bob4@example.com", "bob4")
|
|
|
|
Follow
|
|
|> Ash.Changeset.for_create(:follow, %{following_id: bob.id}, actor: alice)
|
|
|> Ash.create!()
|
|
|
|
assert count_follows(alice.id, bob.id) == 1
|
|
|
|
assert :ok =
|
|
Follow
|
|
|> Ash.ActionInput.for_action(:unfollow, %{following_id: bob.id}, actor: alice)
|
|
|> Ash.run_action()
|
|
|
|
assert count_follows(alice.id, bob.id) == 0
|
|
end
|
|
|
|
test "unfollowing when not following is a noop" do
|
|
alice = user_fixture("alice5@example.com", "alice5")
|
|
bob = user_fixture("bob5@example.com", "bob5")
|
|
|
|
assert :ok =
|
|
Follow
|
|
|> Ash.ActionInput.for_action(:unfollow, %{following_id: bob.id}, actor: alice)
|
|
|> Ash.run_action()
|
|
|
|
assert count_follows(alice.id, bob.id) == 0
|
|
end
|
|
|
|
test "guests cannot unfollow" do
|
|
bob = user_fixture("bob6@example.com", "bob6")
|
|
|
|
assert {:error, error} =
|
|
Follow
|
|
|> Ash.ActionInput.for_action(:unfollow, %{following_id: bob.id})
|
|
|> Ash.run_action()
|
|
|
|
assert Exception.message(error) =~ "forbidden"
|
|
end
|
|
end
|
|
|
|
describe "follower/following counts" do
|
|
test "follower_count and following_count reflect current follows" do
|
|
alice = user_fixture("alice6@example.com", "alice6")
|
|
bob = user_fixture("bob7@example.com", "bob7")
|
|
carol = user_fixture("carol@example.com", "carol")
|
|
|
|
Follow
|
|
|> Ash.Changeset.for_create(:follow, %{following_id: bob.id}, actor: alice)
|
|
|> Ash.create!()
|
|
|
|
Follow
|
|
|> Ash.Changeset.for_create(:follow, %{following_id: carol.id}, actor: alice)
|
|
|> Ash.create!()
|
|
|
|
Follow
|
|
|> Ash.Changeset.for_create(:follow, %{following_id: bob.id}, actor: carol)
|
|
|> Ash.create!()
|
|
|
|
alice_loaded = User |> Ash.get!(alice.id, load: [:follower_count, :following_count], authorize?: false)
|
|
bob_loaded = User |> Ash.get!(bob.id, load: [:follower_count, :following_count], authorize?: false)
|
|
|
|
assert alice_loaded.following_count == 2
|
|
assert alice_loaded.follower_count == 0
|
|
assert bob_loaded.follower_count == 2
|
|
assert bob_loaded.following_count == 0
|
|
end
|
|
|
|
test "am_i_following reflects the actor's follow status" do
|
|
alice = user_fixture("alice7@example.com", "alice7")
|
|
bob = user_fixture("bob8@example.com", "bob8")
|
|
|
|
not_following =
|
|
User |> Ash.get!(bob.id, actor: alice, load: [:am_i_following], authorize?: false)
|
|
|
|
refute not_following.am_i_following
|
|
|
|
Follow
|
|
|> Ash.Changeset.for_create(:follow, %{following_id: bob.id}, actor: alice)
|
|
|> Ash.create!()
|
|
|
|
following =
|
|
User |> Ash.get!(bob.id, actor: alice, load: [:am_i_following], authorize?: false)
|
|
|
|
assert following.am_i_following
|
|
end
|
|
end
|
|
|
|
# ── fixtures ──────────────────────────────────────────────────────────────
|
|
|
|
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 count_follows(follower_id, following_id) do
|
|
Follow
|
|
|> Ash.Query.filter(
|
|
Ash.Expr.expr(follower_id == ^follower_id and following_id == ^following_id)
|
|
)
|
|
|> Ash.read!(authorize?: false)
|
|
|> length()
|
|
end
|
|
end
|