defmodule GitGudWeb.RpmNamespaceTest do
@moduledoc """
The namespace-wide RPM repository at `_all`, over HTTP.
The adapter's own tests drive it directly; these go through the
controller, because what is untested there is the wiring: that the
route resolves, that `_all` aggregates, and — the part worth a
controller test rather than a unit one — that the index a caller gets
is filtered by what that caller can read.
A dashboard listing a package a client can't fetch is a nuisance. An
*index* listing one is worse: dnf resolves a transaction against it
and then fails on the download, blaming the mirror.
"""
use GitGudWeb.ConnCase, async: false
import GitGud.AccountsFixtures
import GitGud.ForgeFixtures
import GitGud.RpmFixtures
alias GitGud.Packages
alias GitGud.Packages.Formats.Rpm
alias GitGud.Repositories
setup do
user = user_fixture()
{:ok, org} =
GitGud.Organizations.create_organization(user, %{
"handle" => "org#{System.unique_integer([:positive])}",
"name" => "Test Org"
})
%{
user: user,
org: org,
tools: org_repo(org, user),
agents: org_repo(org, user),
root: "/pkg/rpm/#{org.handle}/_all"
}
end
defp org_repo(org, user, visibility \\ "public") do
{:ok, repo} =
Repositories.create_repository_for_org(org, user, %{
"name" => unique_repo_name(),
"visibility" => visibility
})
GitGud.Repo.preload(repo, [:owner, :organization])
end
# Published through the adapter rather than assembled by hand: the
# point here is the aggregate, and the adapter's own file is where
# the header format is pinned down.
defp publish(repo, name, version \\ "0.1.2") do
body = build_rpm(%{name: name, version: version})
{:ok, _type, _reply} =
Rpm.store(
repo,
%{
path: ["upload"],
conn: Plug.Test.conn(:post, "/"),
method: "POST",
trailing_slash?: false,
base_url: "https://forge.test",
principal: :anonymous
},
body
)
body
end
defp primary(conn, root) do
conn |> get(root <> "/repodata/primary.xml.gz") |> Map.fetch!(:resp_body) |> :zlib.gunzip()
end
describe "aggregation" do
test "one index carries packages from every repository in the org", %{
conn: conn,
tools: tools,
agents: agents,
root: root
} do
publish(tools, "widget")
publish(agents, "gadget")
xml = primary(conn, root)
assert xml =~ "<name>widget</name>"
assert xml =~ "<name>gadget</name>"
assert xml =~ ~s(packages="2")
end
test "each entry points into its own repository's tree", %{
conn: conn,
tools: tools,
agents: agents,
root: root
} do
publish(tools, "widget")
publish(agents, "gadget")
xml = primary(conn, root)
assert xml =~ ~s(href="Packages/#{tools.name}/w/widget-0.1.2-1.x86_64.rpm")
assert xml =~ ~s(href="Packages/#{agents.name}/g/gadget-0.1.2-1.x86_64.rpm")
end
test "two repositories publishing the same filename don't collide", %{
conn: conn,
tools: tools,
agents: agents,
root: root
} do
from_tools = publish(tools, "koco")
from_agents = publish(agents, "koco", "0.1.3")
# Same package name in two repositories. A stock rpm-md tree
# would file both under `Packages/k/`, and one set of bytes would
# quietly win.
assert conn
|> get(root <> "/Packages/#{tools.name}/k/koco-0.1.2-1.x86_64.rpm")
|> response(200) == from_tools
assert conn
|> get(root <> "/Packages/#{agents.name}/k/koco-0.1.3-1.x86_64.rpm")
|> response(200) == from_agents
end
test "repomd.xml is served and points at the documents", %{
conn: conn,
tools: tools,
root: root
} do
publish(tools, "widget")
body = conn |> get(root <> "/repodata/repomd.xml") |> response(200)
assert body =~ ~s(<location href="repodata/primary.xml.gz"/>)
assert body =~ "<revision>"
end
end
describe "a version with a + in it" do
test "round-trips from the index href to the blob", %{conn: conn, tools: tools, root: root} do
# CI stamps untagged builds `<version>+dev.<unix>`, because RPM
# forbids `-` in a version and `+` sorts after the bare release.
# That puts a `+` in every filename a dev build publishes, and a
# `+` is literal in a URI path but means a space in a query
# string — so this is exactly the character that gets mangled by
# one layer decoding a path as if it were a form.
body = publish(tools, "koco", "0.1.2+dev.1757000000")
xml = primary(conn, root)
assert [_, href] = Regex.run(~r/<location href="([^"]+)"/, xml)
assert href =~ "+dev."
# Fetched as the index advertises it, not as a hand-written path.
assert conn |> get(root <> "/" <> href) |> response(200) == body
end
end
describe "visibility" do
test "an anonymous client's index omits the private repository", %{
conn: conn,
org: org,
user: user,
tools: tools,
root: root
} do
publish(tools, "widget")
private = org_repo(org, user, "private")
publish(private, "secret-thing")
xml = primary(conn, root)
assert xml =~ "<name>widget</name>"
refute xml =~ "secret-thing"
assert xml =~ ~s(packages="1")
end
test "a token for the private repository sees it in the same URL", %{
conn: conn,
org: org,
user: user,
root: root
} do
private = org_repo(org, user, "private")
publish(private, "secret-thing")
{:ok, _t, token} = Packages.create_registry_token(private, "ci", ["pull"])
# A browser session is not a credential here: the registries
# authenticate on the Authorization header only, because dnf and
# apt have no session to offer.
xml = conn |> put_req_header("authorization", token) |> primary(root)
assert xml =~ "secret-thing"
end
test "a private package's path 404s for a caller who can't read it", %{
conn: conn,
org: org,
user: user,
root: root
} do
private = org_repo(org, user, "private")
publish(private, "secret-thing")
path = root <> "/Packages/#{private.name}/s/secret-thing-0.1.2-1.x86_64.rpm"
# Not just absent from the index — unreachable. An index is a
# convenience; the blob path is the access decision.
assert get(conn, path).status in [401, 404]
{:ok, _t, token} = Packages.create_registry_token(private, "ci", ["pull"])
assert conn |> put_req_header("authorization", token) |> get(path) |> response(200)
end
end
describe "the key" do
test "is served unauthenticated at the namespace URL", %{conn: conn, org: org} do
body = conn |> get("/pkg/_keys/rpm/#{org.handle}") |> response(200)
# gpgkey= is the first line of a .repo file, before any
# credential is configured.
assert body =~ "BEGIN PGP PUBLIC KEY BLOCK"
end
test "and is the same key repomd.xml.key serves", %{conn: conn, org: org, root: root} do
from_keys = conn |> get("/pkg/_keys/rpm/#{org.handle}") |> response(200)
from_repo = conn |> get(root <> "/repodata/repomd.xml.key") |> response(200)
assert from_keys == from_repo
end
test "the aggregate's signature verifies against it", %{
conn: conn,
org: org,
tools: tools,
root: root
} do
publish(tools, "widget")
key = conn |> get("/pkg/_keys/rpm/#{org.handle}") |> response(200)
document = conn |> get(root <> "/repodata/repomd.xml") |> response(200)
signature = conn |> get(root <> "/repodata/repomd.xml.asc") |> response(200)
assert {:ok, true} =
GitGud.Packages.Apt.Native.verify_detached(key, document, signature)
# And the signature covers the filtered view, not some canonical
# one: a caller who sees fewer repositories still gets a document
# that verifies.
assert Packages.get_signing_key(org, "rpm")
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
8.0 KiB · text
History
ec2cfdc