6.2 KiB · text History b868907
defmodule GitGudWeb.AuditLive.AdminTest do
  @moduledoc """
  `/admin/audit`: everything, for instance admins in sudo mode, with
  each entry's address and user agent — and the instance's own actions
  (federation, registration invites) that show nowhere else.
  """
  use GitGudWeb.ConnCase, async: false

  import Phoenix.LiveViewTest
  import GitGud.AccountsFixtures

  alias GitGud.Accounts.Scope
  alias GitGud.Audit
  alias GitGud.Audit.Entry
  alias GitGud.Organizations

  setup %{conn: conn} do
    admin = user_fixture() |> GitGud.Accounts.User.admin_changeset(true) |> GitGud.Repo.update!()
    {:ok, admin: admin, conn: log_in_user(conn, admin)}
  end

  defp org_fixture(owner) do
    handle = "ad#{System.unique_integer([:positive])}"
    {:ok, org} = Organizations.create_organization(owner, %{"handle" => handle, "name" => handle})
    org
  end

  defp entries, do: GitGud.Repo.all(Entry)

  describe "the page" do
    test "every org's entries and the instance's, with where they came from", %{
      conn: conn,
      admin: admin
    } do
      owner = user_fixture()
      org = org_fixture(owner)

      scope = owner |> Scope.for_user() |> Scope.put_request({203, 0, 113, 50}, "OrgBrowser/2")
      in_org = Audit.record(scope, "org_secret.set", %{organization: org, target_label: "TOK"})

      instance =
        Audit.record(Scope.for_user(admin), "invite.created", %{target_label: "invite #1"})

      {:ok, view, html} = live(conn, ~p"/admin/audit")

      assert has_element?(view, "#audit-entry-#{in_org.id}")
      assert has_element?(view, "#audit-entry-#{instance.id}")
      assert html =~ "203.0.113.50"
      assert html =~ "OrgBrowser/2"
    end

    test "by organization, and instance-wide only", %{conn: conn, admin: admin} do
      [a, b] = [org_fixture(user_fixture()), org_fixture(user_fixture())]
      scope = Scope.for_user(admin)
      in_a = Audit.record(scope, "org_secret.set", %{organization: a, target_label: "A"})
      in_b = Audit.record(scope, "org_secret.set", %{organization: b, target_label: "B"})
      mine = Audit.record(scope, "invite.created", %{target_label: "invite #1"})

      {:ok, view, _} = live(conn, ~p"/admin/audit/all/org/#{a.handle}")
      assert has_element?(view, "#audit-entry-#{in_a.id}")
      refute has_element?(view, "#audit-entry-#{in_b.id}")
      refute has_element?(view, "#audit-entry-#{mine.id}")

      {:ok, view, _} = live(conn, ~p"/admin/audit")

      view
      |> form("#audit-filter", %{"filter" => %{"scope" => "true"}})
      |> render_submit()

      assert_patch(view, ~p"/admin/audit/all/scope/instance")
      assert has_element?(view, "#audit-entry-#{mine.id}")
      refute has_element?(view, "#audit-entry-#{in_a.id}")
    end

    test "not for anyone else, and only in sudo mode", %{conn: conn, admin: admin} do
      assert {:error, {:redirect, %{to: "/"}}} =
               live(log_in_user(build_conn(), user_fixture()), ~p"/admin/audit")

      stale = DateTime.add(DateTime.utc_now(:second), -30, :minute)
      conn = log_in_user(conn, admin, token_authenticated_at: stale)
      assert {:error, {:redirect, %{to: to}}} = live(conn, ~p"/admin/audit")
      assert to =~ "/users/log-in?"
    end

    test "the dashboard links to it", %{conn: conn} do
      {:ok, view, _} = live(conn, ~p"/")
      assert has_element?(view, ~s(a[href="/admin/audit"]))
    end
  end

  describe "federation admin" do
    setup %{conn: conn} do
      {:ok, view, _} = live(conn, ~p"/admin/federation")
      {:ok, view: view}
    end

    test "blocking and unblocking an instance, and unblocking it again", %{view: view} do
      render_click(view, "block_instance", %{"host" => "bad.example", "reason" => "spam"})
      render_click(view, "unblock_instance", %{"host" => "bad.example"})
      render_click(view, "unblock_instance", %{"host" => "bad.example"})

      assert [blocked, unblocked] = Enum.sort_by(entries(), & &1.id)
      assert {blocked.action, blocked.target_label} == {"instance.blocked", "bad.example"}
      assert blocked.metadata == %{"reason" => "spam", "severity" => "suspend"}
      assert unblocked.action == "instance.unblocked"
      assert {blocked.organization_id, blocked.repository_id} == {nil, nil}
    end

    test "the allowlist, added once", %{view: view} do
      render_click(view, "allowlist", %{"host" => "good.example"})
      render_click(view, "allowlist", %{"host" => "good.example"})
      render_click(view, "unallowlist", %{"host" => "good.example"})

      assert ~w(instance.allowlisted instance.unallowlisted) ==
               entries() |> Enum.sort_by(& &1.id) |> Enum.map(& &1.action)
    end

    test "a remote actor", %{view: view} do
      url = "https://bad.example/users/troll"
      render_click(view, "block_actor", %{"url" => url, "reason" => ""})
      render_click(view, "unblock_actor", %{"url" => url})

      assert ~w(remote_actor.blocked remote_actor.unblocked) ==
               entries() |> Enum.sort_by(& &1.id) |> Enum.map(& &1.action)

      assert Enum.all?(entries(), &(&1.target_label == url))
    end

    test "report decisions, and replaced content restored", %{view: view} do
      {owner, repo} = GitGud.ForgeFixtures.repository_fixture(%{visibility: "public"})
      issue = GitGud.ForgeFixtures.issue_fixture(repo, owner, %{"body" => "words"})
      {:ok, one} = GitGud.Reports.open(user_fixture(), {"issue", issue.id}, "spam")
      {:ok, two} = GitGud.Reports.open(user_fixture(), {"issue", issue.id}, "abuse")

      render_click(view, "dismiss_report", %{"id" => to_string(one.id)})
      render_click(view, "moderate_content", %{"report_id" => to_string(two.id), "note" => ""})
      render_click(view, "restore_content", %{"id" => to_string(two.id)})

      assert ~w(report.dismissed report.content_moderated report.content_restored) ==
               entries() |> Enum.sort_by(& &1.id) |> Enum.map(& &1.action)
    end
  end

  test "registration invites, never the link", %{conn: conn} do
    {:ok, view, _} = live(conn, ~p"/users/settings/invites")
    render_click(view, "create_invite", %{})
    [invite] = GitGud.Repo.all(GitGud.Accounts.Invite)
    render_click(view, "revoke_invite", %{"id" => to_string(invite.id)})

    assert ~w(invite.created invite.revoked) ==
             entries() |> Enum.sort_by(& &1.id) |> Enum.map(& &1.action)

    refute inspect(entries()) =~ invite.token
  end
end