test/support/forge_promotion_fixtures.ex

58e6347eeb72 · 3 KB

defmodule OpenAgents.ForgePromotionFixtures do
  @moduledoc """
  A real bare forge repository with one real commit.

  Promotion's precondition is that the SHA is already in the WAL-backed
  repository, and `OpenAgents.Forge.Targets` deliberately runs that check in
  every environment including test. A fixture that stubbed it would prove
  nothing, so this one writes an actual commit object with Git plumbing.
  """

  alias OpenAgents.Accounts
  alias OpenAgents.Forge.Repos

  @doc "An account that is a current operator for the duration of the test."
  def operator_fixture(key) do
    user = promotion_user_fixture(key)
    grant_operator(user)
    user
  end

  @doc "An ordinary active account that is never an operator."
  def promotion_user_fixture(key) do
    digest = :crypto.hash(:sha256, key)
    github_id = digest |> binary_part(0, 7) |> :binary.decode_unsigned()
    suffix = digest |> Base.encode16(case: :lower) |> binary_part(0, 12)

    {:ok, user} =
      Accounts.upsert_github_user(%{
        github_id: github_id,
        github_login: "promotion-#{suffix}",
        github_avatar_url: "https://avatars.githubusercontent.com/u/#{github_id}?v=4"
      })

    user
  end

  # The operator allowlist is one application-wide value shared by every
  # concurrently running test, so a grant adds only its own id and its cleanup
  # removes only that id.
  @doc "Add one account to the operator allowlist until the test ends."
  def grant_operator(%{github_id: github_id}) do
    update_operator_ids(&[github_id | &1])
    ExUnit.Callbacks.on_exit(fn -> update_operator_ids(&List.delete(&1, github_id)) end)
    :ok
  end

  @doc "Remove one account from the operator allowlist immediately."
  def revoke_operator(%{github_id: github_id}) do
    update_operator_ids(&List.delete(&1, github_id))
  end

  defp update_operator_ids(fun) do
    :global.trans({{:openagents, :admin_github_ids}, self()}, fn ->
      ids = Application.get_env(:openagents, :admin_github_ids, [])
      Application.put_env(:openagents, :admin_github_ids, fun.(ids))
    end)

    :ok
  end

  @doc "Point the forge data and WAL directories at a private temporary tree."
  def isolate_forge_storage! do
    base = Path.join(System.tmp_dir!(), "forge-promotion-#{System.unique_integer([:positive])}")
    File.mkdir_p!(base)
    previous_data = Application.get_env(:openagents, :forge_data_dir)
    previous_wal = Application.get_env(:openagents, :forge_wal_dir)
    Application.put_env(:openagents, :forge_data_dir, Path.join(base, "data"))
    Application.put_env(:openagents, :forge_wal_dir, Path.join(base, "wal"))

    ExUnit.Callbacks.on_exit(fn ->
      Application.put_env(:openagents, :forge_data_dir, previous_data)
      Application.put_env(:openagents, :forge_wal_dir, previous_wal)
      File.rm_rf(base)
    end)

    :ok
  end

  @doc "Create one commit in `repo`'s bare forge repository and return its SHA."
  def seeded_commit(repo, message \\ "seed") do
    path = Repos.ensure_repo!(repo)
    {blob, 0} = plumb(path, ["hash-object", "-w", "--stdin"], "content #{message}\n")
    {tree, 0} = plumb(path, ["mktree"], "100644 blob #{String.trim(blob)}\tf.txt\n")

    {commit, 0} =
      plumb(path, ["commit-tree", String.trim(tree), "-m", message], "",
        env: [
          {"GIT_AUTHOR_NAME", "t"},
          {"GIT_AUTHOR_EMAIL", "t@t"},
          {"GIT_COMMITTER_NAME", "t"},
          {"GIT_COMMITTER_EMAIL", "t@t"}
        ]
      )

    sha = String.trim(commit)
    {_, 0} = Repos.git(path, ["update-ref", "refs/heads/main", sha])
    sha
  end

  defp plumb(path, args, stdin, opts \\ []) do
    input = Path.join(System.tmp_dir!(), "plumb-#{System.unique_integer([:positive])}")
    File.write!(input, stdin)

    try do
      System.cmd(
        "sh",
        ["-c", ~s(exec git --git-dir "$GD" "$@" < "$IN"), "sh"] ++ args,
        env: [{"GD", path}, {"IN", input}] ++ Keyword.get(opts, :env, [])
      )
    after
      File.rm(input)
    end
  end
end