defmodule GitGudWeb.CargoRegistryTest do
@moduledoc """
Phase 3: the Cargo sparse index and publish API, over HTTP.
The interesting assertions are the ones where a mistake is silent
rather than loud — the prefix shard, the dependency rename
translation, `features2`/`v: 2`, and the 304.
"""
use GitGudWeb.ConnCase, async: false
alias GitGud.Packages
alias GitGud.Packages.Formats.Cargo
alias GitGud.Repositories.Storage
import GitGud.ForgeFixtures
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/cargo/#{Storage.owner_handle(user)}/#{repo.name}"}
end
# `cargo publish`'s body: u32 LE length, JSON metadata, u32 LE
# length, the .crate tarball.
defp publish_body(metadata, crate) do
json = Jason.encode!(metadata)
<<byte_size(json)::little-unsigned-32, json::binary, byte_size(crate)::little-unsigned-32,
crate::binary>>
end
defp publish(conn, root, token, metadata, crate \\ "crate bytes") do
conn =
if token, do: put_req_header(conn, "authorization", token), else: conn
conn
# Cargo really does label this JSON. It is not JSON.
|> put_req_header("content-type", "application/json")
|> put_req_header("accept", "application/json")
|> put(root <> "/api/v1/crates/new", publish_body(metadata, crate))
end
defp meta(name, vers, extra \\ %{}) do
Map.merge(%{"name" => name, "vers" => vers, "deps" => [], "features" => %{}}, extra)
end
defp index_lines(conn, root, name) do
path = Enum.join(Cargo.index_path(name), "/")
conn = get(conn, root <> "/" <> path)
{conn,
conn.resp_body
|> String.split("\n", trim: true)
|> Enum.map(&Jason.decode!/1)}
end
describe "index_path/1" do
test "shards by the lowercased name's length" do
assert Cargo.index_path("a") == ["1", "a"]
assert Cargo.index_path("ab") == ["2", "ab"]
assert Cargo.index_path("abc") == ["3", "a", "abc"]
assert Cargo.index_path("cargo") == ["ca", "rg", "cargo"]
assert Cargo.index_path("Serde") == ["se", "rd", "serde"]
end
end
describe "config.json" do
test "points cargo at the download and API roots", %{conn: conn, root: root} do
conn = get(conn, root <> "/config.json")
assert conn.status == 200
config = Jason.decode!(conn.resp_body)
assert String.ends_with?(config["dl"], "/api/v1/crates")
assert String.ends_with?(config["api"], root)
end
test "a public registry does not demand a token for every fetch", %{conn: conn, root: root} do
assert Jason.decode!(get(conn, root <> "/config.json").resp_body)["auth-required"] == false
end
test "a private one does" do
{user, private} = repository_fixture(%{visibility: "private"})
{:ok, _t, token} = Packages.create_registry_token(private, "ci", ["pull"])
root = "/pkg/cargo/#{Storage.owner_handle(user)}/#{private.name}"
conn =
build_conn()
|> put_req_header("authorization", token)
|> get(root <> "/config.json")
assert Jason.decode!(conn.resp_body)["auth-required"] == true
end
end
describe "the index" do
test "serves one JSON object per version, newline-delimited", %{
conn: conn,
root: root,
token: token
} do
publish(conn, root, token, meta("widget", "1.0.0"))
publish(build_conn(), root, token, meta("widget", "1.1.0"))
{conn, lines} = index_lines(build_conn(), root, "widget")
assert conn.status == 200
assert get_resp_header(conn, "content-type") == ["text/plain; charset=utf-8"]
assert Enum.map(lines, & &1["vers"]) == ["1.0.0", "1.1.0"]
end
test "cksum is the sha256 of the .crate", %{conn: conn, root: root, token: token} do
publish(conn, root, token, meta("widget", "1.0.0"), "the real crate")
{_conn, [entry]} = index_lines(build_conn(), root, "widget")
assert entry["cksum"] ==
Base.encode16(:crypto.hash(:sha256, "the real crate"), case: :lower)
end
test "a crate served from the wrong shard is a 404", %{conn: conn, root: root, token: token} do
publish(conn, root, token, meta("widget", "1.0.0"))
# widget's real shard is wi/dg/widget.
assert get(build_conn(), root <> "/wi/dg/widget").status == 200
assert get(build_conn(), root <> "/3/w/widget").status == 404
assert get(build_conn(), root <> "/xx/yy/widget").status == 404
end
test "404 for a crate nobody published", %{conn: conn, root: root} do
assert get(conn, root <> "/no/th/nothing").status == 404
end
test "only `yanked` changes after publication", %{
conn: conn,
repo: repo,
root: root,
token: token
} do
publish(conn, root, token, meta("widget", "1.0.0"))
{_conn, [before]} = index_lines(build_conn(), root, "widget")
pkg = Packages.get_package(repo, "cargo", "widget")
{:ok, _} = Packages.set_yanked(Packages.get_version(pkg, "1.0.0"), true)
{_conn, [after_yank]} = index_lines(build_conn(), root, "widget")
assert before["yanked"] == false
assert after_yank["yanked"] == true
assert Map.delete(before, "yanked") == Map.delete(after_yank, "yanked")
end
end
describe "features2 and v" do
test "a crate with only old-style features stays at v: 1", %{
conn: conn,
root: root,
token: token
} do
publish(
conn,
root,
token,
meta("widget", "1.0.0", %{"features" => %{"default" => ["std"], "std" => []}})
)
{_conn, [entry]} = index_lines(build_conn(), root, "widget")
assert entry["v"] == 1
refute Map.has_key?(entry, "features2")
assert entry["features"] == %{"default" => ["std"], "std" => []}
end
test "`dep:` and `pkg?/feat` move to features2 and bump v to 2", %{
conn: conn,
root: root,
token: token
} do
features = %{
"default" => ["std"],
"serde" => ["dep:serde"],
"json" => ["serde?/derive"]
}
publish(conn, root, token, meta("widget", "1.0.0", %{"features" => features}))
{_conn, [entry]} = index_lines(build_conn(), root, "widget")
assert entry["v"] == 2
# Old cargo reads `features` and ignores `features2` — so the
# features it can still understand must stay where it looks.
assert entry["features"] == %{"default" => ["std"]}
assert entry["features2"] == %{"serde" => ["dep:serde"], "json" => ["serde?/derive"]}
end
test "split_features/1 partitions on the new syntax" do
assert Cargo.split_features(%{"a" => ["b"], "c" => ["dep:d"], "e" => ["f?/g"]}) ==
{%{"a" => ["b"]}, %{"c" => ["dep:d"], "e" => ["f?/g"]}}
end
end
describe "dependencies" do
test "a renamed dependency swaps name and package", %{
conn: conn,
root: root,
token: token
} do
deps = [
%{
"name" => "serde",
"explicit_name_in_toml" => "serde1",
"version_req" => "^1.0",
"features" => ["derive"],
"optional" => true,
"default_features" => false,
"target" => "cfg(unix)",
"kind" => "normal",
"registry" => "https://example.invalid"
}
]
publish(conn, root, token, meta("widget", "1.0.0", %{"deps" => deps}))
{_conn, [entry]} = index_lines(build_conn(), root, "widget")
assert [dep] = entry["deps"]
# The index names the dependency as the crate refers to it…
assert dep["name"] == "serde1"
# …and says which crate that actually is.
assert dep["package"] == "serde"
# `version_req` on the way in, `req` on the way out.
assert dep["req"] == "^1.0"
assert dep["features"] == ["derive"]
assert dep["optional"] == true
assert dep["default_features"] == false
assert dep["target"] == "cfg(unix)"
assert dep["kind"] == "normal"
end
test "an unrenamed dependency has a null package", %{conn: conn, root: root, token: token} do
deps = [%{"name" => "serde", "version_req" => "^1.0"}]
publish(conn, root, token, meta("widget", "1.0.0", %{"deps" => deps}))
{_conn, [entry]} = index_lines(build_conn(), root, "widget")
assert [%{"name" => "serde", "package" => nil, "default_features" => true}] = entry["deps"]
end
end
describe "rust_version" do
test "is stored without an operator", %{conn: conn, root: root, token: token} do
publish(conn, root, token, meta("widget", "1.0.0", %{"rust_version" => "=1.60"}))
{_conn, [entry]} = index_lines(build_conn(), root, "widget")
assert entry["rust_version"] == "1.60"
end
test "is absent when the crate doesn't declare one", %{conn: conn, root: root, token: token} do
publish(conn, root, token, meta("widget", "1.0.0"))
{_conn, [entry]} = index_lines(build_conn(), root, "widget")
refute Map.has_key?(entry, "rust_version")
end
end
describe "caching" do
test "an unchanged index revalidates to 304", %{conn: conn, root: root, token: token} do
publish(conn, root, token, meta("widget", "1.0.0"))
{first, _} = index_lines(build_conn(), root, "widget")
assert [etag] = get_resp_header(first, "etag")
revalidated =
build_conn()
|> put_req_header("if-none-match", etag)
|> get(root <> "/wi/dg/widget")
assert revalidated.status == 304
assert revalidated.resp_body == ""
end
test "a weak validator from a proxy still matches", %{conn: conn, root: root, token: token} do
publish(conn, root, token, meta("widget", "1.0.0"))
{first, _} = index_lines(build_conn(), root, "widget")
[etag] = get_resp_header(first, "etag")
revalidated =
build_conn()
|> put_req_header("if-none-match", "W/" <> etag)
|> get(root <> "/wi/dg/widget")
assert revalidated.status == 304
end
test "publishing a new version invalidates it", %{conn: conn, root: root, token: token} do
publish(conn, root, token, meta("widget", "1.0.0"))
{first, _} = index_lines(build_conn(), root, "widget")
[etag] = get_resp_header(first, "etag")
publish(build_conn(), root, token, meta("widget", "1.1.0"))
refreshed =
build_conn()
|> put_req_header("if-none-match", etag)
|> get(root <> "/wi/dg/widget")
assert refreshed.status == 200
end
end
describe "download" do
test "serves the .crate bytes", %{conn: conn, root: root, token: token} do
publish(conn, root, token, meta("widget", "1.0.0"), "the real crate")
conn = get(build_conn(), root <> "/api/v1/crates/widget/1.0.0/download")
assert conn.status == 200
assert conn.resp_body == "the real crate"
end
test "404 for a version that was never published", %{conn: conn, root: root, token: token} do
publish(conn, root, token, meta("widget", "1.0.0"))
assert get(build_conn(), root <> "/api/v1/crates/widget/9.9.9/download").status == 404
end
end
describe "publish" do
test "answers with the warnings object cargo expects", %{
conn: conn,
root: root,
token: token
} do
conn = publish(conn, root, token, meta("widget", "1.0.0"))
assert conn.status == 200
assert %{"warnings" => %{"invalid_categories" => [], "invalid_badges" => [], "other" => []}} =
Jason.decode!(conn.resp_body)
end
test "a truncated metadata prefix is a cargo-shaped error", %{
conn: conn,
root: root,
token: token
} do
conn =
conn
|> put_req_header("authorization", token)
|> put_req_header("content-type", "application/json")
|> put(root <> "/api/v1/crates/new", <<255, 255, 255, 255, 1, 2>>)
assert conn.status == 400
assert %{"errors" => [%{"detail" => detail}]} = Jason.decode!(conn.resp_body)
assert detail =~ "metadata length prefix"
end
test "a truncated crate prefix is caught too", %{conn: conn, root: root, token: token} do
json = Jason.encode!(meta("widget", "1.0.0"))
conn =
conn
|> put_req_header("authorization", token)
|> put_req_header("content-type", "application/json")
|> put(
root <> "/api/v1/crates/new",
<<byte_size(json)::little-unsigned-32, json::binary, 255, 255, 255, 255>>
)
assert conn.status == 400
assert Jason.decode!(conn.resp_body)["errors"] != []
end
test "409 on republishing the same version with different bytes", %{
conn: conn,
root: root,
token: token
} do
publish(conn, root, token, meta("widget", "1.0.0"), "first")
conn = publish(build_conn(), root, token, meta("widget", "1.0.0"), "second")
assert conn.status == 409
end
test "an anonymous publish is refused", %{conn: conn, root: root} do
assert publish(conn, root, nil, meta("widget", "1.0.0")).status == 401
end
end
describe "yank and unyank" do
test "round-trip without removing the bytes", %{
conn: conn,
repo: repo,
root: root,
token: token
} do
publish(conn, root, token, meta("widget", "1.0.0"), "the real crate")
yanked =
build_conn()
|> put_req_header("authorization", token)
|> delete(root <> "/api/v1/crates/widget/1.0.0/yank")
assert yanked.status == 200
assert Jason.decode!(yanked.resp_body) == %{"ok" => true}
pkg = Packages.get_package(repo, "cargo", "widget")
assert Packages.get_version(pkg, "1.0.0").yanked
# A build that already pinned this version must still resolve it.
assert get(build_conn(), root <> "/api/v1/crates/widget/1.0.0/download").resp_body ==
"the real crate"
unyanked =
build_conn()
|> put_req_header("authorization", token)
|> put(root <> "/api/v1/crates/widget/1.0.0/unyank")
assert unyanked.status == 200
refute Packages.get_version(pkg, "1.0.0").yanked
end
test "yanking needs write access", %{conn: conn, repo: repo, root: root, token: token} do
publish(conn, root, token, meta("widget", "1.0.0"))
{:ok, _t, readonly} = Packages.create_registry_token(repo, "ro", ["pull"])
conn =
build_conn()
|> put_req_header("authorization", readonly)
|> delete(root <> "/api/v1/crates/widget/1.0.0/yank")
assert conn.status == 403
end
test "404 for an unpublished version", %{conn: conn, root: root, token: token} do
conn =
conn
|> put_req_header("authorization", token)
|> delete(root <> "/api/v1/crates/ghost/1.0.0/yank")
assert conn.status == 404
end
end
describe "search and owners" do
test "search matches on the normalized name", %{conn: conn, root: root, token: token} do
publish(conn, root, token, meta("widget", "1.0.0", %{"description" => "a widget"}))
publish(build_conn(), root, token, meta("gadget", "2.0.0"))
body = get(build_conn(), root <> "/api/v1/crates?q=WID").resp_body |> Jason.decode!()
assert [%{"name" => "widget", "max_version" => "1.0.0", "description" => "a widget"}] =
body["crates"]
assert body["meta"]["total"] == 1
end
test "search with no query lists everything, capped by per_page", %{
conn: conn,
root: root,
token: token
} do
for n <- ["alpha", "beta", "gamma"],
do: publish(build_conn(), root, token, meta(n, "1.0.0"))
body = get(conn, root <> "/api/v1/crates?per_page=2").resp_body |> Jason.decode!()
assert length(body["crates"]) == 2
end
test "owners reports the repository's owner", %{conn: conn, root: root, token: token} do
publish(conn, root, token, meta("widget", "1.0.0"))
body =
get(build_conn(), root <> "/api/v1/crates/widget/owners").resp_body |> Jason.decode!()
assert [%{"login" => login}] = body["users"]
assert is_binary(login)
end
test "changing owners is refused — the forge is the source of truth", %{
conn: conn,
root: root,
token: token
} do
publish(conn, root, token, meta("widget", "1.0.0"))
conn =
build_conn()
|> put_req_header("authorization", token)
|> put_req_header("content-type", "application/json")
|> put(root <> "/api/v1/crates/widget/owners", Jason.encode!(%{"users" => ["someone"]}))
assert conn.status == 403
assert Jason.decode!(conn.resp_body)["errors"] != []
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
16.4 KiB · text
History
b868907