test/openagents_web/controllers/pull_request_controller_test.exs

58e6347eeb72 · 5 KB

defmodule OpenAgentsWeb.PullRequestControllerTest do
  use OpenAgentsWeb.ConnCase

  alias OpenAgents.Forge.Repos
  alias OpenAgents.Repositories

  setup do
    base =
      Path.join(
        System.tmp_dir!(),
        "pull-request-controller-#{System.unique_integer([:positive])}"
      )

    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"))

    on_exit(fn ->
      restore_env(:forge_data_dir, previous_data)
      restore_env(:forge_wal_dir, previous_wal)
      File.rm_rf(base)
    end)

    :ok
  end

  test "POST, GET, and PATCH /api/v1/repos/:owner/:repo/pulls manage a pull request", %{
    conn: conn
  } do
    target = repository_fixture()
    source = repository_fixture()
    seed_repository(target)
    seed_repository(source)

    user = github_user("api-token-pull-request-lifecycle")
    {:ok, _membership} = Repositories.add_member(source, user, "owner")
    conn = put_forge_api_token(conn, "pull-request-lifecycle", target)

    create_conn =
      post(conn, "/api/v1/repos/#{target.owner}/#{target.name}/pulls", %{
        title: "Add pull request support",
        body: "Issue-backed pull request",
        head_repository: "#{source.owner}/#{source.name}",
        head: "main",
        base: "main"
      })

    assert %{
             "number" => number,
             "title" => "Add pull request support",
             "state" => "open",
             "draft" => true,
             "head" => %{"ref" => "main"},
             "base" => %{"ref" => "main"}
           } = json_response(create_conn, 201)

    show_conn = get(conn, "/api/v1/repos/#{target.owner}/#{target.name}/pulls/#{number}")

    assert %{"number" => ^number, "title" => "Add pull request support"} =
             json_response(show_conn, 200)

    update_conn =
      patch(conn, "/api/v1/repos/#{target.owner}/#{target.name}/pulls/#{number}", %{
        title: "Ship pull request support",
        state: "closed",
        draft: false
      })

    assert %{
             "number" => ^number,
             "title" => "Ship pull request support",
             "state" => "closed",
             "draft" => false
           } = json_response(update_conn, 200)
  end

  test "POST /api/v1/repos/:owner/:repo/pulls rejects a disabled repository", %{conn: conn} do
    target = repository_fixture(%{pull_requests_enabled: false})
    conn = put_forge_api_token(conn, "pull-request-disabled", target)

    conn =
      post(conn, "/api/v1/repos/#{target.owner}/#{target.name}/pulls", %{
        title: "Cannot open",
        head_repository: "#{target.owner}/#{target.name}",
        head: "main"
      })

    assert %{"message" => "Pull requests are disabled for this repository."} =
             json_response(conn, 409)
  end

  test "POST /api/v1/repos/:owner/:repo/pulls requires a bearer token", %{conn: conn} do
    repository = repository_fixture()

    conn =
      post(conn, "/api/v1/repos/#{repository.owner}/#{repository.name}/pulls", %{
        title: "Unauthenticated pull request"
      })

    assert %{"error" => "invalid_api_token"} = json_response(conn, 401)
  end

  test "GET /api/v1/repos/:owner/:repo/pulls lists pull requests", %{conn: conn} do
    repository = repository_fixture()

    conn =
      get(conn, "/api/v1/repos/#{repository.owner}/#{repository.name}/pulls")

    assert [] == json_response(conn, 200)
  end

  test "PATCH /api/v1/repos/:owner/:repo updates the owner pull request setting", %{conn: conn} do
    repository = repository_fixture()
    conn = put_forge_api_token(conn, "pull-request-settings", repository)

    conn =
      patch(conn, "/api/v1/repos/#{repository.owner}/#{repository.name}", %{
        pull_requests_enabled: false
      })

    assert %{"pull_requests_enabled" => false} = json_response(conn, 200)
  end

  test "PATCH /api/v1/repos/:owner/:repo requires an owner", %{conn: conn} do
    repository = repository_fixture()
    conn = put_forge_api_token(conn, "pull-request-nonowner")

    conn =
      patch(conn, "/api/v1/repos/#{repository.owner}/#{repository.name}", %{
        pull_requests_enabled: false
      })

    assert %{"code" => "forbidden"} = json_response(conn, 403)
  end

  defp seed_repository(repository) do
    path = Repos.ensure_repo!(repository.storage_key, repository.default_branch)
    blob = git!(path, ["hash-object", "-w", "--stdin"], "fixture\n")
    tree = git!(path, ["mktree"], "100644 blob #{blob}\tREADME.md\n")

    commit =
      git!(path, ["commit-tree", tree, "-m", "Seed repository"], "",
        env: [
          {"GIT_AUTHOR_NAME", "Test Author"},
          {"GIT_AUTHOR_EMAIL", "author@example.test"},
          {"GIT_COMMITTER_NAME", "Test Author"},
          {"GIT_COMMITTER_EMAIL", "author@example.test"}
        ]
      )

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

  defp git!(git_dir, args, input, options \\ []) do
    input_path =
      Path.join(
        System.tmp_dir!(),
        "pull-request-controller-input-#{System.unique_integer([:positive])}"
      )

    File.write!(input_path, input)

    try do
      {output, 0} =
        System.cmd(
          "sh",
          ["-c", ~s(exec git --git-dir "$GIT_DIR" "$@" < "$INPUT"), "sh"] ++ args,
          env: [{"GIT_DIR", git_dir}, {"INPUT", input_path}] ++ Keyword.get(options, :env, [])
        )

      String.trim(output)
    after
      File.rm(input_path)
    end
  end

  defp restore_env(key, nil), do: Application.delete_env(:openagents, key)
  defp restore_env(key, value), do: Application.put_env(:openagents, key, value)
end