test/openagents/repository_lifecycle_test.exs

58e6347eeb72 · 7 KB

defmodule OpenAgents.RepositoryLifecycleTest do
  use OpenAgents.DataCase

  import OpenAgents.AccountsFixtures

  alias OpenAgents.Repo
  alias OpenAgents.Repositories
  alias OpenAgents.Repositories.{Namespace, NamespaceAlias, ProvisioningOutbox, RepositoryImport}

  test "projects a GitHub user namespace by numeric account ID and retains a renamed slug alias" do
    user = repository_user_fixture("before-rename")

    assert {:ok, namespace} = Repositories.ensure_user_namespace(user)
    assert namespace.provider == "github"
    assert namespace.provider_account_id == user.github_id
    assert namespace.slug == "before-rename"
    assert namespace.kind == "user"
    assert namespace.owner_user_id == user.id

    assert {:ok, renamed} =
             Repositories.upsert_github_namespace(%{
               provider_account_id: user.github_id,
               slug: "after-rename",
               kind: "user",
               owner_user_id: user.id
             })

    assert renamed.id == namespace.id
    assert renamed.slug == "after-rename"
    assert Repositories.get_namespace_by_slug!("after-rename").id == namespace.id
    assert Repositories.get_namespace_by_slug!("before-rename").id == namespace.id

    assert Repo.get_by!(NamespaceAlias,
             namespace_id: namespace.id,
             slug_key: "before-rename"
           )
  end

  test "creates a ready-to-provision repository, owner membership, and outbox atomically" do
    user = repository_user_fixture("repo-owner")

    assert {:ok, repository, :created} =
             Repositories.create_user_repository(
               user,
               %{
                 name: "new-project",
                 description: "Created through the public repository contract",
                 visibility: "private",
                 default_branch: "main"
               },
               "create-key-1"
             )

    assert repository.owner == user.github_login
    assert repository.namespace.provider_account_id == user.github_id
    assert repository.lifecycle_state == "provisioning"
    assert repository.provisioning_kind == "empty"
    assert is_binary(repository.storage_key)
    assert Repositories.writable?(repository, user)

    repository_id = repository.id

    assert %ProvisioningOutbox{
             repository_id: ^repository_id,
             operation: "create",
             state: "pending"
           } = Repo.get_by!(ProvisioningOutbox, repository_id: repository.id)
  end

  test "replays the same idempotent create and rejects a different request" do
    user = repository_user_fixture("idempotent-owner")
    attrs = %{name: "same-project", visibility: "private", default_branch: "main"}

    assert {:ok, first, :created} =
             Repositories.create_user_repository(user, attrs, "create-key-2")

    assert {:ok, replayed, :replayed} =
             Repositories.create_user_repository(user, attrs, "create-key-2")

    assert replayed.id == first.id

    assert {:error, :idempotency_conflict} =
             Repositories.create_user_repository(
               user,
               %{attrs | name: "different-project"},
               "create-key-2"
             )
  end

  test "records a one-time GitHub import without a credential" do
    user = repository_user_fixture("import-owner")

    source = %{
      provider: "github",
      source_repository_id: 4_242,
      source_owner_id: user.github_id,
      source_full_name: "import-owner/source-project",
      source_default_branch: "main",
      source_ref_digest: String.duplicate("a", 64),
      source_head_sha: String.duplicate("b", 40),
      source_refs: %{
        "refs/heads/main" => String.duplicate("b", 40),
        "refs/tags/v1" => String.duplicate("c", 40)
      }
    }

    assert {:ok, repository, repository_import, :created} =
             Repositories.create_user_import(
               user,
               source,
               %{name: "source-project", visibility: "private"},
               "import-key-1"
             )

    assert repository.provisioning_kind == "github_import"
    assert repository_import.repository_id == repository.id
    assert repository_import.source_repository_id == 4_242
    assert repository_import.source_owner_id == user.github_id
    assert repository_import.source_ref_digest == String.duplicate("a", 64)
    assert repository_import.source_refs == source.source_refs
    assert repository_import.state == "pending"

    fields = RepositoryImport.__schema__(:fields)
    refute :token in fields
    refute :clone_url in fields
    refute :credential in fields

    assert %ProvisioningOutbox{operation: "github_import"} =
             Repo.get_by!(ProvisioningOutbox, repository_id: repository.id)
  end

  test "lists only repositories visible to the active user" do
    owner = repository_user_fixture("list-owner")
    viewer = repository_user_fixture("list-viewer")
    outsider = repository_user_fixture("list-outsider")

    assert {:ok, public_repository, :created} =
             Repositories.create_user_repository(
               owner,
               %{name: "public-project", visibility: "public"},
               "public-key"
             )

    assert {:ok, private_repository, :created} =
             Repositories.create_user_repository(
               owner,
               %{name: "private-project", visibility: "private"},
               "private-key"
             )

    assert {:ok, _membership} = Repositories.add_member(private_repository, viewer, "viewer")

    viewer_repository_ids = Enum.map(Repositories.list_visible_repositories(viewer), & &1.id)
    outsider_repository_ids = Enum.map(Repositories.list_visible_repositories(outsider), & &1.id)

    # A repository is readable through membership at any point in its
    # lifecycle, and readable to everyone else only once it is both public and
    # provisioned. Both of these are still `provisioning`, so the viewer sees
    # only the private one it belongs to, and the stranger sees neither.
    #
    # `list_visible_repositories/1` used to hold its own copy of the predicate,
    # and that copy had lost the lifecycle half, so it disagreed with
    # `list_visible_repositories_page/4` and with `get_visible_by_path!/3`
    # about the same repository. All of them compose `readable_by/2` now.
    refute public_repository.id in viewer_repository_ids
    assert private_repository.id in viewer_repository_ids
    refute public_repository.id in outsider_repository_ids
    refute private_repository.id in outsider_repository_ids

    public_repository
    |> Ecto.Changeset.change(lifecycle_state: "ready", ready_at: DateTime.utc_now())
    |> Repo.update!()

    ready_outsider_ids = Enum.map(Repositories.list_visible_repositories(outsider), & &1.id)

    assert public_repository.id in ready_outsider_ids
    refute private_repository.id in ready_outsider_ids
  end

  test "an organization namespace retains GitHub identity separately from its slug" do
    assert {:ok, namespace} =
             Repositories.upsert_github_namespace(%{
               provider_account_id: 115_798_681,
               provider_node_id: "O_kgDOBubymQ",
               slug: "OpenAgentsInc",
               kind: "organization"
             })

    assert %Namespace{
             provider: "github",
             provider_account_id: 115_798_681,
             provider_node_id: "O_kgDOBubymQ",
             slug_key: "openagentsinc",
             kind: "organization",
             owner_user_id: nil
           } = namespace
  end

  test "reserved product paths cannot become repository namespaces" do
    assert {:error, changeset} =
             Repositories.upsert_github_namespace(%{
               provider_account_id: 9_999_991,
               slug: "repositories",
               kind: "organization"
             })

    assert {"is reserved", _metadata} = changeset.errors[:slug_key]
  end
end