5.8 KiB · text History b868907
defmodule GitGudWeb.UserRunnersLiveTest do
  @moduledoc """
  /users/settings/runners: a user mints, lists and removes their own
  runners, and only theirs. Their personal repositories' runners pages
  show those runners as inherited; the admin page shows whose they are.
  """
  use GitGudWeb.ConnCase, async: false

  import Ecto.Query
  import Phoenix.LiveViewTest
  import GitGud.AccountsFixtures
  import GitGud.ForgeFixtures

  alias GitGud.Audit.Entry
  alias GitGud.Repositories.Storage
  alias GitGud.Workflows.Runner
  alias GitGud.Workflows.Runners

  defp entries, do: GitGud.Repo.all(from e in Entry, order_by: e.id)

  defp page(conn, user) do
    {:ok, lv, _} = live(log_in_user(conn, user), ~p"/users/settings/runners")
    lv
  end

  test "renders, and is linked from the account menu", %{conn: conn} do
    user = user_fixture()
    lv = page(conn, user)

    assert has_element?(lv, "#new-runner-form")
    assert has_element?(lv, "#mint-token")
    assert has_element?(lv, "#nav-runners[href='/users/settings/runners']")
  end

  test "mints a registration token for the user, shown once and audited", %{conn: conn} do
    user = user_fixture()
    lv = page(conn, user)

    lv |> element("#mint-token") |> render_click()

    token = :sys.get_state(lv.pid).socket.assigns.new_token
    assert has_element?(lv, "#new-reg-token", token)
    assert has_element?(lv, "#reg-token-terms")

    assert {:ok, runner, _} = Runners.register_with_token(token, %{name: "laptop"})
    assert runner.owner_id == user.id

    assert [minted] = entries()
    assert minted.action == "runner.registration_token_minted"
    assert minted.subject_user_id == user.id
    refute inspect(minted) =~ token
  end

  test "creates a runner offline, lists it, and removes it", %{conn: conn} do
    user = user_fixture()
    lv = page(conn, user)

    lv
    |> form("#new-runner-form", %{
      "new_runner" => %{"name" => "laptop", "labels" => "self-hosted"}
    })
    |> render_submit()

    runner = GitGud.Repo.get_by!(Runner, name: "laptop")
    assert runner.owner_id == user.id
    assert has_element?(lv, "#new-runner-config")
    assert has_element?(lv, "#runner-row-#{runner.id}", "user")

    lv |> element("#runner-remove-#{runner.id}") |> render_click()

    refute GitGud.Repo.get(Runner, runner.id)
    refute has_element?(lv, "#runner-row-#{runner.id}")

    assert [created, deleted] = entries()
    assert {created.action, deleted.action} == {"runner.created", "runner.deleted"}
    assert Enum.all?([created, deleted], &(&1.subject_user_id == user.id))
  end

  test "sets a runner's architecture", %{conn: conn} do
    user = user_fixture()
    {:ok, runner, _, _} = Runners.create_offline(%{name: "pi"}, user)
    lv = page(conn, user)

    lv
    |> form("#runner-architecture-#{runner.id}", %{"runner" => %{"architecture" => "aarch64"}})
    |> render_change()

    assert GitGud.Repo.reload!(runner).architecture == "aarch64"
  end

  test "another user's runners are neither shown nor removable", %{conn: conn} do
    owner = user_fixture()
    {:ok, theirs, _, _} = Runners.create_offline(%{name: "theirs"}, owner)
    {:ok, global, _, _} = Runners.create_offline(%{name: "pool"})

    lv = page(conn, user_fixture())

    refute has_element?(lv, "#runner-row-#{theirs.id}")
    # The global runner is listed, but not this page's to change.
    assert has_element?(lv, "#runner-row-#{global.id}")
    refute has_element?(lv, "#runner-remove-#{global.id}")

    for r <- [theirs, global] do
      render_click(lv, "delete_runner", %{"id" => to_string(r.id)})
      assert GitGud.Repo.get(Runner, r.id)

      render_change(lv, "set_architecture", %{
        "runner" => %{"id" => to_string(r.id), "architecture" => "aarch64"}
      })

      assert GitGud.Repo.reload!(r).architecture == "x86_64"
    end

    assert entries() == []
  end

  test "one of the user's runners connecting shows without a reload", %{conn: conn} do
    user = user_fixture()
    {:ok, runner, _, _} = Runners.create_offline(%{name: "caprica"}, user)
    lv = page(conn, user)

    {:ok, _} = Runners.touch_last_seen(runner)

    assert has_element?(lv, "#runner-row-#{runner.id}", "last seen")
    assert has_element?(lv, "#flash-info", "Runner 'caprica' connected.")
  end

  test "a personal repository's runners page lists its owner's runners, read-only",
       %{conn: conn} do
    {owner, repo} = repository_fixture()
    repo = GitGud.Repo.preload(repo, [:owner, :organization])
    {:ok, runner, _, _} = Runners.create_offline(%{name: "laptop"}, owner)

    {:ok, lv, _} =
      live(
        log_in_user(conn, owner),
        "/r/#{Storage.repo_handle(repo)}/#{repo.name}/settings/runners"
      )

    assert has_element?(lv, "#runner-row-#{runner.id}", "user")
    refute has_element?(lv, "#runner-architecture-#{runner.id}")
    assert has_element?(lv, "#own-runners-link")

    render_click(lv, "delete_runner", %{"id" => to_string(runner.id)})
    assert GitGud.Repo.get(Runner, runner.id)

    # And hears it connect.
    {:ok, _} = Runners.touch_last_seen(runner)
    assert has_element?(lv, "#flash-info", "Runner 'laptop' connected.")
  end

  test "the admin page shows a user's runner and whose it is", %{conn: conn} do
    admin = user_fixture() |> GitGud.Accounts.User.admin_changeset(true) |> GitGud.Repo.update!()
    owner = user_fixture()
    {:ok, runner, _, _} = Runners.create_offline(%{name: "laptop"}, owner)

    {:ok, lv, _} = live(log_in_user(conn, admin), ~p"/admin/runners")

    assert has_element?(lv, "#runner-row-#{runner.id}", "user")
    assert has_element?(lv, "#runner-row-#{runner.id} a[href='/u/#{owner.handle}']")

    lv |> element("#runner-remove-#{runner.id}") |> render_click()
    refute GitGud.Repo.get(Runner, runner.id)

    # Recorded in the owner's security log.
    assert [%Entry{action: "runner.deleted", subject_user_id: uid}] = entries()
    assert uid == owner.id
  end
end