defmodule TarakanWeb.ModerationLiveTest do use TarakanWeb.ConnCase import Phoenix.LiveViewTest alias Tarakan.Accounts alias Tarakan.Accounts.Scope alias Tarakan.Moderation test "reporting requires an authenticated account", %{conn: conn} do assert {:error, {:redirect, %{to: "/accounts/log-in"}}} = live(conn, ~p"/moderation/report") end test "a participant submits a restricted report and can inspect its case", %{conn: conn} do reporter = account_fixture() subject = account_fixture() conn = log_in_account(conn, reporter) {:ok, view, _html} = live(conn, ~p"/moderation/report?subject_type=account&subject_id=#{subject.id}") assert has_element?(view, "#moderation-report-form") assert has_element?(view, "#header-report-content") refute has_element?(view, "#header-moderation-queue") view |> form("#moderation-report-form", report: %{ subject_type: "account", subject_id: subject.id, reason: "harassment", description: "This account is posting targeted harassment in public review evidence." } ) |> render_submit() {path, flash} = assert_redirect(view) assert path =~ ~r{^/moderation/cases/\d+$} assert flash["info"] {:ok, case_view, _html} = live(conn, path) assert has_element?(case_view, "#moderation-case") assert has_element?(case_view, "#moderation-case-status", "Open") refute has_element?(case_view, "#moderation-case-actions") refute has_element?(case_view, "#moderation-case-assignment") end test "an ordinary account cannot open the moderator queue", %{conn: conn} do conn = log_in_account(conn, account_fixture()) assert {:error, {:redirect, %{to: "/"}}} = live(conn, ~p"/moderation/queue") end test "a moderator assigns and resolves a queued report", %{conn: conn} do case_record = report_account!(account_fixture(), account_fixture()) moderator = moderator_account_fixture() conn = log_in_account(conn, moderator) {:ok, queue_view, _html} = live(conn, ~p"/moderation/queue") assert has_element?(queue_view, "#header-moderation-queue") assert has_element?(queue_view, "#moderation-case-count", "1 open") assert has_element?(queue_view, "#assign-moderation-case-#{case_record.id}") queue_view |> element("#assign-moderation-case-#{case_record.id}") |> render_click() assert has_element?(queue_view, "#cases-#{case_record.id}", "In review") {:ok, case_view, _html} = live(conn, ~p"/moderation/cases/#{case_record.id}") assert has_element?(case_view, "#moderation-case-decision-form") case_view |> form("#moderation-case-decision-form", resolution: %{ reason: "The evidence was independently reviewed and confirms a policy violation." } ) |> render_submit(%{"disposition" => "resolved"}) assert has_element?(case_view, "#moderation-case-status", "Resolved") assert has_element?(case_view, "#moderation-case-resolution") refute has_element?(case_view, "#moderation-case-decision-form") end test "the subject can appeal without receiving participant identity data", %{conn: conn} do reporter = account_fixture() subject = account_fixture() resolver = moderator_account_fixture() case_record = reporter |> report_account!(subject) |> resolve_case!(resolver, "resolved") conn = log_in_account(conn, subject) {:ok, view, _html} = live(conn, ~p"/moderation/cases/#{case_record.id}") assert has_element?(view, "#moderation-appeal-form") refute has_element?(view, "#moderation-case-actions") view |> form("#moderation-appeal-form", appeal: %{ reason: "The decision cites evidence from a different account and should be reconsidered." } ) |> render_submit() assert has_element?(view, "#moderation-case-appeals") assert has_element?(view, "#moderation-case-appeals", "Open") refute has_element?(view, "#moderation-appeal-form") refute has_element?(view, "#moderation-case-actions") end test "an independent moderator decides an appeal", %{conn: conn} do reporter = account_fixture() subject = account_fixture() resolver = moderator_account_fixture() appeal_moderator = moderator_account_fixture() case_record = reporter |> report_account!(subject) |> resolve_case!(resolver, "resolved") {:ok, appeal} = Moderation.appeal(Accounts.scope_for_account(subject), case_record, %{ "reason" => "The original decision relied on unrelated evidence and needs independent review." }) conn = log_in_account(conn, appeal_moderator) {:ok, view, _html} = live(conn, ~p"/moderation/cases/#{case_record.id}") assert has_element?(view, "#moderation-appeal-decision-form-#{appeal.id}") view |> form("#moderation-appeal-decision-form-#{appeal.id}", appeal_decision: %{ appeal_id: appeal.id, reason: "Independent review confirms the original evidence was unrelated." } ) |> render_submit(%{"decision" => "upheld"}) assert has_element?(view, "#moderation-case-status", "Overturned") assert has_element?(view, "#moderation-appeal-#{appeal.id}", "Upheld") refute has_element?(view, "#moderation-appeal-decision-form-#{appeal.id}") end test "an expired sudo session redirects resolution to re-authentication", %{conn: conn} do case_record = report_account!(account_fixture(), account_fixture()) moderator = moderator_account_fixture() {:ok, assigned} = Moderation.assign(Scope.for_account(moderator), case_record) # Outside the two-hour sudo window. stale_at = DateTime.add(DateTime.utc_now(), -3, :hour) conn = log_in_account(conn, moderator, token_authenticated_at: stale_at) {:ok, case_view, _html} = live(conn, ~p"/moderation/cases/#{assigned.id}") assert has_element?(case_view, "#moderation-case-decision-form") assert {:error, {:live_redirect, %{to: "/accounts/log-in?return_to=%2Fmoderation%2Fcases%2F" <> _}}} = case_view |> form("#moderation-case-decision-form", resolution: %{ reason: "The evidence was independently reviewed and confirms a violation." } ) |> render_submit(%{"disposition" => "resolved"}) {:ok, unresolved} = Moderation.get_case(Scope.for_account(moderator), assigned.id) assert unresolved.status == "in_review" end defp report_account!(reporter, subject) do {:ok, case_record} = Moderation.report(Scope.for_account(reporter), %{ "subject_type" => "account", "subject_id" => subject.id, "reason" => "fabricated_evidence", "description" => "The submitted evidence appears fabricated and needs independent review." }) case_record end defp resolve_case!(case_record, moderator, disposition) do {:ok, assigned} = Moderation.assign(Scope.for_account(moderator), case_record) {:ok, decided} = Moderation.resolve( Scope.for_account(moderator), assigned, disposition, "The evidence was independently reviewed and supports this moderation decision." ) decided end end