test/openagents/repositories_test.exs

58e6347eeb72 · 7 KB

defmodule OpenAgents.RepositoriesTest do
  use OpenAgents.DataCase

  import OpenAgents.AccountsFixtures

  alias OpenAgents.Issues
  alias OpenAgents.Issues.Comment
  alias OpenAgents.Labels
  alias OpenAgents.Milestones
  alias OpenAgents.ProjectItems.ProjectItem
  alias OpenAgents.Projects
  alias OpenAgents.Repositories

  setup do
    initial = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")

    {:ok, second} =
      Repositories.create_repository(%{
        owner: "SecondOrg",
        name: "second-repo",
        visibility: "private",
        default_branch: "trunk"
      })

    user = repository_user_fixture("tenant-member")
    {:ok, _membership} = Repositories.add_member(second, user, "maintainer")

    %{initial: initial, second: second, user: user}
  end

  test "the explicit initial repository has a stable canonical identity", %{initial: initial} do
    assert initial.id == "00000000-0000-4000-8000-000000000001"
    assert initial.owner == "OpenAgentsInc"
    assert initial.name == "openagents.com"
    assert initial.visibility == "public"
    assert initial.default_branch == "main"

    assert Repositories.get_by_path!("openagentsinc", "OPENAGENTS.COM").id == initial.id
  end

  test "issue and milestone numbers are allocated independently per repository", %{
    initial: initial,
    second: second
  } do
    assert {:ok, initial_issue} = Issues.create_issue(initial, %{title: "Initial"})
    assert {:ok, second_issue} = Issues.create_issue(second, %{title: "Second"})
    assert initial_issue.number == 1
    assert second_issue.number == 1

    assert {:ok, initial_milestone} = Milestones.create_milestone(initial, %{title: "Initial"})
    assert {:ok, second_milestone} = Milestones.create_milestone(second, %{title: "Second"})
    assert initial_milestone.number == 1
    assert second_milestone.number == 1
  end

  test "public paths cannot cross repository ownership or expose a private repository", %{
    initial: initial,
    second: second
  } do
    assert {:ok, initial_issue} = Issues.create_issue(initial, %{title: "Initial"})
    assert {:ok, second_issue} = Issues.create_issue(second, %{title: "Second"})

    assert Issues.get_issue_by_path!("OpenAgentsInc", "openagents.com", 1).id ==
             initial_issue.id

    assert Issues.get_issue_by_number!(second, 1).id == second_issue.id

    assert_raise Ecto.NoResultsError, fn ->
      Issues.get_issue_by_path!("SecondOrg", "second-repo", 1)
    end
  end

  test "labels and milestones from another repository are rejected", %{
    initial: initial,
    second: second
  } do
    assert {:ok, issue} = Issues.create_issue(initial, %{title: "Scoped"})
    assert {:ok, second_label} = Labels.create_label(second, %{name: "private", color: "ffffff"})
    assert {:ok, milestone} = Milestones.create_milestone(second, %{title: "Second"})

    # Adding a name that exists only in the other repository creates a fresh,
    # locally-scoped label; it never links across the repository boundary.
    assert {:ok, labelled} = Issues.add_labels(issue, ["private"])
    assert [%{"name" => "private"}] = labelled.labels

    local_label = Labels.get_label_by_name!(initial, "private")
    refute local_label.id == second_label.id

    # The milestone lookup stays strict: a number from another repository
    # cannot be attached at all.
    assert_raise Ecto.NoResultsError, fn ->
      Issues.set_milestone(issue, milestone.number)
    end

    assert Enum.map(Labels.list_labels(second), & &1.name) == ["private"]
  end

  test "only active repository members are assignable", %{initial: initial, second: second} do
    initial_only = repository_user_fixture("initial-only")
    {:ok, _membership} = Repositories.add_member(initial, initial_only, "contributor")

    assert Enum.map(Repositories.list_assignable_users(initial), & &1.github_login) == [
             "initial-only"
           ]

    assert Enum.map(Repositories.list_assignable_users(second), & &1.github_login) == [
             "tenant-member"
           ]

    assert_raise Ecto.NoResultsError, fn ->
      Repositories.get_assignable_user_by_login!(second, "initial-only")
    end
  end

  test "a banned membership is neither writable nor assignable", %{second: second, user: user} do
    assert {:ok, banned} = OpenAgents.Accounts.ban_user(user, "repository_policy")
    refute Repositories.writable?(second, banned)
    assert Repositories.list_assignable_users(second) == []

    assert_raise Ecto.NoResultsError, fn ->
      Repositories.get_writable_by_path!(second.owner, second.name, banned)
    end
  end

  test "a project item may name a source issue in another repository", %{
    initial: initial,
    second: second,
    user: user
  } do
    {:ok, _membership} = Repositories.add_member(initial, user, "maintainer")
    assert {:ok, project} = Projects.create_project(initial, %{title: "Initial"}, user)
    assert {:ok, second_issue} = Issues.create_issue(second, %{title: "Second"})

    assert {:ok, item} =
             %ProjectItem{}
             |> ProjectItem.changeset(%{
               project_id: project.id,
               issue_id: second_issue.id,
               repository_id: initial.id,
               issue_repository_id: second.id
             })
             |> Repo.insert()

    assert item.repository_id == initial.id
    assert item.issue_repository_id == second.id
  end

  test "project-item and comment constraints reject mismatched repository identities", %{
    initial: initial,
    second: second,
    user: user
  } do
    {:ok, _membership} = Repositories.add_member(initial, user, "maintainer")
    assert {:ok, project} = Projects.create_project(initial, %{title: "Initial"}, user)
    assert {:ok, second_issue} = Issues.create_issue(second, %{title: "Second"})

    assert {:error, item_changeset} =
             %ProjectItem{}
             |> ProjectItem.changeset(%{
               project_id: project.id,
               issue_id: second_issue.id,
               repository_id: initial.id,
               issue_repository_id: initial.id
             })
             |> Repo.insert()

    assert %{issue_id: ["does not exist"]} = errors_on(item_changeset)

    now = DateTime.utc_now() |> DateTime.truncate(:second)

    assert {:error, comment_changeset} =
             %Comment{}
             |> Comment.changeset(%{
               issue_id: second_issue.id,
               repository_id: initial.id,
               body: "cross tenant",
               created_at: now,
               updated_at: now
             })
             |> Repo.insert()

    assert %{issue_id: ["does not exist"]} = errors_on(comment_changeset)
  end

  test "project paths require both the repository and project number", %{
    initial: initial,
    second: second,
    user: user
  } do
    {:ok, _membership} = Repositories.add_member(initial, user, "maintainer")
    assert {:ok, initial_project} = Projects.create_project(initial, %{title: "Initial"}, user)
    assert {:ok, second_project} = Projects.create_project(second, %{title: "Second"}, user)

    assert initial_project.number == second_project.number

    assert Projects.get_project_by_path!("OpenAgentsInc", "openagents.com", 1).id ==
             initial_project.id

    assert Projects.get_project_by_number!(second, 1).id == second_project.id

    assert_raise Ecto.NoResultsError, fn ->
      Projects.get_project_by_path!("SecondOrg", "second-repo", 1)
    end
  end
end