test/openagents_web/controllers/private_issue_metadata_test.exs

58e6347eeb72 · 7 KB

defmodule OpenAgentsWeb.PrivateIssueMetadataTest do
  @moduledoc """
  Issue metadata reads for a private repository, from three principals.

  The six ancillary issue families — comments, labels, milestones, assignees,
  issue labels, and issue assignees — resolved through a public-only query, so
  a private repository answered `404` to its own owner. These tests fix the
  three answers that matter: a member reads their own records, an anonymous
  caller sees what it saw before, and a non-member cannot tell a private
  repository from one that does not exist.
  """

  use OpenAgentsWeb.ConnCase

  import OpenAgents.IssuesFixtures
  import OpenAgents.LabelsFixtures
  import OpenAgents.MilestonesFixtures

  alias OpenAgents.Issues
  alias OpenAgents.Repositories

  setup %{conn: conn} do
    owner = github_user("private-metadata-owner", "metadata-owner")

    repository =
      repository_with_member_fixture(owner, %{
        owner: "metadata-owner",
        name: "private-work",
        visibility: "private"
      })

    issue = issue_fixture(repository, %{title: "a private issue"})
    {:ok, comment} = Issues.create_comment(issue, %{body: "a comment of mine"}, owner)
    {:ok, issue} = Issues.add_labels(issue, ["mine"], owner)
    {:ok, issue} = Issues.add_assignees(issue, [owner.github_login], owner)
    label = label_fixture(repository, %{name: "shipped", color: "0075ca"})
    milestone = milestone_fixture(repository, %{title: "first release", number: 1})

    %{
      owner: owner,
      repository: repository,
      issue: issue,
      comment: comment,
      label: label,
      milestone: milestone,
      member_conn: put_forge_api_token(conn, "private-metadata-member", repository),
      outsider_conn: put_forge_api_token(conn, "private-metadata-outsider")
    }
  end

  defp paths(%{issue: issue, comment: comment, label: label, milestone: milestone, owner: owner}) do
    base = "/api/v1/repos/metadata-owner/private-work"

    %{
      "issue comments" => "#{base}/issues/#{issue.number}/comments",
      "one comment" => "#{base}/issues/comments/#{comment.id}",
      "issue labels" => "#{base}/issues/#{issue.number}/labels",
      "issue assignees" => "#{base}/issues/#{issue.number}/assignees",
      "labels" => "#{base}/labels",
      "one label" => "#{base}/labels/#{label.name}",
      "milestones" => "#{base}/milestones",
      "one milestone" => "#{base}/milestones/#{milestone.number}",
      "assignees" => "#{base}/assignees",
      "one assignee" => "#{base}/assignees/#{owner.github_login}"
    }
  end

  describe "a member of a private repository" do
    test "reads every issue metadata family with a bearer token", context do
      for {name, path} <- paths(context) do
        conn = get(context.member_conn, path)

        assert conn.status == 200,
               "#{name} answered #{conn.status} to a member of the private repository"
      end
    end

    test "reads the records themselves, not an empty envelope", context do
      %{member_conn: conn, issue: issue, comment: comment} = context

      assert %{"comments" => [read]} =
               conn
               |> get("/api/v1/repos/metadata-owner/private-work/issues/#{issue.number}/comments")
               |> json_response(200)

      assert read["id"] == comment.id
      assert read["body"] == "a comment of mine"

      assert %{"labels" => labels} =
               conn
               |> get("/api/v1/repos/metadata-owner/private-work/labels")
               |> json_response(200)

      assert "shipped" in Enum.map(labels, & &1["name"])

      assert %{"milestones" => [milestone]} =
               conn
               |> get("/api/v1/repos/metadata-owner/private-work/milestones")
               |> json_response(200)

      assert milestone["title"] == "first release"

      assert %{"assignees" => assignees} =
               conn
               |> get("/api/v1/repos/metadata-owner/private-work/assignees")
               |> json_response(200)

      assert context.owner.github_login in Enum.map(assignees, & &1["login"])

      assert %{"labels" => [issue_label]} =
               conn
               |> get("/api/v1/repos/metadata-owner/private-work/issues/#{issue.number}/labels")
               |> json_response(200)

      assert issue_label["name"] == "mine"
    end
  end

  describe "a caller with no claim on the repository" do
    test "an anonymous caller cannot tell the private repository from a missing one", context do
      for {name, path} <- paths(context) do
        absent = String.replace(path, "/metadata-owner/private-work", "/nobody/nonexistent")

        private_body = build_conn() |> get(path) |> assert_api_error(404, "not_found")
        absent_body = build_conn() |> get(absent) |> assert_api_error(404, "not_found")

        assert Map.delete(private_body, "request_id") == Map.delete(absent_body, "request_id"),
               "#{name} answers a private repository differently from an absent one"
      end
    end

    test "a member of another repository gets the same refusal", context do
      for {name, path} <- paths(context) do
        absent = String.replace(path, "/metadata-owner/private-work", "/nobody/nonexistent")

        private_body = context.outsider_conn |> get(path) |> assert_api_error(404, "not_found")
        absent_body = context.outsider_conn |> get(absent) |> assert_api_error(404, "not_found")

        assert Map.delete(private_body, "request_id") == Map.delete(absent_body, "request_id"),
               "#{name} discloses the private repository to a non-member"
      end
    end
  end

  describe "a public repository" do
    setup do
      repository =
        repository_fixture(%{
          owner: "metadata-public",
          name: "open-work",
          visibility: "public"
        })

      issue = issue_fixture(repository, %{title: "a public issue"})
      {:ok, comment} = Issues.create_comment(issue, %{body: "a public comment"})
      _label = label_fixture(repository, %{name: "shipped", color: "0075ca"})
      _milestone = milestone_fixture(repository, %{title: "first release", number: 1})

      %{public_issue: issue, public_comment: comment, public_repository: repository}
    end

    test "still answers an anonymous caller exactly as before", %{
      public_issue: issue,
      public_comment: comment
    } do
      base = "/api/v1/repos/metadata-public/open-work"

      for path <- [
            "#{base}/issues/#{issue.number}/comments",
            "#{base}/issues/comments/#{comment.id}",
            "#{base}/issues/#{issue.number}/labels",
            "#{base}/issues/#{issue.number}/assignees",
            "#{base}/labels",
            "#{base}/labels/shipped",
            "#{base}/milestones",
            "#{base}/milestones/1",
            "#{base}/assignees"
          ] do
        assert build_conn() |> get(path) |> Map.get(:status) == 200,
               "#{path} stopped answering an anonymous caller"
      end

      assert %{"comments" => [read]} =
               build_conn()
               |> get("#{base}/issues/#{issue.number}/comments")
               |> json_response(200)

      assert read["body"] == "a public comment"
    end

    test "a bearer token widens nothing that was already public", %{
      conn: conn,
      public_issue: issue
    } do
      base = "/api/v1/repos/metadata-public/open-work"
      path = "#{base}/issues/#{issue.number}/comments"

      anonymous = build_conn() |> get(path) |> json_response(200)

      authenticated =
        conn
        |> put_forge_api_token("private-metadata-stranger")
        |> get(path)
        |> json_response(200)

      assert anonymous == authenticated
    end
  end

  describe "the repository really is private" do
    test "so a refusal means the visibility predicate ran", %{repository: repository} do
      assert repository.visibility == "private"
      assert Repositories.get_by_path!("metadata-owner", "private-work").id == repository.id
    end
  end
end