test/openagents_web/controllers/repository_import_controller_test.exs

58e6347eeb72 · 11 KB

defmodule OpenAgentsWeb.RepositoryImportControllerTest do
  use OpenAgentsWeb.ConnCase, async: false

  alias OpenAgents.{Accounts, ApiTokens, GitHubOAuth, Repo}

  setup {Req.Test, :verify_on_exit!}

  setup do
    original = Application.get_env(:openagents, :github_api)

    Application.put_env(:openagents, :github_api,
      base_url: "https://github-api.internal",
      request_options: [plug: {Req.Test, __MODULE__}]
    )

    on_exit(fn -> Application.put_env(:openagents, :github_api, original) end)
    :ok
  end

  test "POST /api/v1/user/repos/imports accepts one frozen GitHub snapshot", %{conn: conn} do
    user = github_user("repository-import-api", "octavia")
    assert {:ok, user} = store_repository_grant(user, "gho_import_fixture")
    main_sha = String.duplicate("a", 40)
    tag_sha = String.duplicate("b", 40)

    expect_import_source(user, main_sha, tag_sha)

    response =
      conn
      |> authorize(user)
      |> put_req_header("idempotency-key", "import-key-1")
      |> post(~p"/api/v1/user/repos/imports", %{
        source: %{provider: "github", repository: "octavia/source-project"},
        name: "copied-project",
        private: true
      })

    assert %{
             "name" => "copied-project",
             "lifecycle_state" => "provisioning",
             "import" => %{
               "id" => import_id,
               "provider" => "github",
               "source_full_name" => "octavia/source-project",
               "source_head_sha" => ^main_sha,
               "state" => "pending",
               "lfs_warning" => true
             }
           } = json_response(response, 202)

    repository_import = Repo.get!(OpenAgents.Repositories.RepositoryImport, import_id)

    assert repository_import.source_refs == %{
             "refs/heads/main" => main_sha,
             "refs/tags/v1.0.0" => tag_sha
           }

    refute Map.has_key?(repository_import.source_refs, "credential")

    status =
      conn
      |> authorize(user)
      |> get(~p"/api/v1/repository-imports/#{import_id}")

    assert json_response(status, 200)["import"]["id"] == import_id

    other_user = github_user("repository-import-other")

    assert conn
           |> authorize(other_user)
           |> get(~p"/api/v1/repository-imports/#{import_id}")
           |> json_response(404)
  end

  test "an import inherits the GitHub repository visibility when omitted", %{conn: conn} do
    user = github_user("repository-public-import-api", "octavia")
    assert {:ok, user} = store_repository_grant(user, "gho_public_import_fixture")
    main_sha = String.duplicate("c", 40)

    expect_import_source(user, main_sha, nil, false)

    response =
      conn
      |> authorize(user)
      |> put_req_header("idempotency-key", "public-import-key")
      |> post(~p"/api/v1/user/repos/imports", %{
        source: %{provider: "github", repository: "octavia/source-project"}
      })

    assert %{"private" => false, "visibility" => "public"} = json_response(response, 202)
  end

  test "organization creation requires an active GitHub administrator membership", %{conn: conn} do
    user = github_user("repository-org-api")
    assert {:ok, user} = store_repository_grant(user, "gho_org_fixture")

    Req.Test.expect(__MODULE__, fn github_conn ->
      assert github_conn.request_path == "/user/memberships/orgs"

      Req.Test.json(github_conn, [
        %{
          "state" => "active",
          "role" => "admin",
          "organization" => %{
            "id" => 42,
            "node_id" => "O_42",
            "login" => "acme",
            "avatar_url" => "https://avatars.githubusercontent.com/u/42?v=4"
          }
        }
      ])
    end)

    created =
      conn
      |> authorize(user)
      |> put_req_header("idempotency-key", "org-create-key")
      |> post(~p"/api/v1/orgs/acme/repos", %{name: "org-project", private: false})

    assert %{
             "full_name" => "acme/org-project",
             "private" => false,
             "owner" => %{"login" => "acme", "type" => "Organization"}
           } = json_response(created, 202)
  end

  describe "an upstream mirror of a repository this account does not own" do
    test "a foreign public source is refused by name, and the refusal names the source", %{
      conn: conn
    } do
      user = github_user("repository-foreign-import", "octavia")
      assert {:ok, user} = store_repository_grant(user, "gho_foreign_fixture")

      expect_foreign_source(String.duplicate("d", 40), false, "MIT")

      response =
        conn
        |> authorize(user)
        |> put_req_header("idempotency-key", "foreign-import-key")
        |> post(~p"/api/v1/user/repos/imports", %{
          source: %{provider: "github", repository: "tobi/walgit"}
        })

      body = json_response(response, 403)

      assert body["code"] == "source_namespace_mismatch"
      # The source owner is what failed, so the message says so. The old
      # message named the destination namespace, which was this account's own.
      assert body["message"] =~ "tobi/walgit"
      assert body["message"] =~ "owned by another GitHub account"
      assert body["message"] =~ ~s("mirror": true)
      assert body["failed"] == %{"source" => "tobi/walgit", "destination" => "eligible"}
    end

    test "the same source is brought in as a mirror, and the response names the upstream", %{
      conn: conn
    } do
      user = github_user("repository-mirror-api", "octavia")
      assert {:ok, user} = store_repository_grant(user, "gho_mirror_fixture")
      main_sha = String.duplicate("e", 40)

      expect_foreign_source(main_sha, false, "MIT")

      response =
        conn
        |> authorize(user)
        |> put_req_header("idempotency-key", "mirror-key")
        |> post(~p"/api/v1/user/repos/imports", %{
          source: %{provider: "github", repository: "tobi/walgit"},
          mirror: true
        })

      assert %{
               "name" => "walgit",
               "mirror" => true,
               "visibility" => "public",
               "upstream" => %{
                 "url" => "https://github.com/tobi/walgit",
                 "license" => "MIT",
                 "direction" => "one_way",
                 "accepts_pushes" => false
               },
               "permissions" => permissions
             } = json_response(response, 202)

      # The Git plane refuses every push to a mirror, so no projection of it
      # may report a push permission its owner does not have.
      assert permissions["push"] == false
      assert permissions["admin"] == true
    end

    test "an upstream with no license records the absence rather than omitting it", %{conn: conn} do
      user = github_user("repository-unlicensed-mirror", "octavia")
      assert {:ok, user} = store_repository_grant(user, "gho_unlicensed_fixture")

      expect_foreign_source(String.duplicate("f", 40), false, nil)

      response =
        conn
        |> authorize(user)
        |> put_req_header("idempotency-key", "unlicensed-mirror-key")
        |> post(~p"/api/v1/user/repos/imports", %{
          source: %{provider: "github", repository: "tobi/walgit"},
          mirror: true
        })

      assert %{"upstream" => %{"license" => "none"}} = json_response(response, 202)
    end

    test "a private source cannot be mirrored", %{conn: conn} do
      user = github_user("repository-private-mirror", "octavia")
      assert {:ok, user} = store_repository_grant(user, "gho_private_mirror_fixture")

      expect_foreign_source(String.duplicate("1", 40), true, "MIT")

      response =
        conn
        |> authorize(user)
        |> put_req_header("idempotency-key", "private-mirror-key")
        |> post(~p"/api/v1/user/repos/imports", %{
          source: %{provider: "github", repository: "tobi/walgit"},
          mirror: true
        })

      assert %{"code" => "source_repository_not_public"} = json_response(response, 403)
    end

    test "an owned repository publishes the distinction too", %{conn: conn} do
      user = github_user("repository-owned-projection")

      response =
        conn
        |> authorize(user)
        |> put_req_header("idempotency-key", "owned-projection-key")
        |> post(~p"/api/v1/user/repos", %{name: "mine", private: false})

      assert %{"mirror" => false, "upstream" => nil} = json_response(response, 202)
    end
  end

  defp store_repository_grant(user, token) do
    Accounts.store_github_token(user, token, GitHubOAuth.required_scopes())
  end

  defp expect_foreign_source(main_sha, private?, license) do
    Req.Test.expect(__MODULE__, fn github_conn ->
      assert github_conn.request_path == "/repos/tobi/walgit"

      Req.Test.json(github_conn, %{
        "id" => 909,
        "node_id" => "R_909",
        "name" => "walgit",
        "full_name" => "tobi/walgit",
        "private" => private?,
        "default_branch" => "main",
        "license" => if(license, do: %{"spdx_id" => license, "key" => "mit"}),
        "owner" => %{
          "id" => 777_777,
          "node_id" => "U_777777",
          "login" => "tobi",
          "avatar_url" => "https://avatars.githubusercontent.com/u/777777?v=4",
          "type" => "User"
        },
        "permissions" => %{"pull" => true, "push" => false, "admin" => false}
      })
    end)

    Req.Test.expect(__MODULE__, fn github_conn ->
      assert github_conn.request_path == "/repos/tobi/walgit/git/matching-refs/heads/"

      Req.Test.json(github_conn, [
        %{"ref" => "refs/heads/main", "object" => %{"type" => "commit", "sha" => main_sha}}
      ])
    end)

    Req.Test.expect(__MODULE__, fn github_conn ->
      assert github_conn.request_path == "/repos/tobi/walgit/git/matching-refs/tags/"
      Req.Test.json(github_conn, [])
    end)

    Req.Test.expect(__MODULE__, fn github_conn ->
      assert github_conn.request_path == "/repos/tobi/walgit/git/trees/main"

      Req.Test.json(github_conn, %{
        "truncated" => false,
        "tree" => [%{"path" => "README.md", "type" => "blob", "size" => 200}]
      })
    end)
  end

  defp expect_import_source(user, main_sha, tag_sha, private? \\ true) do
    Req.Test.expect(__MODULE__, fn github_conn ->
      assert github_conn.request_path == "/repos/octavia/source-project"

      Req.Test.json(github_conn, %{
        "id" => 501,
        "node_id" => "R_501",
        "name" => "source-project",
        "full_name" => "octavia/source-project",
        "private" => private?,
        "default_branch" => "main",
        "owner" => %{
          "id" => user.github_id,
          "node_id" => "U_#{user.github_id}",
          "login" => "octavia",
          "avatar_url" => "https://avatars.githubusercontent.com/u/#{user.github_id}?v=4",
          "type" => "User"
        },
        "permissions" => %{"pull" => true, "push" => false, "admin" => false}
      })
    end)

    Req.Test.expect(__MODULE__, fn github_conn ->
      assert github_conn.request_path ==
               "/repos/octavia/source-project/git/matching-refs/heads/"

      Req.Test.json(github_conn, [
        %{"ref" => "refs/heads/main", "object" => %{"type" => "commit", "sha" => main_sha}}
      ])
    end)

    Req.Test.expect(__MODULE__, fn github_conn ->
      assert github_conn.request_path ==
               "/repos/octavia/source-project/git/matching-refs/tags/"

      tags =
        if tag_sha,
          do: [%{"ref" => "refs/tags/v1.0.0", "object" => %{"type" => "tag", "sha" => tag_sha}}],
          else: []

      Req.Test.json(github_conn, tags)
    end)

    Req.Test.expect(__MODULE__, fn github_conn ->
      assert github_conn.request_path == "/repos/octavia/source-project/git/trees/main"

      Req.Test.json(github_conn, %{
        "truncated" => false,
        "tree" => [%{"path" => ".gitattributes", "type" => "blob", "size" => 200}]
      })
    end)
  end

  defp authorize(conn, user) do
    {:ok, _credential, plaintext} =
      ApiTokens.create(user, %{name: "repository import API test", scopes: ["forge:write"]})

    put_req_header(conn, "authorization", "Bearer " <> plaintext)
  end
end