666 lines
24 KiB
Elixir
666 lines
24 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.Help.DiscoveryViewport
|
|
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,
|
|
"safety_confirmed" => true
|
|
}
|
|
|
|
{: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 "an incomplete start time does not get a misleading future error", context do
|
|
changeset =
|
|
Activity.create_changeset(%Activity{}, Map.put(context.attrs, "starts_at", ""))
|
|
|
|
assert "can't be blank" in errors_on(changeset).starts_at
|
|
refute "must be in the future" in errors_on(changeset).starts_at
|
|
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 "global activity events carry only the bounded index projection", context do
|
|
:ok = Activities.subscribe()
|
|
{:ok, activity} = Activities.create_activity(context.organizer_scope, context.attrs)
|
|
|
|
assert_receive {:activity_created, created}
|
|
assert created.id == activity.id
|
|
assert created.approved_participant_count == 1
|
|
assert %Ecto.Association.NotLoaded{} = created.participants
|
|
assert %Ecto.Association.NotLoaded{} = created.messages
|
|
assert %Ecto.Association.NotLoaded{} = created.creator.social_identities
|
|
|
|
:ok = Activities.subscribe_user(context.participant_scope)
|
|
{:ok, _request} = Activities.request_to_join(context.participant_scope, activity.id)
|
|
|
|
assert_receive {:activity_updated, updated}
|
|
assert updated.id == activity.id
|
|
assert updated.approved_participant_count == 1
|
|
assert %Ecto.Association.NotLoaded{} = updated.participants
|
|
assert %Ecto.Association.NotLoaded{} = updated.messages
|
|
assert_receive {:my_activity_updated, participant_update, true}
|
|
assert participant_update.id == activity.id
|
|
end
|
|
|
|
test "invalid and missing client identifiers are rejected without raising", context do
|
|
assert {:error, :not_found} =
|
|
Activities.approve_participant(context.organizer_scope, "not-a-uuid")
|
|
|
|
assert {:error, :not_found} =
|
|
Activities.decline_participant(context.organizer_scope, "not-a-uuid")
|
|
|
|
missing_id = Ecto.UUID.generate()
|
|
|
|
assert {:error, :not_found} =
|
|
Activities.approve_participant(context.organizer_scope, missing_id)
|
|
|
|
assert {:error, :not_found} =
|
|
Activities.decline_participant(context.organizer_scope, missing_id)
|
|
|
|
assert {:error, :not_found} =
|
|
Activities.request_to_join(context.participant_scope, missing_id)
|
|
|
|
assert {:error, :not_found} =
|
|
Activities.send_message(context.participant_scope, missing_id, %{"body" => "No"})
|
|
|
|
assert {:error, :not_found} =
|
|
Activities.cancel_activity(context.organizer_scope, missing_id)
|
|
|
|
assert {:error, :not_found} =
|
|
Activities.complete_activity(context.organizer_scope, missing_id)
|
|
|
|
for invalid_id <- ["not-a-uuid", missing_id] do
|
|
assert {:error, :not_found} =
|
|
Activities.leave_activity(context.participant_scope, invalid_id)
|
|
|
|
assert {:error, :not_found} =
|
|
Activities.request_to_join(context.participant_scope, invalid_id)
|
|
|
|
assert {:error, :not_found} =
|
|
Activities.send_message(context.participant_scope, invalid_id, %{"body" => "No"})
|
|
|
|
assert {:error, :not_found} =
|
|
Activities.cancel_activity(context.organizer_scope, invalid_id)
|
|
|
|
assert {:error, :not_found} =
|
|
Activities.complete_activity(context.organizer_scope, invalid_id)
|
|
end
|
|
end
|
|
|
|
test "activity location labels are validated before the database write", context do
|
|
assert {:error, changeset} =
|
|
Activities.create_activity(
|
|
context.organizer_scope,
|
|
Map.put(context.attrs, "location_label", String.duplicate("x", 256))
|
|
)
|
|
|
|
assert "should be at most 255 character(s)" in errors_on(changeset).location_label
|
|
end
|
|
|
|
test "activity capacity outside the PostgreSQL integer range is rejected before insert",
|
|
context do
|
|
assert {:error, changeset} =
|
|
Activities.create_activity(
|
|
context.organizer_scope,
|
|
Map.put(context.attrs, "capacity", 2_147_483_648)
|
|
)
|
|
|
|
assert "must be less than or equal to 2147483647" in errors_on(changeset).capacity
|
|
end
|
|
|
|
test "forged coordinate types become validation errors instead of raising", context do
|
|
assert {:error, changeset} =
|
|
Activities.create_activity(
|
|
context.organizer_scope,
|
|
Map.put(context.attrs, "latitude", %{"nested" => "value"})
|
|
)
|
|
|
|
assert "select a valid location" in errors_on(changeset).location
|
|
end
|
|
|
|
test "forged category filters return no activities instead of raising", context do
|
|
assert {:ok, _activity} =
|
|
Activities.create_activity(context.organizer_scope, context.attrs)
|
|
|
|
assert [] ==
|
|
Activities.list_open_activities(context.participant_scope, %{
|
|
"category_id" => "not-a-uuid"
|
|
})
|
|
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 "blocking an approved participant revokes exact location and group chat", 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)
|
|
{:ok, loaded} = Activities.get_activity(context.participant_scope, activity.id)
|
|
|
|
assert Activities.coordinates_for(context.participant_scope, loaded).exact
|
|
assert {:ok, _block} = Trust.block(context.organizer_scope, context.participant.id)
|
|
|
|
assert {:error, :not_found} =
|
|
Activities.get_activity(context.participant_scope, activity.id)
|
|
|
|
assert Activities.coordinates_for(context.participant_scope, loaded).exact == false
|
|
assert Activities.paginate_messages(context.participant_scope, loaded).entries == []
|
|
|
|
assert {:error, :blocked} =
|
|
Activities.send_message(context.participant_scope, activity.id, %{
|
|
"body" => "This must not be delivered"
|
|
})
|
|
end
|
|
|
|
test "activity creation requires server-side safety consent", context do
|
|
attrs = Map.delete(context.attrs, "safety_confirmed")
|
|
|
|
assert {:error, changeset} =
|
|
Activities.create_activity(context.organizer_scope, attrs)
|
|
|
|
assert "confirm the safety guidance before publishing" in errors_on(changeset).safety_confirmed
|
|
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
|
|
assert Enum.all?(first.entries ++ second.entries, &(&1.approved_participant_count == 1))
|
|
|
|
assert Enum.all?(first.entries ++ second.entries, fn activity ->
|
|
match?(%Ecto.Association.NotLoaded{}, activity.participants)
|
|
end)
|
|
end
|
|
|
|
test "activity discovery loads only the selected map viewport", context do
|
|
{:ok, nearby} =
|
|
Activities.create_activity(
|
|
context.organizer_scope,
|
|
context.attrs
|
|
|> Map.put("title", "Nearby activity")
|
|
|> Map.put("latitude", "50.4501")
|
|
|> Map.put("longitude", "30.5234")
|
|
)
|
|
|
|
{:ok, far_away} =
|
|
Activities.create_activity(
|
|
context.organizer_scope,
|
|
context.attrs
|
|
|> Map.put("title", "Far-away activity")
|
|
|> Map.put("latitude", "52.5200")
|
|
|> Map.put("longitude", "13.4050")
|
|
)
|
|
|
|
viewport = viewport!(30.0, 50.0, 31.0, 51.0, 10)
|
|
|
|
ids =
|
|
context.outsider_scope
|
|
|> Activities.paginate_open_activities(%{}, viewport: viewport, limit: 100)
|
|
|> Map.fetch!(:entries)
|
|
|> Enum.map(& &1.id)
|
|
|
|
assert nearby.id in ids
|
|
refute far_away.id in ids
|
|
end
|
|
|
|
test "activity discovery filters by the published approximate point, not the exact point",
|
|
context do
|
|
{:ok, activity} =
|
|
Activities.create_activity(
|
|
context.organizer_scope,
|
|
context.attrs
|
|
|> Map.put("title", "Privacy-safe viewport activity")
|
|
|> Map.put("latitude", "50.4501")
|
|
|> Map.put("longitude", "30.5234")
|
|
)
|
|
|
|
exact_only_viewport = viewport!(30.523, 50.449, 30.524, 50.451, 16)
|
|
|
|
exact_only_entries =
|
|
context.outsider_scope
|
|
|> Activities.paginate_open_activities(%{},
|
|
viewport: exact_only_viewport,
|
|
limit: 100
|
|
)
|
|
|> Map.fetch!(:entries)
|
|
|
|
refute Enum.any?(exact_only_entries, &(&1.id == activity.id))
|
|
|
|
published_point_viewport = viewport!(30.519, 50.449, 30.521, 50.451, 16)
|
|
|
|
published_entries =
|
|
context.outsider_scope
|
|
|> Activities.paginate_open_activities(%{},
|
|
viewport: published_point_viewport,
|
|
limit: 100
|
|
)
|
|
|> Map.fetch!(:entries)
|
|
|
|
assert Enum.any?(published_entries, &(&1.id == activity.id))
|
|
end
|
|
|
|
test "activity map discovery clusters dense points and keeps sparse points", context do
|
|
for {offset, index} <- Enum.with_index([0.0, 0.00005, 0.0001], 1) do
|
|
assert {:ok, _activity} =
|
|
Activities.create_activity(
|
|
context.organizer_scope,
|
|
context.attrs
|
|
|> Map.put("title", "Dense activity #{index}")
|
|
|> Map.put("longitude", Float.to_string(30.5234 + offset))
|
|
)
|
|
end
|
|
|
|
{:ok, sparse} =
|
|
Activities.create_activity(
|
|
context.organizer_scope,
|
|
context.attrs
|
|
|> Map.put("title", "Sparse activity")
|
|
|> Map.put("latitude", "50.8")
|
|
|> Map.put("longitude", "30.8")
|
|
)
|
|
|
|
items =
|
|
Activities.map_discovery_items(
|
|
context.outsider_scope,
|
|
%{},
|
|
viewport!(30.0, 50.0, 31.0, 51.0, 9)
|
|
)
|
|
|
|
cluster = Enum.find(items, &match?(%{type: "cluster", count: 3}, &1))
|
|
assert cluster
|
|
assert_in_delta cluster.latitude, 50.45, 0.000_001
|
|
assert_in_delta cluster.longitude, 30.52, 0.000_001
|
|
|
|
assert Enum.any?(items, fn
|
|
%{type: "activity", id: id} -> id == sparse.id
|
|
_item -> false
|
|
end)
|
|
end
|
|
|
|
test "hidden activity is discoverable only through its public area description", context do
|
|
{:ok, hidden} =
|
|
Activities.create_activity(
|
|
context.organizer_scope,
|
|
context.attrs
|
|
|> Map.put("title", "Hidden-map activity")
|
|
|> Map.put("location_label", "Kyiv Podil riverside")
|
|
|> Map.put("location_visibility", "hidden")
|
|
)
|
|
|
|
viewport = viewport!(30.0, 50.0, 31.0, 51.0, 10)
|
|
|
|
without_area =
|
|
Activities.paginate_open_activities(context.outsider_scope, %{},
|
|
viewport: viewport,
|
|
limit: 100
|
|
)
|
|
|
|
refute Enum.any?(without_area.entries, &(&1.id == hidden.id))
|
|
|
|
with_area =
|
|
Activities.paginate_open_activities(
|
|
context.outsider_scope,
|
|
%{"area" => "Podil"},
|
|
viewport: viewport,
|
|
limit: 100
|
|
)
|
|
|
|
assert Enum.any?(with_area.entries, &(&1.id == hidden.id))
|
|
|
|
assert Activities.map_discovery_items(context.outsider_scope, %{"area" => "Podil"}, viewport) ==
|
|
[]
|
|
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 "leaving revokes private activity access and rejoining returns to approval pending",
|
|
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)
|
|
{:ok, approved_activity} = Activities.get_activity(context.participant_scope, activity.id)
|
|
|
|
assert Activities.coordinates_for(context.participant_scope, approved_activity).exact
|
|
|
|
assert {:ok, _message} =
|
|
Activities.send_message(context.participant_scope, activity.id, %{
|
|
"body" => "Visible before leaving"
|
|
})
|
|
|
|
assert {:ok, left} = Activities.leave_activity(context.participant_scope, activity.id)
|
|
assert left.status == :left
|
|
|
|
assert {:ok, left_activity} = Activities.get_activity(context.participant_scope, activity.id)
|
|
assert Activities.coordinates_for(context.participant_scope, left_activity).exact == false
|
|
assert Activities.paginate_messages(context.participant_scope, left_activity).entries == []
|
|
|
|
assert {:error, :forbidden} =
|
|
Activities.send_message(context.participant_scope, activity.id, %{
|
|
"body" => "Must not be visible after leaving"
|
|
})
|
|
|
|
assert {:ok, rejoined} =
|
|
Activities.request_to_join(context.participant_scope, activity.id)
|
|
|
|
assert rejoined.id == request.id
|
|
assert rejoined.status == :requested
|
|
assert is_nil(rejoined.reviewed_at)
|
|
assert is_nil(rejoined.left_at)
|
|
|
|
assert Enum.map(Activities.list_my_activities(context.participant_scope), & &1.id) == [
|
|
activity.id
|
|
]
|
|
end
|
|
|
|
test "my activities exclude organizers blocked in either direction", context do
|
|
{:ok, activity} = Activities.create_activity(context.organizer_scope, context.attrs)
|
|
{:ok, _request} = Activities.request_to_join(context.participant_scope, activity.id)
|
|
|
|
assert Enum.map(Activities.list_my_activities(context.participant_scope), & &1.id) == [
|
|
activity.id
|
|
]
|
|
|
|
assert {:ok, _block} = Trust.block(context.participant_scope, context.organizer.id)
|
|
assert Activities.list_my_activities(context.participant_scope) == []
|
|
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
|
|
|
|
defp viewport!(west, south, east, north, zoom) do
|
|
{:ok, viewport} =
|
|
DiscoveryViewport.cast(%{
|
|
west: west,
|
|
south: south,
|
|
east: east,
|
|
north: north,
|
|
zoom: zoom,
|
|
width: 1_200,
|
|
height: 720
|
|
})
|
|
|
|
viewport
|
|
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
|