defmodule GitGud.Packages.Formats.RpmTest do
@moduledoc """
The RPM adapter: publishing an `.rpm` and serving the rpm-md tree it
produces.
Packages are synthesised rather than downloaded. The header parser
has its own file and is tested against a real openSUSE package there;
what this file is about is the adapter — that a publish lands with
the metadata the index needs, that `repomd.xml` agrees with the
documents it points at, and that `_all` aggregates a namespace the
way apt's does.
The one property worth naming: `repomd.xml` pins a checksum, a size
and a timestamp for each document, and dnf rejects a repository where
those don't match what it downloads. So "the same request twice
produces the same bytes" is not tidiness here, it is the difference
between a working repository and one that fails on every second
fetch.
"""
use GitGud.DataCase, async: false
import GitGud.ForgeFixtures
import GitGud.RpmFixtures
alias GitGud.Packages
alias GitGud.Packages.Formats.Rpm
setup do
{user, repo} = repository_fixture()
%{user: user, repo: repo}
end
# ── driving the adapter ──────────────────────────────────────────────
defp ctx(overrides) do
Map.merge(
%{
path: [],
conn: Plug.Test.conn(:get, "/"),
method: "GET",
trailing_slash?: false,
base_url: "https://forge.test/pkg/rpm/acme/_all",
principal: :anonymous
},
overrides
)
end
defp publish(repo, body \\ nil),
do: Rpm.store(repo, ctx(%{method: "POST", path: ["upload"]}), body || build_rpm())
defp get(repo, path) do
{:ok, _type, body} = Rpm.index(repo, ctx(%{path: path}))
IO.iodata_to_binary(body)
end
defp gunzip(repo, path), do: repo |> get(path) |> :zlib.gunzip()
# ── tests ────────────────────────────────────────────────────────────
describe "publishing" do
test "stores an RPM under its EVR with the metadata the index needs", %{repo: repo} do
assert {:ok, "application/json", body} = publish(repo)
reply = body |> IO.iodata_to_binary() |> Jason.decode!()
assert reply["package"] == "koco"
assert reply["version"] == "0.1.2-1"
assert reply["arch"] == "x86_64"
assert reply["filename"] == "koco-0.1.2-1.x86_64.rpm"
assert [package] = Packages.list_packages(repo, "rpm")
assert package.name == "koco"
assert [file] = Packages.list_package_files(package)
meta = file.metadata["rpm"]
assert meta["license"] == "GPLv3"
assert meta["installed_size"] == 4096
assert meta["header_start"] > 96
end
test "an epoch shows up in the version, and zero doesn't", %{repo: repo} do
{:ok, _, body} = publish(repo, build_rpm(%{epoch: 2}))
assert Jason.decode!(IO.iodata_to_binary(body))["version"] == "2:0.1.2-1"
# RPM treats a missing epoch and epoch 0 as the same thing, so
# spelling the zero would make the common case read as `0:0.1.2-1`
# for no gain.
{:ok, _, plain} = publish(repo, build_rpm(%{epoch: 0, version: "0.2.0"}))
assert Jason.decode!(IO.iodata_to_binary(plain))["version"] == "0.2.0-1"
end
test "refuses something that isn't an RPM", %{repo: repo} do
assert {:error, {422, message}} = publish(repo, "hello, not an rpm")
assert message =~ "lead magic"
end
test "refuses a second upload of the same filename", %{repo: repo} do
{:ok, _, _} = publish(repo)
assert {:error, :file_exists} = publish(repo, build_rpm(%{size: 9999}))
end
test "only POST writes", %{repo: repo} do
assert {:error, {405, _}} = Rpm.store(repo, ctx(%{method: "PUT"}), build_rpm())
end
end
describe "primary.xml" do
setup %{repo: repo} do
{:ok, _, _} = publish(repo)
:ok
end
test "names the package the way dnf keys on it", %{repo: repo} do
xml = gunzip(repo, ["repodata", "primary.xml.gz"])
assert xml =~ ~s(packages="1")
assert xml =~ "<name>koco</name>"
assert xml =~ "<arch>x86_64</arch>"
assert xml =~ ~s(<version epoch="0" ver="0.1.2" rel="1"/>)
end
test "the location is where the package actually is", %{repo: repo} do
xml = gunzip(repo, ["repodata", "primary.xml.gz"])
assert [_, href] = Regex.run(~r/<location href="([^"]+)"/, xml)
# Not an assertion about the string: fetching it has to produce
# the blob. A location that 404s is the one index bug a client
# reports as "repository is broken" with no further detail.
assert {:blob, file} = Rpm.index(repo, ctx(%{path: String.split(href, "/")}))
assert file.filename == "koco-0.1.2-1.x86_64.rpm"
end
test "the checksum is the package's, and is marked as its id", %{repo: repo} do
xml = gunzip(repo, ["repodata", "primary.xml.gz"])
[package] = Packages.list_packages(repo, "rpm")
[file] = Packages.list_package_files(package)
assert xml =~ ~s(<checksum type="sha256" pkgid="YES">#{file.sha256}</checksum>)
end
test "drops rpmlib requirements the way createrepo does", %{repo: repo} do
xml = gunzip(repo, ["repodata", "primary.xml.gz"])
assert xml =~ ~s|<rpm:entry name="libc.so.6()(64bit)"|
refute xml =~ "rpmlib(FileDigests)"
end
test "carries the header range, so dnf can range-request", %{repo: repo} do
xml = gunzip(repo, ["repodata", "primary.xml.gz"])
assert xml =~ ~r/<rpm:header-range start="\d+" end="\d+"\/>/
end
test "lists the files anything might depend on, and not the rest", %{repo: repo} do
xml = gunzip(repo, ["repodata", "primary.xml.gz"])
# createrepo's filter: bin/ and /etc land in primary, everything
# else waits in filelists. Diverging would make our repository
# resolve differently from a stock one.
assert xml =~ "<file>/usr/bin/koco</file>"
refute xml =~ "/usr/share/doc/koco/README.md"
end
end
describe "filelists.xml" do
test "has every file, including the ones primary filtered", %{repo: repo} do
{:ok, _, _} = publish(repo)
xml = gunzip(repo, ["repodata", "filelists.xml.gz"])
assert xml =~ "<file>/usr/bin/koco</file>"
assert xml =~ "<file>/usr/share/doc/koco/README.md</file>"
end
end
describe "repomd.xml" do
setup %{repo: repo} do
{:ok, _, _} = publish(repo)
%{xml: get(repo, ["repodata", "repomd.xml"])}
end
test "points at all three documents", %{xml: xml} do
for kind <- ~w(primary filelists other) do
assert xml =~ ~s(<data type="#{kind}">)
assert xml =~ ~s(<location href="repodata/#{kind}.xml.gz"/>)
end
end
test "its checksums match the bytes actually served", %{repo: repo, xml: xml} do
# The whole reason the documents may not depend on the clock. A
# mismatch here is what dnf reports as a corrupt repository.
for kind <- ~w(primary filelists other) do
[_, compressed, plain] =
Regex.run(
~r/<data type="#{kind}">.*?<checksum type="sha256">(\w+)<.*?<open-checksum type="sha256">(\w+)</s,
xml
)
served = get(repo, ["repodata", "#{kind}.xml.gz"])
assert digest(served) == compressed
assert digest(:zlib.gunzip(served)) == plain
end
end
test "its sizes match too", %{repo: repo, xml: xml} do
[_, size, open_size] =
Regex.run(~r/<data type="primary">.*?<size>(\d+)<.*?<open-size>(\d+)</s, xml)
served = get(repo, ["repodata", "primary.xml.gz"])
assert Integer.to_string(byte_size(served)) == size
assert Integer.to_string(byte_size(:zlib.gunzip(served))) == open_size
end
test "the revision is the newest publish, not now", %{repo: repo, xml: xml} do
[_, revision] = Regex.run(~r|<revision>(\d+)</revision>|, xml)
[package] = Packages.list_packages(repo, "rpm")
[file] = Packages.list_package_files(package)
expected = file.inserted_at |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_unix()
assert String.to_integer(revision) == expected
end
test "the same request twice is the same bytes", %{repo: repo, xml: xml} do
assert get(repo, ["repodata", "repomd.xml"]) == xml
assert get(repo, ["repodata", "primary.xml.gz"]) ==
get(repo, ["repodata", "primary.xml.gz"])
end
test "an empty repository is still a valid one", %{user: user} do
{_user, empty} = repository_fixture(user)
xml = get(empty, ["repodata", "repomd.xml"])
assert xml =~ "<revision>0</revision>"
assert gunzip(empty, ["repodata", "primary.xml.gz"]) =~ ~s(packages="0")
end
end
describe "signing" do
test "repomd.xml.asc verifies against the key repomd.xml.key serves", %{repo: repo} do
{:ok, _, _} = publish(repo)
document = get(repo, ["repodata", "repomd.xml"])
{:ok, "application/pgp-signature", signature} =
Rpm.index(repo, ctx(%{path: ["repodata", "repomd.xml.asc"]}))
{:ok, "application/pgp-keys", key} =
Rpm.index(repo, ctx(%{path: ["repodata", "repomd.xml.key"]}))
# This is exactly what `repo_gpgcheck=1` does.
assert {:ok, true} =
GitGud.Packages.Apt.Native.verify_detached(key, document, signature)
end
test "the key is the namespace's, not the repository's", %{user: user, repo: repo} do
{:ok, %{public_pem: from_repo}} = Packages.signing_key(repo, "rpm")
{:ok, %{public_pem: from_namespace}} = Packages.signing_key(user, "rpm")
# One `gpgkey=` covers every repository the namespace publishes,
# and covers `_all` as well.
assert from_repo == from_namespace
end
test "it is a different key from the deb one", %{user: user} do
{:ok, %{public_pem: rpm}} = Packages.signing_key(user, "rpm")
{:ok, %{public_pem: deb}} = Packages.signing_key(user, "deb")
refute rpm == deb
end
end
describe "browsing" do
test "the root lists the tree apt-style", %{repo: repo} do
{:ok, _, _} = publish(repo)
html = get(repo, [])
assert html =~ "repodata/"
assert html =~ "Packages/"
assert html =~ "dnf reads this tree"
end
test "a path that serves nothing is a 404, not an empty page", %{repo: repo} do
assert {:error, :not_found} = Rpm.index(repo, ctx(%{path: ["nope"]}))
end
end
defp digest(bytes), do: :sha256 |> :crypto.hash(bytes) |> Base.encode16(case: :lower)
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
10.4 KiB · text
History
b868907