test/openagents_web/controllers/issue_dependency_controller_test.exs

58e6347eeb72 · 6 KB

defmodule OpenAgentsWeb.IssueDependencyControllerTest do
  use OpenAgentsWeb.ConnCase

  alias OpenAgents.Issues
  alias OpenAgents.Repositories

  setup %{conn: conn}, do: {:ok, conn: put_forge_api_token(conn, "dependencies", repository())}

  describe "create" do
    test "POST records prerequisites and answers with the resulting graph", %{conn: conn} do
      {:ok, blocked} = Issues.create_issue(repository(), %{title: "Aim agents at the backlog"})
      {:ok, blocker} = Issues.create_issue(repository(), %{title: "Deliver the work system"})

      conn = post(conn, dependencies_path(blocked), %{blocked_by: [blocker.number]})

      assert %{
               "blocked" => true,
               "blocked_by" => [
                 %{
                   "number" => number,
                   "title" => "Deliver the work system",
                   "state" => "open"
                 }
               ],
               "blocks" => []
             } = json_response(conn, 201)

      assert number == blocker.number
    end

    test "POST refuses a prerequisite that does not exist", %{conn: conn} do
      {:ok, blocked} = Issues.create_issue(repository(), %{title: "Waiting"})

      conn = post(conn, dependencies_path(blocked), %{blocked_by: [999_999]})

      assert %{"errors" => %{"blocked_by" => [message]}} = json_response(conn, 422)
      assert message =~ "does not exist in this repository"
    end

    test "POST refuses a self reference", %{conn: conn} do
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Alone"})

      conn = post(conn, dependencies_path(issue), %{blocked_by: [issue.number]})

      assert %{"errors" => %{"blocked_by" => [message]}} = json_response(conn, 422)
      assert message =~ "cannot be a prerequisite of itself"
    end

    test "POST refuses an edge that would close a cycle", %{conn: conn} do
      {:ok, first} = Issues.create_issue(repository(), %{title: "First"})
      {:ok, second} = Issues.create_issue(repository(), %{title: "Second"})
      assert :ok = Issues.add_dependencies(second, [first.number])

      conn = post(conn, dependencies_path(first), %{blocked_by: [second.number]})

      assert %{"errors" => %{"blocked_by" => [message]}} = json_response(conn, 422)
      assert message =~ "Would create a dependency cycle"
    end

    test "POST refuses a body without a list of numbers", %{conn: conn} do
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Waiting"})

      conn = post(conn, dependencies_path(issue), %{blocked_by: "12"})

      assert %{"errors" => %{"blocked_by" => [message]}} = json_response(conn, 422)
      assert message =~ "must be a list of issue numbers"
    end

    test "POST answers 404 for an issue that does not exist", %{conn: conn} do
      conn =
        post(
          conn,
          "/api/v1/repos/OpenAgentsInc/openagents.com/issues/999999/dependencies",
          %{blocked_by: []}
        )

      assert json_response(conn, 404)
    end
  end

  describe "index" do
    test "GET reads both directions of the graph", %{conn: conn} do
      {:ok, blocked} = Issues.create_issue(repository(), %{title: "Waiting"})
      {:ok, blocker} = Issues.create_issue(repository(), %{title: "Prerequisite"})
      assert :ok = Issues.add_dependencies(blocked, [blocker.number])

      assert %{"blocked" => true, "blocked_by" => [_one], "blocks" => []} =
               get(conn, dependencies_path(blocked)) |> json_response(200)

      assert %{"blocked" => false, "blocked_by" => [], "blocks" => [_one]} =
               get(conn, dependencies_path(blocker)) |> json_response(200)
    end

    test "GET reports an issue with no prerequisites as unblocked", %{conn: conn} do
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Ready"})

      assert %{"blocked" => false, "blocked_by" => [], "blocks" => []} =
               get(conn, dependencies_path(issue)) |> json_response(200)
    end
  end

  describe "delete" do
    test "DELETE removes one prerequisite", %{conn: conn} do
      {:ok, blocked} = Issues.create_issue(repository(), %{title: "Waiting"})
      {:ok, blocker} = Issues.create_issue(repository(), %{title: "Prerequisite"})
      assert :ok = Issues.add_dependencies(blocked, [blocker.number])

      conn = delete(conn, dependencies_path(blocked) <> "/#{blocker.number}")

      assert %{"blocked" => false, "blocked_by" => []} = json_response(conn, 200)
    end

    test "DELETE answers 404 when the edge is not recorded", %{conn: conn} do
      {:ok, blocked} = Issues.create_issue(repository(), %{title: "Waiting"})
      {:ok, other} = Issues.create_issue(repository(), %{title: "Unrelated"})

      conn = delete(conn, dependencies_path(blocked) <> "/#{other.number}")

      assert %{"message" => message} = json_response(conn, 404)
      assert message =~ "is not a prerequisite of this issue"
    end
  end

  describe "authorization" do
    test "an anonymous visitor cannot read the graph of a private repository" do
      private_repository = repository_fixture(%{visibility: "private"})
      {:ok, issue} = Issues.create_issue(private_repository, %{title: "Private"})

      path =
        "/api/v1/repos/#{private_repository.owner}/#{private_repository.name}" <>
          "/issues/#{issue.number}/dependencies"

      assert get(build_conn(), path) |> json_response(404)

      member_conn = put_forge_api_token(build_conn(), "private-graph", private_repository)
      assert %{"blocked" => false} = get(member_conn, path) |> json_response(200)
    end

    test "a bearer without write access cannot record a prerequisite" do
      other_repository = repository_fixture()
      {:ok, blocked} = Issues.create_issue(other_repository, %{title: "Waiting"})
      {:ok, blocker} = Issues.create_issue(other_repository, %{title: "Prerequisite"})

      conn = put_forge_api_token(build_conn(), "dependency-nonmember")

      path =
        "/api/v1/repos/#{other_repository.owner}/#{other_repository.name}" <>
          "/issues/#{blocked.number}/dependencies"

      assert post(conn, path, %{blocked_by: [blocker.number]}) |> json_response(404)
      assert %{blocked_by: []} = Issues.dependencies(blocked)
    end

    test "an anonymous visitor cannot record a prerequisite" do
      {:ok, blocked} = Issues.create_issue(repository(), %{title: "Waiting"})
      {:ok, blocker} = Issues.create_issue(repository(), %{title: "Prerequisite"})

      conn = post(build_conn(), dependencies_path(blocked), %{blocked_by: [blocker.number]})

      assert conn.status in [401, 404]
      assert %{blocked_by: []} = Issues.dependencies(blocked)
    end
  end

  defp dependencies_path(issue) do
    "/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/dependencies"
  end

  defp repository do
    Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
  end
end