who_need_help/test/who_need_help/activities_test.exs

307 lines
11 KiB
Elixir

defmodule WhoNeedHelp.ActivitiesTest do
use WhoNeedHelp.DataCase, async: false
import WhoNeedHelp.AccountsFixtures
alias WhoNeedHelp.{Activities, Catalog, Trust}
alias WhoNeedHelp.Activities.Activity
alias WhoNeedHelp.Repo
alias WhoNeedHelp.Trust.AuditEvent
setup do
Catalog.seed_defaults()
organizer = user_fixture(display_name: "Organizer")
participant = user_fixture(display_name: "Participant")
outsider = user_fixture(display_name: "Outsider")
coffee =
Catalog.list_categories(:activity)
|> Enum.find(&(&1.slug == "coffee-meetup"))
attrs = %{
"title" => "Coffee and conversation",
"description" => "Meet at a public café for conversation and tea.",
"structured_data" => %{"setting" => "cafe"},
"location_label" => "Central square",
"latitude" => "50.4501",
"longitude" => "30.5234",
"location_visibility" => "approximate_public",
"starts_at" => DateTime.utc_now(:second) |> DateTime.add(2, :hour),
"join_deadline" => DateTime.utc_now(:second) |> DateTime.add(1, :hour),
"capacity" => 2,
"category_id" => coffee.id
}
{:ok,
organizer: organizer,
participant: participant,
outsider: outsider,
organizer_scope: user_scope_fixture(organizer),
participant_scope: user_scope_fixture(participant),
outsider_scope: user_scope_fixture(outsider),
coffee: coffee,
attrs: attrs}
end
test "organizer approves a participant and the approved group can chat", context do
assert {:ok, activity} =
Activities.create_activity(context.organizer_scope, context.attrs)
assert [%{role: :organizer, status: :approved}] = activity.participants
assert Activity.public_coordinates(activity) == %{
latitude: 50.45,
longitude: 30.52,
exact: false
}
assert Activities.coordinates_for(context.outsider_scope, activity).exact == false
assert Activities.coordinates_for(context.organizer_scope, activity).exact == true
assert {:error, :forbidden} =
Activities.send_message(context.outsider_scope, activity.id, %{"body" => "Hello"})
assert {:ok, request} =
Activities.request_to_join(context.participant_scope, activity.id)
assert request.status == :requested
assert {:ok, approved} =
Activities.approve_participant(context.organizer_scope, request.id)
assert approved.status == :approved
assert {:ok, message} =
Activities.send_message(context.participant_scope, activity.id, %{
"body" => "See you at the café."
})
assert message.body == "See you at the café."
assert {:ok, loaded} = Activities.get_activity(context.participant_scope, activity.id)
assert [%{body: "See you at the café."}] = loaded.messages
assert Activities.coordinates_for(context.participant_scope, loaded).exact == true
assert {:error, :capacity_reached} =
Activities.request_to_join(context.outsider_scope, activity.id)
end
test "unapproved viewers receive no private chat or pending participant data", context do
{:ok, activity} = Activities.create_activity(context.organizer_scope, context.attrs)
{:ok, request} = Activities.request_to_join(context.participant_scope, activity.id)
assert {:ok, public_activity} =
Activities.get_activity(context.outsider_scope, activity.id)
assert public_activity.messages == []
assert Enum.all?(public_activity.participants, &(&1.status == :approved))
refute Enum.any?(public_activity.participants, &(&1.id == request.id))
assert {:ok, requester_view} =
Activities.get_activity(context.participant_scope, activity.id)
assert requester_view.messages == []
assert Enum.any?(requester_view.participants, &(&1.id == request.id))
end
test "activity completion never changes urgent-help reputation", context do
{:ok, activity} = Activities.create_activity(context.organizer_scope, context.attrs)
{:ok, request} = Activities.request_to_join(context.participant_scope, activity.id)
{:ok, _approved} = Activities.approve_participant(context.organizer_scope, request.id)
assert {:ok, completed} = Activities.complete_activity(context.organizer_scope, activity.id)
assert completed.status == :completed
assert Trust.reputation(context.organizer.id).completed == 0
assert Trust.reputation(context.participant.id).completed == 0
assert {:error, :not_found} = Activities.get_activity(context.outsider_scope, activity.id)
assert {:ok, _activity} = Activities.get_activity(context.participant_scope, activity.id)
end
test "activity category and structured fields are enforced", context do
hiking =
Catalog.list_categories(:activity)
|> Enum.find(&(&1.slug == "hiking-group"))
invalid =
context.attrs
|> Map.put("category_id", hiking.id)
|> Map.put("structured_data", %{
"difficulty" => "easy",
"route_plan" => "Marked public trail"
})
assert {:error, changeset} =
Activities.create_activity(context.organizer_scope, invalid)
assert "safety_plan_confirmed is required" in errors_on(changeset).structured_data
help_category = Catalog.seed_defaults()
invalid =
context.attrs
|> Map.put("category_id", help_category.id)
|> Map.put("structured_data", %{"pickup_status" => "reserved"})
assert {:error, changeset} =
Activities.create_activity(context.organizer_scope, invalid)
assert "select an activity category" in errors_on(changeset).category_id
end
test "blocking prevents activity discovery and joining", context do
{:ok, activity} = Activities.create_activity(context.organizer_scope, context.attrs)
assert {:ok, _block} = Trust.block(context.organizer_scope, context.participant.id)
assert [] = Activities.list_open_activities(context.participant_scope)
assert {:error, :blocked} =
Activities.request_to_join(context.participant_scope, activity.id)
end
test "activity discovery uses stable cursor pages", context do
activities =
for offset <- 2..5 do
attrs =
context.attrs
|> Map.put("title", "Cursor activity #{offset}")
|> Map.put(
"starts_at",
DateTime.utc_now(:second) |> DateTime.add(offset, :hour)
)
|> Map.put(
"join_deadline",
DateTime.utc_now(:second) |> DateTime.add(offset - 1, :hour)
)
{:ok, activity} = Activities.create_activity(context.organizer_scope, attrs)
activity
end
first = Activities.paginate_open_activities(context.outsider_scope, %{}, limit: 2)
second =
Activities.paginate_open_activities(context.outsider_scope, %{},
limit: 2,
after: first.next_cursor
)
expected_ids =
activities
|> Enum.sort_by(&{&1.starts_at, &1.id})
|> Enum.map(& &1.id)
actual_ids = Enum.map(first.entries ++ second.entries, & &1.id)
assert actual_ids == expected_ids
assert length(Enum.uniq(actual_ids)) == 4
assert is_binary(first.next_cursor)
assert second.next_cursor == nil
end
test "my activities are derived from the organizer and participant membership rows", context do
{:ok, organized} = Activities.create_activity(context.organizer_scope, context.attrs)
{:ok, request} = Activities.request_to_join(context.participant_scope, organized.id)
assert Enum.map(Activities.list_my_activities(context.organizer_scope), & &1.id) == [
organized.id
]
assert Enum.map(Activities.list_my_activities(context.participant_scope), & &1.id) == [
organized.id
]
assert {:ok, _left} = Activities.leave_activity(context.participant_scope, organized.id)
assert Activities.list_my_activities(context.participant_scope) == []
assert request.status == :requested
end
test "activity message reports expose only the linked group to audited moderators", context do
moderator =
user_fixture(display_name: "Activity moderator")
|> Ecto.Changeset.change(role: :moderator)
|> Repo.update!()
attrs = Map.put(context.attrs, "capacity", 3)
{:ok, activity} = Activities.create_activity(context.organizer_scope, attrs)
{:ok, request} = Activities.request_to_join(context.participant_scope, activity.id)
{:ok, _approved} = Activities.approve_participant(context.organizer_scope, request.id)
{:ok, organizer_message} =
Activities.send_message(context.organizer_scope, activity.id, %{
"body" => "Meet beside the public entrance."
})
{:ok, participant_message} =
Activities.send_message(context.participant_scope, activity.id, %{
"body" => "Understood."
})
assert {:error, :forbidden} =
Trust.report(context.outsider_scope, %{
"reason" => "harassment",
"details" => "I should not see this private message.",
"activity_message_id" => organizer_message.id
})
assert {:ok, report} =
Trust.report(context.participant_scope, %{
"reason" => "harassment",
"details" => "Please review this approved group conversation.",
"activity_message_id" => organizer_message.id
})
assert {:ok, evidence} =
Trust.report_evidence(user_scope_fixture(moderator), report.id)
assert evidence.messages == []
expected_message_ids =
[organizer_message, participant_message]
|> Enum.sort_by(&{&1.inserted_at, &1.id})
|> Enum.map(& &1.id)
assert Enum.map(evidence.activity_messages, & &1.id) == expected_message_ids
assert Repo.exists?(
from event in AuditEvent,
where:
event.action == "report.evidence_viewed" and event.target_id == ^report.id and
event.actor_id == ^moderator.id
)
end
test "moderators can hide and restore a reported activity", context do
moderator =
user_fixture(display_name: "Activity moderator")
|> Ecto.Changeset.change(role: :moderator)
|> Repo.update!()
moderator_scope = user_scope_fixture(moderator)
{:ok, activity} = Activities.create_activity(context.organizer_scope, context.attrs)
assert {:error, :cannot_report_self} =
Trust.report(context.organizer_scope, %{
"reason" => "other",
"details" => "Cannot report my own activity.",
"activity_id" => activity.id
})
assert {:ok, _report} =
Trust.report(context.outsider_scope, %{
"reason" => "dangerous_request",
"details" => "The public plan needs moderator review.",
"activity_id" => activity.id
})
assert {:ok, hidden} =
Trust.hide_activity(moderator_scope, activity.id, "Safety review")
assert hidden.hidden_at
assert {:error, :not_found} = Activities.get_activity(context.outsider_scope, activity.id)
assert {:ok, restored} = Trust.restore_activity(moderator_scope, activity.id)
assert is_nil(restored.hidden_at)
assert {:ok, _activity} = Activities.get_activity(context.outsider_scope, activity.id)
end
end