test/openagents/issues/closing_references_test.exs

main at 58e6347eeb72 · 8 KB

defmodule OpenAgents.Issues.ClosingReferencesTest do
  @moduledoc """
  #130: what a commit's closing reference is allowed to do to an issue.

  The push path calls `apply_commit/5` for commits newly reachable from the
  default branch. These are the rules it obeys once it gets there: same
  repository, write authority, once, never reopen.
  """

  use OpenAgents.DataCase

  alias OpenAgents.Issues
  alias OpenAgents.Issues.ClosingReferences
  alias OpenAgents.Repo

  @sha "9606cbc0e0f1a2b3c4d5e6f708192a3b4c5d6e7f"

  setup do
    author = repository_user_fixture("pusher")
    repository = repository_with_member_fixture(author, %{}, "maintainer")
    {:ok, issue} = Issues.create_issue(repository, %{title: "Ship the closer"})

    %{author: author, repository: repository, issue: issue}
  end

  defp reload(issue), do: Repo.get!(OpenAgents.Issues.Issue, issue.id)

  describe "closing" do
    test "a Closes reference closes the issue and records the commit", context do
      %{author: author, repository: repository, issue: issue} = context
      message = "Ship it\n\nCloses ##{issue.number}\n"

      assert [reference] = ClosingReferences.apply_commit(repository, author, @sha, message)

      assert reference.commit_sha == @sha
      assert reference.closed
      assert reference.verb == "closes"
      assert reference.closed_by_user_id == author.id
      assert reload(issue).state == "closed"
      assert reload(issue).state_reason == "completed"
    end

    test "the close is attributed to the pushing principal, not a system actor", context do
      %{author: author, repository: repository, issue: issue} = context

      assert [reference] =
               ClosingReferences.apply_commit(
                 repository,
                 author,
                 @sha,
                 "Closes ##{issue.number}",
                 repo: "demo",
                 wal_seq: 3,
                 principal: "user:#{author.id}"
               )

      assert reference.principal == "user:#{author.id}"
      assert reference.closed_by_user_id == author.id
      assert reference.repo == "demo"
      assert reference.wal_seq == 3
    end

    test "Fixes and Resolves close the same way", context do
      %{author: author, repository: repository} = context
      {:ok, fixed} = Issues.create_issue(repository, %{title: "Fixed"})
      {:ok, resolved} = Issues.create_issue(repository, %{title: "Resolved"})

      ClosingReferences.apply_commit(repository, author, @sha, "Fixes ##{fixed.number}")

      ClosingReferences.apply_commit(
        repository,
        author,
        String.replace(@sha, "9", "a"),
        "Resolves ##{resolved.number}"
      )

      assert reload(fixed).state == "closed"
      assert reload(resolved).state == "closed"
    end

    test "a list closes every issue it names", context do
      %{author: author, repository: repository, issue: first} = context
      {:ok, second} = Issues.create_issue(repository, %{title: "Second"})

      message = "Ship both\n\nCloses ##{first.number}, ##{second.number}"

      assert [_one, _two] = ClosingReferences.apply_commit(repository, author, @sha, message)
      assert reload(first).state == "closed"
      assert reload(second).state == "closed"
    end
  end

  describe "what it refuses" do
    test "a pusher without write authority records nothing", context do
      %{repository: repository, issue: issue} = context
      outsider = repository_user_fixture("outsider")

      assert ClosingReferences.apply_commit(
               repository,
               outsider,
               @sha,
               "Closes ##{issue.number}"
             ) == []

      assert reload(issue).state == "open"
      assert ClosingReferences.for_issue(issue) == []
    end

    test "a read-only member records nothing", context do
      %{repository: repository, issue: issue} = context
      viewer = repository_user_fixture("viewer")
      {:ok, _membership} = OpenAgents.Repositories.add_member(repository, viewer, "viewer")

      assert ClosingReferences.apply_commit(repository, viewer, @sha, "Closes ##{issue.number}") ==
               []

      assert reload(issue).state == "open"
    end

    test "a nonexistent issue number records nothing", context do
      %{author: author, repository: repository} = context

      assert ClosingReferences.apply_commit(repository, author, @sha, "Closes #99999") == []
    end

    test "an issue in another repository is not closed", context do
      %{author: author, repository: repository} = context
      elsewhere = repository_with_member_fixture(author, %{}, "maintainer")
      {:ok, foreign} = Issues.create_issue(elsewhere, %{title: "Not yours"})

      message = "Closes #{elsewhere.owner}/#{elsewhere.name}##{foreign.number}"

      assert ClosingReferences.apply_commit(repository, author, @sha, message) == []
      assert reload(foreign).state == "open"
    end

    test "a nil actor records nothing", context do
      %{repository: repository, issue: issue} = context

      assert ClosingReferences.apply_commit(repository, nil, @sha, "Closes ##{issue.number}") ==
               []

      assert reload(issue).state == "open"
    end

    test "a malformed reference records nothing and raises nothing", context do
      %{author: author, repository: repository, issue: issue} = context

      for message <- ["Closes #", "Closes #abc", "Closes", "", "Closes #0", nil, 42] do
        assert ClosingReferences.apply_commit(repository, author, @sha, message) == []
      end

      assert reload(issue).state == "open"
    end
  end

  describe "idempotency" do
    test "applying the same commit twice closes once and records one row", context do
      %{author: author, repository: repository, issue: issue} = context
      message = "Closes ##{issue.number}"

      assert [_recorded] = ClosingReferences.apply_commit(repository, author, @sha, message)
      assert ClosingReferences.apply_commit(repository, author, @sha, message) == []

      assert length(ClosingReferences.for_issue(issue)) == 1
    end

    test "an already-closed issue records the reference without reclosing", context do
      %{author: author, repository: repository, issue: issue} = context

      {:ok, closed} =
        Issues.update_issue(
          issue,
          %{"state" => "closed", "state_reason" => "not_planned"},
          author
        )

      assert [reference] =
               ClosingReferences.apply_commit(
                 repository,
                 author,
                 @sha,
                 "Closes ##{issue.number}"
               )

      refute reference.closed
      reloaded = reload(closed)
      assert reloaded.state == "closed"
      assert reloaded.state_reason == "not_planned"
    end

    test "a second commit referencing the same issue records its own row", context do
      %{author: author, repository: repository, issue: issue} = context
      other = String.replace(@sha, "9", "b")

      ClosingReferences.apply_commit(repository, author, @sha, "Closes ##{issue.number}")
      ClosingReferences.apply_commit(repository, author, other, "Closes ##{issue.number}")

      references = ClosingReferences.for_issue(issue)
      assert length(references) == 2
      assert Enum.count(references, & &1.closed) == 1
    end
  end

  describe "the derived blocked flag from #100" do
    test "closing through a commit unblocks a dependent", context do
      %{author: author, repository: repository, issue: prerequisite} = context
      {:ok, dependent} = Issues.create_issue(repository, %{title: "Waits on the closer"})

      assert :ok = Issues.add_dependencies(dependent, [prerequisite.number])
      assert %{blocked: true} = Issues.dependencies(dependent)

      ClosingReferences.apply_commit(
        repository,
        author,
        @sha,
        "Closes ##{prerequisite.number}"
      )

      assert %{blocked: false} = Issues.dependencies(dependent)

      assert Issues.list_issues(repository, blocked: true) == []
    end
  end

  describe "reading back" do
    test "for_commit/2 returns the references one commit made", context do
      %{author: author, repository: repository, issue: issue} = context

      ClosingReferences.apply_commit(repository, author, @sha, "Closes ##{issue.number}")

      assert [reference] = ClosingReferences.for_commit(repository, @sha)
      assert reference.issue.number == issue.number
      assert ClosingReferences.for_commit(repository, "0000000") == []
    end
  end
end