5.5 KiB · text History b868907
defmodule GitGudWeb.SecurityLiveTest do
  @moduledoc """
  A repository's Security page and the switches that turn alerts on:
  who sees the page and its header link, the alert lists, a personal
  repository's and an org's switch (each audited), and a default-branch
  push queueing a scan.
  """
  use GitGudWeb.ConnCase, async: false
  use Oban.Testing, repo: GitGud.Repo

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

  alias GitGud.Advisories
  alias GitGud.Advisories.Alert
  alias GitGud.Advisories.Workers.Scan
  alias GitGud.Organizations
  alias GitGud.Repositories.Storage

  setup do
    # A "scanner" that exists, so the instance reads as able to scan.
    scanner = Path.join(System.tmp_dir!(), "osv-#{System.unique_integer([:positive])}")
    File.write!(scanner, "#!/bin/sh\necho '{}'\n")
    File.chmod!(scanner, 0o755)
    before = Application.get_env(:git_gud, Advisories, [])
    Application.put_env(:git_gud, Advisories, Keyword.merge(before, scanner: scanner))

    on_exit(fn ->
      Application.put_env(:git_gud, Advisories, before)
      File.rm(scanner)
    end)

    {owner, repo} = repository_fixture(%{visibility: "public"})
    repo = GitGud.Repo.preload(repo, [:owner, :organization])
    {:ok, owner: owner, repo: repo, path: "/r/#{Storage.repo_handle(repo)}/#{repo.name}"}
  end

  defp alert!(repo, attrs) do
    %Alert{repository_id: repo.id, first_seen_at: DateTime.utc_now(:second)}
    |> Alert.changeset(
      Map.merge(
        %{
          advisory_id: "GHSA-#{System.unique_integer([:positive])}",
          ecosystem: "Hex",
          package: "plug",
          version: "1.19.1",
          manifest_path: "mix.lock",
          severity: 8.7,
          fixed_versions: ["1.19.5"]
        },
        attrs
      )
    )
    |> Ecto.Changeset.change(Map.take(attrs, [:state]))
    |> GitGud.Repo.insert!()
  end

  describe "the page" do
    setup %{repo: repo} do
      {:ok, repo} = Advisories.set_repo(repo, true)
      {:ok, repo: repo}
    end

    test "lists open alerts, and the fixed ones on their tab", %{
      conn: conn,
      owner: owner,
      repo: repo,
      path: path
    } do
      open = alert!(repo, %{package: "plug"})
      fixed = alert!(repo, %{package: "mint", state: "fixed"})
      {:ok, view, _} = live(log_in_user(conn, owner), path <> "/security")

      assert has_element?(view, "#alert-#{open.id}", "plug")
      refute has_element?(view, "#alert-#{fixed.id}")

      view |> element("#security-tab-fixed") |> render_click()
      assert_patch(view, path <> "/security/fixed")
      assert has_element?(view, "#alert-#{fixed.id}", "mint")
    end

    test "the header links it, with the open count, for whoever may look", %{
      conn: conn,
      owner: owner,
      repo: repo,
      path: path
    } do
      alert!(repo, %{})
      {:ok, view, _} = live(log_in_user(conn, owner), path <> "/issues")
      assert has_element?(view, "#repo-security-link")

      # A reader of the public repository sees neither.
      {:ok, view, _} = live(log_in_user(build_conn(), user_fixture()), path <> "/issues")
      refute has_element?(view, "#repo-security-link")

      assert {:error, {:redirect, %{to: ^path}}} =
               live(log_in_user(build_conn(), user_fixture()), path <> "/security")
    end

    test "a state it doesn't know goes back to the open list", %{
      conn: conn,
      owner: owner,
      path: path
    } do
      assert {:error, {:live_redirect, %{to: to}}} =
               live(log_in_user(conn, owner), path <> "/security/whatever")

      assert to == path <> "/security"
    end
  end

  describe "the switches" do
    test "a personal repository's, in its settings, audited", %{
      conn: conn,
      owner: owner,
      repo: repo,
      path: path
    } do
      {:ok, view, _} = live(log_in_user(conn, owner), path <> "/settings")
      view |> element("#repo-advisories-toggle") |> render_click()

      assert GitGud.Repo.reload!(repo).advisories_enabled
      assert_enqueued(worker: Scan, args: %{"repository_id" => repo.id})

      assert [%{action: "repo.advisories_enabled"}] =
               GitGud.Repo.all(from e in GitGud.Audit.Entry, where: e.repository_id == ^repo.id)
    end

    test "an org's, in its settings, for its admins", %{conn: conn, owner: owner} do
      handle = "sec#{System.unique_integer([:positive])}"

      {:ok, org} =
        Organizations.create_organization(owner, %{"handle" => handle, "name" => handle})

      {:ok, repo} = GitGud.Repositories.create_repository_for_org(org, owner, %{"name" => "svc"})

      {:ok, view, _} = live(log_in_user(conn, owner), ~p"/orgs/#{handle}/settings/profile")
      view |> element("#org-advisories-toggle") |> render_click()

      assert GitGud.Repo.reload!(org).advisories_enabled
      assert_enqueued(worker: Scan, args: %{"repository_id" => repo.id, "force" => true})

      assert [%{action: "org.advisories_enabled"}] =
               GitGud.Repo.all(from e in GitGud.Audit.Entry, where: e.organization_id == ^org.id)
    end
  end

  test "a push to the default branch queues a scan where alerts are on", %{repo: repo} do
    {:ok, repo} = Advisories.set_repo(repo, true)
    Oban.drain_queue(queue: :advisories)

    perform_job(GitGud.Repositories.Workers.CommitBackfill, %{
      "repository_id" => repo.id,
      "ref" => "refs/heads/#{repo.default_branch}",
      "old_sha_hex" => String.duplicate("0", 40),
      "new_sha_hex" => String.duplicate("0", 40)
    })

    assert_enqueued(worker: Scan, args: %{"repository_id" => repo.id, "force" => false})
  end
end