7.9 KiB · text History b0551cd
defmodule GitGudWeb.RawRegistryTest do
  @moduledoc """
  The raw registry, over HTTP.

  The client this format exists for is a microcontroller with a few
  hundred lines of C and no package manager, so the assertions are the
  ones such a client depends on and cannot work around: the manifest
  names a `latest`, every file carries a URL that can be fetched
  verbatim and a SHA-256 that matches the bytes on the other end, and
  published bytes never change under a filename that was already served.
  """
  use GitGudWeb.ConnCase, async: false

  alias GitGud.Packages
  alias GitGud.Repositories.Storage

  import GitGud.ForgeFixtures

  @firmware <<0xE9, 0x06, 0x02, 0x1F>> <> :crypto.strong_rand_bytes(1024)

  setup do
    {user, repo} = repository_fixture(%{visibility: "public"})
    {:ok, _t, token} = Packages.create_registry_token(repo, "ci", ["pull", "push"])

    {:ok,
     user: user,
     repo: repo,
     token: token,
     root: "/pkg/raw/#{Storage.owner_handle(user)}/#{repo.name}"}
  end

  defp publish(conn, root, token, version, filename, body) do
    conn = if token, do: put_req_header(conn, "authorization", token), else: conn

    conn
    |> put_req_header("content-type", "application/octet-stream")
    |> put("#{root}/#{URI.encode(version, &URI.char_unreserved?/1)}/#{filename}", body)
  end

  defp manifest(conn, root), do: Jason.decode!(get(conn, root <> "/manifest.json").resp_body)

  describe "publishing" do
    test "stores the bytes and reports the digest", %{conn: conn, root: root, token: token} do
      conn = publish(conn, root, token, "1.0.0", "firmware.bin", @firmware)

      assert conn.status == 200
      body = Jason.decode!(conn.resp_body)
      assert body["version"] == "1.0.0"
      assert body["filename"] == "firmware.bin"
      assert body["size"] == byte_size(@firmware)
      assert body["sha256"] == Base.encode16(:crypto.hash(:sha256, @firmware), case: :lower)
    end

    test "needs a token", %{conn: conn, root: root} do
      assert publish(conn, root, nil, "1.0.0", "firmware.bin", @firmware).status == 401
    end

    test "the same bytes twice is a no-op, not a conflict", %{
      conn: conn,
      root: root,
      token: token
    } do
      publish(conn, root, token, "1.0.0", "firmware.bin", @firmware)

      assert publish(build_conn(), root, token, "1.0.0", "firmware.bin", @firmware).status == 200
    end

    test "different bytes under a published filename are refused", %{
      conn: conn,
      root: root,
      token: token
    } do
      publish(conn, root, token, "1.0.0", "firmware.bin", @firmware)

      conn = publish(build_conn(), root, token, "1.0.0", "firmware.bin", @firmware <> "more")
      assert conn.status == 409
    end

    test "'latest' is reserved in the version slot", %{conn: conn, root: root, token: token} do
      conn = publish(conn, root, token, "latest", "firmware.bin", @firmware)

      assert conn.status == 400
      assert conn.resp_body =~ "reserved"
    end

    test "an empty body is refused", %{conn: conn, root: root, token: token} do
      assert publish(conn, root, token, "1.0.0", "firmware.bin", "").status == 400
    end
  end

  describe "the manifest" do
    test "is empty but well-formed before anything is published", %{conn: conn, root: root} do
      body = manifest(conn, root)

      assert body["latest"] == nil
      assert body["versions"] == %{}
    end

    test "lists every version, with a fetchable url and a checksum", %{
      conn: conn,
      root: root,
      token: token,
      user: user,
      repo: repo
    } do
      publish(conn, root, token, "1.0.0", "firmware.bin", @firmware)

      body = manifest(build_conn(), root)
      assert body["name"] == repo.name
      assert body["latest"] == "1.0.0"

      version = body["versions"]["1.0.0"]
      refute version["yanked"]
      assert version["published_at"] =~ ~r/^\d{4}-/

      assert [file] = version["files"]
      assert file["name"] == "firmware.bin"
      assert file["size"] == byte_size(@firmware)
      assert file["sha256"] == Base.encode16(:crypto.hash(:sha256, @firmware), case: :lower)
      assert file["content_type"] == "application/octet-stream"

      # The URL is the contract: a device follows it verbatim.
      handle = Storage.owner_handle(user)
      assert file["url"] =~ "/pkg/raw/#{handle}/#{repo.name}/1.0.0/firmware.bin"
      assert %URI{path: path} = URI.parse(file["url"])
      assert get(build_conn(), path).resp_body == @firmware
    end

    test "'latest' is the most recently published, not the highest string", %{
      conn: conn,
      root: root,
      token: token
    } do
      publish(conn, root, token, "2.0.0", "firmware.bin", @firmware)
      publish(build_conn(), root, token, "1.0.0", "firmware.bin", @firmware <> "older")

      assert manifest(build_conn(), root)["latest"] == "1.0.0"
    end

    test "skips a yanked version when choosing latest", %{
      conn: conn,
      root: root,
      token: token,
      repo: repo
    } do
      publish(conn, root, token, "1.0.0", "firmware.bin", @firmware)
      publish(build_conn(), root, token, "1.1.0", "firmware.bin", @firmware <> "bad")

      package = Packages.get_package(repo, "raw", repo.name)
      {:ok, _} = package |> Packages.get_version("1.1.0") |> Packages.set_yanked(true, "bricks")

      body = manifest(build_conn(), root)
      assert body["latest"] == "1.0.0"
      assert body["versions"]["1.1.0"]["yanked"]
    end

    test "is served at the registry root as well", %{conn: conn, root: root, token: token} do
      publish(conn, root, token, "1.0.0", "firmware.bin", @firmware)

      assert Jason.decode!(get(build_conn(), root).resp_body)["latest"] == "1.0.0"
    end

    test "is not cached, or a fleet never sees an update", %{conn: conn, root: root} do
      conn = get(conn, root <> "/manifest.json")

      assert get_resp_header(conn, "cache-control") == ["no-cache"]
    end
  end

  describe "downloads" do
    setup %{conn: conn, root: root, token: token} do
      publish(conn, root, token, "1.0.0", "firmware.bin", @firmware)
      :ok
    end

    test "a public repository serves bytes to anyone", %{root: root} do
      conn = get(build_conn(), root <> "/1.0.0/firmware.bin")

      assert conn.status == 200
      assert conn.resp_body == @firmware
    end

    test "'latest' resolves without the client knowing a version", %{root: root, token: token} do
      publish(build_conn(), root, token, "1.1.0", "firmware.bin", @firmware <> "newer")

      assert get(build_conn(), root <> "/latest/firmware.bin").resp_body == @firmware <> "newer"
    end

    test "an unknown version or filename is a 404", %{root: root} do
      assert get(build_conn(), root <> "/9.9.9/firmware.bin").status == 404
      assert get(build_conn(), root <> "/1.0.0/nothing.bin").status == 404
    end

    test "a version with a build-metadata '+' round-trips", %{root: root, token: token} do
      version = "0.1.0+ci20260916.g1a2b3c4"
      publish(build_conn(), root, token, version, "firmware.bin", @firmware <> "ci")

      [file] = manifest(build_conn(), root)["versions"][version]["files"]
      assert %URI{path: path} = URI.parse(file["url"])
      assert get(build_conn(), path).resp_body == @firmware <> "ci"
    end
  end

  describe "a private repository" do
    test "challenges an anonymous reader", %{} do
      {user, private} = repository_fixture(%{visibility: "private"})
      root = "/pkg/raw/#{Storage.owner_handle(user)}/#{private.name}"

      conn = get(build_conn(), root <> "/manifest.json")

      assert conn.status == 401
      assert [_challenge] = get_resp_header(conn, "www-authenticate")
    end

    test "serves the manifest to a token", %{} do
      {user, private} = repository_fixture(%{visibility: "private"})
      {:ok, _t, token} = Packages.create_registry_token(private, "ci", ["pull"])
      root = "/pkg/raw/#{Storage.owner_handle(user)}/#{private.name}"

      conn =
        build_conn()
        |> put_req_header("authorization", token)
        |> get(root <> "/manifest.json")

      assert conn.status == 200
    end
  end
end