5.3 KiB · text History b868907
defmodule GitGud.Workflows.PullRequestActivityTest do
  @moduledoc """
  `pull_request` `closed`, `edited`, `labeled` and `unlabeled`: dispatched
  from where they happen, and run only by a workflow whose `types:` asks.
  """
  use GitGud.DataCase, async: false

  alias GitGud.PullRequests
  alias GitGud.Repositories.Repository
  alias GitGud.Workflows.WorkflowRun
  alias GitGud.Workflows.WorkflowSchedule

  import GitGud.ForgeFixtures
  import GitGud.WorkflowGitHelpers

  @moduletag :capture_log

  @wf ".forgejo/workflows/ci.yml"

  defp workflow(types) do
    """
    name: ci
    on:
      pull_request:
        types: [#{Enum.join(types, ", ")}]
      schedule:
        - cron: "0 3 * * *"
    jobs:
      build:
        runs-on: self-hosted
        steps:
          - run: echo hello
    """
  end

  @default_types """
  name: ci
  on: pull_request
  jobs:
    build:
      runs-on: self-hosted
      steps:
        - run: echo hello
  """

  setup do
    {user, repo} = repository_fixture()
    {:ok, repo: repo, user: user}
  end

  defp open_pr!(repo, user, contents) do
    main = commit!(repo, %{"README" => "hi\n"}, [])
    git!(repo.disk_path, ["update-ref", "refs/heads/main", main])
    feature = commit!(repo, %{"README" => "hi\n", @wf => contents}, [main])
    git!(repo.disk_path, ["update-ref", "refs/heads/feature", feature])

    {:ok, pr} = PullRequests.create_pull_request(repo, user, pr_attrs("feature", "main"))
    drain!()
    pr
  end

  defp drain!, do: Oban.drain_queue(queue: :ci)

  defp actions(%Repository{id: rid}) do
    from(r in WorkflowRun,
      where: r.repository_id == ^rid and r.trigger == "pull_request",
      order_by: r.id
    )
    |> Repo.all()
    |> Enum.map(& &1.event_payload["action"])
  end

  defp last_payload(%Repository{id: rid}) do
    from(r in WorkflowRun,
      where: r.repository_id == ^rid and r.trigger == "pull_request",
      order_by: [desc: r.id],
      limit: 1
    )
    |> Repo.one!()
    |> Map.fetch!(:event_payload)
  end

  test "types: [closed] fires on close, with merged false", %{repo: repo, user: user} do
    pr = open_pr!(repo, user, workflow(["closed"]))
    assert actions(repo) == []

    {:ok, _closed} = PullRequests.set_state(pr, "closed", actor_id: user.id)
    drain!()

    assert actions(repo) == ["closed"]
    payload = last_payload(repo)
    assert payload["pull_request"]["merged"] == false
    assert payload["pull_request"]["state"] == "closed"
    assert payload["sender"]["login"] == user.handle
  end

  test "types: [closed] fires on merge, with merged true", %{repo: repo, user: user} do
    pr = open_pr!(repo, user, workflow(["closed"]))

    {:ok, merged} = PullRequests.merge!(pr, user)
    # The schedule comes with the merge's push, played by the push worker.
    Oban.drain_queue(queue: :git_cache)
    drain!()

    assert actions(repo) == ["closed"]
    payload = last_payload(repo)
    assert payload["pull_request"]["merged"] == true
    assert payload["pull_request"]["state"] == "closed"

    assert payload["pull_request"]["merge_commit_sha"] ==
             GitGud.Git.to_hex(merged.merge_commit_sha)

    # The merge brought the workflow onto the default branch, and with
    # it the schedule.
    assert [%WorkflowSchedule{cron: "0 3 * * *"}] =
             Repo.all(from s in WorkflowSchedule, where: s.repository_id == ^repo.id)
  end

  test "the default types don't fire on close", %{repo: repo, user: user} do
    pr = open_pr!(repo, user, @default_types)
    assert actions(repo) == ["opened"]

    {:ok, _closed} = PullRequests.set_state(pr, "closed")
    drain!()
    assert actions(repo) == ["opened"]
  end

  test "labeled carries the label; unlabeled too; re-adding a label isn't an event", %{
    repo: repo,
    user: user
  } do
    pr = open_pr!(repo, user, workflow(["labeled", "unlabeled"]))
    label = label_fixture(repo, %{"name" => "deploy-preview"})

    :ok = PullRequests.attach_label(pr, label)
    :ok = PullRequests.attach_label(pr, label)
    drain!()
    assert actions(repo) == ["labeled"]
    assert last_payload(repo)["label"]["name"] == "deploy-preview"

    :ok = PullRequests.detach_label(pr, label)
    :ok = PullRequests.detach_label(pr, label)
    drain!()
    assert actions(repo) == ["labeled", "unlabeled"]
    assert last_payload(repo)["label"]["name"] == "deploy-preview"
  end

  test "editing the title or body fires edited; other fields don't", %{
    repo: repo,
    user: user
  } do
    pr = open_pr!(repo, user, workflow(["edited"]))

    {:ok, pr} = PullRequests.update_pull_request(pr, %{"title" => "A new title"})
    drain!()
    assert actions(repo) == ["edited"]
    assert last_payload(repo)["pull_request"]["title"] == "A new title"

    {:ok, pr} = PullRequests.update_pull_request(pr, %{"title" => "A new title"})
    drain!()
    assert actions(repo) == ["edited"]

    {:ok, _pr} = PullRequests.update_pull_request(pr, %{"body" => "More words"})
    drain!()
    assert actions(repo) == ["edited", "edited"]
  end

  test "a closed pull request still runs nothing but closed", %{repo: repo, user: user} do
    pr = open_pr!(repo, user, workflow(["labeled", "closed"]))
    {:ok, closed} = PullRequests.set_state(pr, "closed")
    drain!()
    assert actions(repo) == ["closed"]

    :ok = PullRequests.attach_label(closed, label_fixture(repo))
    drain!()
    assert actions(repo) == ["closed"]
  end
end