defmodule GitGud.Packages.Formats.FdroidTest do
@moduledoc """
The F-Droid adapter: publishing an APK and serving the index it
produces.
Uploads go through a synthesised APK rather than a real one — a real
one is 19 MB and the parsers it exercises have their own tests. What
this file is about is the adapter: that a publish lands as a
`PackageFile` with the metadata the index needs, that the index
document has the shape F-Droid expects, and that `_all` aggregates
namespaces the way apt's does.
"""
use GitGud.DataCase, async: false
import GitGud.ForgeFixtures
alias GitGud.Packages
alias GitGud.Packages.Formats.Fdroid
@cert GitGud.ApkFixtures.signing_cert()
# A tiny but real PNG, so what comes back out is checkable.
@icon_png GitGud.ApkFixtures.icon_png()
setup do
{user, repo} = repository_fixture()
%{user: user, repo: repo}
end
defp build_apk(extra_entries \\ []), do: GitGud.ApkFixtures.signed_apk(extra_entries)
defp ctx(overrides) do
Map.merge(
%{
path: [],
conn: Plug.Test.conn(:get, "/"),
method: "GET",
trailing_slash?: false,
base_url: "https://forge.test/pkg/fdroid/acme/_all",
principal: :anonymous
},
overrides
)
end
defp publish(repo, body) do
Fdroid.store(repo, ctx(%{method: "POST", path: ["upload"]}), body)
end
defp decode({:ok, "application/json", body}),
do: body |> IO.iodata_to_binary() |> Jason.decode!()
# ── tests ────────────────────────────────────────────────────────────
describe "publishing" do
test "stores an APK with the metadata the index needs", %{repo: repo} do
assert {:ok, "application/json", body} = publish(repo, build_apk())
reply = body |> IO.iodata_to_binary() |> Jason.decode!()
assert reply["package"] == "org.neiam.gitgud.app"
assert reply["versionCode"] == 1
assert reply["versionName"] == "0.1.0"
assert reply["filename"] == "org.neiam.gitgud.app_1.apk"
# The signer is read out of the APK, never asserted by the client.
assert reply["signer"] ==
:sha256 |> :crypto.hash(@cert) |> Base.encode16(case: :lower)
assert [package] = Packages.list_packages(repo, "fdroid")
assert package.name == "org.neiam.gitgud.app"
assert [file] = Packages.list_package_files(package)
assert file.metadata["min_sdk"] == 26
assert file.metadata["nativecode"] == ["arm64-v8a"]
end
test "republishing a versionCode says which one, and why", %{repo: repo} do
apk = build_apk()
{:ok, _, _} = publish(repo, apk)
# Same versionCode, different bytes — which is what a CI build
# with a hardcoded versionCode produces on its second run.
different = build_apk([{~c"extra.txt", "changes the digest"}])
assert {:error, {409, message}} = publish(repo, different)
assert message =~ "org.neiam.gitgud.app_1.apk"
assert message =~ "versionCode 1"
assert message =~ "won't install an update"
end
test "republishing identical bytes is fine", %{repo: repo} do
apk = build_apk()
assert {:ok, _, _} = publish(repo, apk)
# A re-run that produced the same artifact is not a conflict.
assert {:ok, _, _} = publish(repo, apk)
end
test "refuses something that isn't an APK", %{repo: repo} do
assert {:error, {422, _}} = publish(repo, "definitely not a zip")
end
test "refuses anything but POST", %{repo: repo} do
assert {:error, {405, _}} = Fdroid.store(repo, ctx(%{method: "DELETE"}), "")
end
end
describe "index-v2" do
setup %{repo: repo} do
{:ok, _, _} = publish(repo, build_apk())
:ok
end
test "describes the repository and its packages", %{repo: repo} do
assert %{"repo" => r, "packages" => packages} =
decode(Fdroid.index(repo, ctx(%{path: ["index-v2.json"]})))
assert r["address"] == "https://forge.test/pkg/fdroid/acme/_all/repo"
# Milliseconds. Seconds would read as 1970 and every client would
# treat the repository as permanently stale.
assert r["timestamp"] > 1_700_000_000_000
assert %{"org.neiam.gitgud.app" => app} = packages
assert [{sha, version}] = Map.to_list(app["versions"])
assert String.match?(sha, ~r/\A[0-9a-f]{64}\z/)
assert version["file"]["name"] == "/org.neiam.gitgud.app_1.apk"
assert version["file"]["sha256"] == sha
assert version["manifest"]["versionCode"] == 1
assert version["manifest"]["versionName"] == "0.1.0"
assert version["manifest"]["usesSdk"] == %{"minSdkVersion" => 26, "targetSdkVersion" => 34}
assert version["manifest"]["nativecode"] == ["arm64-v8a"]
end
test "carries the signing certificate as a list", %{repo: repo} do
assert %{"packages" => %{"org.neiam.gitgud.app" => app}} =
decode(Fdroid.index(repo, ctx(%{path: ["index-v2.json"]})))
[{_sha, version}] = Map.to_list(app["versions"])
expected = :sha256 |> :crypto.hash(@cert) |> Base.encode16(case: :lower)
assert version["manifest"]["signer"] == %{"sha256" => [expected]}
end
test "lists the permissions the manifest declares", %{repo: repo} do
assert %{"packages" => %{"org.neiam.gitgud.app" => app}} =
decode(Fdroid.index(repo, ctx(%{path: ["index-v2.json"]})))
[{_sha, version}] = Map.to_list(app["versions"])
names = Enum.map(version["manifest"]["usesPermission"], & &1["name"])
assert "android.permission.INTERNET" in names
assert "android.permission.CAMERA" in names
end
end
describe "entry" do
test "pins index-v2.json by hash and size", %{repo: repo} do
{:ok, _, _} = publish(repo, build_apk())
entry = decode(Fdroid.index(repo, ctx(%{path: ["entry.json"]})))
index = Fdroid.index(repo, ctx(%{path: ["index-v2.json"]})) |> decode_raw()
assert entry["version"] == 20_002
assert entry["index"]["name"] == "/index-v2.json"
assert entry["index"]["numPackages"] == 1
assert entry["index"]["size"] == byte_size(index)
# The signature on entry.jar covers the index transitively, so
# this hash is the only thing making that true.
assert entry["index"]["sha256"] ==
:sha256 |> :crypto.hash(index) |> Base.encode16(case: :lower)
end
test "the index is byte-identical across requests", %{repo: repo} do
{:ok, _, _} = publish(repo, build_apk())
first = Fdroid.index(repo, ctx(%{path: ["index-v2.json"]})) |> decode_raw()
second = Fdroid.index(repo, ctx(%{path: ["index-v2.json"]})) |> decode_raw()
# Not a nicety. entry.json pins this document by hash, so a
# timestamp of `now` would make every fetch look like tampering to
# a client that had just been handed the hash — and would make the
# sign-at-publish design impossible, since there'd be nothing
# stable to sign.
assert first == second
end
test "an empty repository reports a zero timestamp, not the current time", %{repo: repo} do
# Claiming "updated just now" on every request is both false and a
# rollback that the first real publish has to climb out of.
assert %{"repo" => %{"timestamp" => 0}} =
decode(Fdroid.index(repo, ctx(%{path: ["index-v2.json"]})))
end
test "entry.jar is a signed JAR carrying entry.json", %{repo: repo} do
{:ok, _, _} = publish(repo, build_apk())
assert {:ok, "application/java-archive", jar} =
Fdroid.index(repo, ctx(%{path: ["entry.jar"]}))
{:ok, members} = :zip.unzip(IO.iodata_to_binary(jar), [:memory])
names = members |> Enum.map(fn {n, _} -> List.to_string(n) end) |> MapSet.new()
assert "META-INF/MANIFEST.MF" in names
assert "META-INF/CERT.RSA" in names
assert "entry.json" in names
# The JAR's payload has to be the same document /entry.json
# serves, or the hash it pins would be signing one index while
# clients fetched another.
{_, packed} = Enum.find(members, fn {n, _} -> n == ~c"entry.json" end)
assert Jason.decode!(packed) == decode(Fdroid.index(repo, ctx(%{path: ["entry.json"]})))
end
test "the repository reports a pinnable fingerprint", %{user: user} do
assert {:ok, fingerprint} = Fdroid.fingerprint(user)
assert String.match?(fingerprint, ~r/\A[0-9a-f]{64}\z/)
# Stable, because it is what a user pins when adding the repo.
assert {:ok, ^fingerprint} = Fdroid.fingerprint(user)
end
end
describe "index-v1" do
setup %{repo: repo} do
{:ok, _, _} = publish(repo, build_apk())
:ok
end
test "is served, because a v2-only repository reads as broken", %{repo: repo} do
# A client that can't speak v2 falls back here silently after
# fetching a perfectly good entry.jar, and reports the 404 —
# "invalid repository / NotFoundException", pointing at nothing.
assert {:ok, "application/java-archive", jar} =
Fdroid.index(repo, ctx(%{path: ["index-v1.jar"]}))
{:ok, members} = :zip.unzip(IO.iodata_to_binary(jar), [:memory])
names = members |> Enum.map(fn {n, _} -> List.to_string(n) end) |> MapSet.new()
assert "index-v1.json" in names
assert "META-INF/CERT.RSA" in names
end
test "carries the flat shape v1 expects", %{repo: repo} do
index = decode(Fdroid.index(repo, ctx(%{path: ["index-v1.json"]})))
assert index["repo"]["version"] == 20_001
assert index["repo"]["address"] == "https://forge.test/pkg/fdroid/acme/_all/repo"
# Read unconditionally by the client even when empty.
assert index["requests"] == %{"install" => [], "uninstall" => []}
assert [app] = index["apps"]
assert app["packageName"] == "org.neiam.gitgud.app"
assert app["suggestedVersionCode"] == "1"
assert [pkg] = index["packages"]["org.neiam.gitgud.app"]
assert pkg["apkName"] == "org.neiam.gitgud.app_1.apk"
assert pkg["hashType"] == "sha256"
assert pkg["versionCode"] == 1
assert pkg["minSdkVersion"] == 26
assert pkg["signer"] == :sha256 |> :crypto.hash(@cert) |> Base.encode16(case: :lower)
end
test "spells permissions as [name, maxSdk] pairs, not v2's objects", %{repo: repo} do
index = decode(Fdroid.index(repo, ctx(%{path: ["index-v1.json"]})))
[pkg] = index["packages"]["org.neiam.gitgud.app"]
assert ["android.permission.CAMERA", nil] in pkg["uses-permission"]
end
end
describe "serving APKs" do
test "hands back the stored file by the name the index points at", %{repo: repo} do
{:ok, _, _} = publish(repo, build_apk())
assert {:blob, file} = Fdroid.index(repo, ctx(%{path: ["org.neiam.gitgud.app_1.apk"]}))
assert file.filename == "org.neiam.gitgud.app_1.apk"
end
test "404s an unknown file", %{repo: repo} do
assert {:error, {404, _}} = Fdroid.index(repo, ctx(%{path: ["nope.apk"]}))
end
test "404s the root with something actionable", %{repo: repo} do
assert {:error, {404, message}} = Fdroid.index(repo, ctx(%{path: []}))
assert message =~ "add this URL as a repository"
end
end
describe "app links and description" do
setup %{repo: repo} do
{:ok, described} =
GitGud.Repositories.update_repository(repo, %{
"description" =>
"A GitGud client for Android, with a CI dashboard you can watch a build from, " <>
"issues you can triage, and pull requests you can merge from a bus stop."
})
assert {:ok, _, _} = publish(described, build_apk())
%{repo: described}
end
test "index-v2 carries the repository's description", %{repo: repo} do
metadata = v2_metadata(repo)
assert metadata["description"]["en-US"] =~ "A GitGud client for Android"
end
test "the summary is short enough for a list", %{repo: repo} do
metadata = v2_metadata(repo)
summary = metadata["summary"]["en-US"]
# F-Droid puts it under the app's name in a list, where the whole
# paragraph doesn't fit.
assert String.length(summary) <= 81
assert String.starts_with?(summary, "A GitGud client for Android")
assert String.ends_with?(summary, "…")
# Cut on a word boundary, not mid-word.
refute summary =~ ~r/\w…$/u
end
test "a description that already fits is the summary", %{user: user} do
{_user, short} = repository_fixture(%{owner: user})
{:ok, short} =
GitGud.Repositories.update_repository(short, %{
"description" => "Watch CI from your phone."
})
assert {:ok, _, _} = publish(short, build_apk())
metadata = v2_metadata(short)
assert metadata["summary"]["en-US"] == "Watch CI from your phone."
assert metadata["description"]["en-US"] == "Watch CI from your phone."
end
test "index-v2 links back to the repository the app is published from", %{repo: repo} do
metadata = v2_metadata(repo)
handle =
GitGud.Repositories.Storage.repo_handle(
GitGud.Repo.preload(repo, [:owner, :organization])
)
base = GitGudWeb.Endpoint.url() <> "/r/#{handle}/#{repo.name}"
assert metadata["sourceCode"] == base
assert metadata["issueTracker"] == base <> "/issues"
assert metadata["changelog"] == base <> "/commits/" <> repo.default_branch
assert metadata["authorName"] == handle
assert metadata["authorWebSite"] == GitGudWeb.Endpoint.url() <> "/u/#{handle}"
end
test "the detected licence is published", %{repo: repo} do
# Detection writes it on the repository; the index reports what
# was found and says nothing when nothing was.
{1, _} =
GitGud.Repo.update_all(
from(r in GitGud.Repositories.Repository, where: r.id == ^repo.id),
set: [license: "AGPL-3.0"]
)
# Reloaded: the index reports what the repository row says, and
# the struct this test has been holding predates the write.
assert v2_metadata(GitGud.Repo.get!(GitGud.Repositories.Repository, repo.id))["license"] ==
"AGPL-3.0"
end
test "an undetected licence is absent from v2 and Unknown in v1", %{user: user} do
{_user, unlicensed} = repository_fixture(%{owner: user})
assert {:ok, _, _} = publish(unlicensed, build_apk())
refute Map.has_key?(v2_metadata(unlicensed), "license")
{:ok, _type, json} = Fdroid.index(unlicensed, ctx(%{path: ["index-v1.json"]}))
[app] = Jason.decode!(json)["apps"]
# v1 has no way to say "we don't know".
assert app["license"] == "Unknown"
end
test "index-v1 carries the same links, unlocalized", %{repo: repo} do
{:ok, _type, json} = Fdroid.index(repo, ctx(%{path: ["index-v1.json"]}))
[app] = Jason.decode!(json)["apps"]
assert app["description"] =~ "A GitGud client for Android"
assert app["summary"] =~ "A GitGud client"
assert app["sourceCode"] =~ "/r/"
assert app["issueTracker"] =~ "/issues"
end
defp v2_metadata(repo) do
{:ok, _type, json} = Fdroid.index(repo, ctx(%{path: ["index-v2.json"]}))
Jason.decode!(json)["packages"]["org.neiam.gitgud.app"]["metadata"]
end
end
describe "an explicit description" do
defp publish_with(repo, headers) do
conn =
Enum.reduce(headers, Plug.Test.conn(:post, "/upload"), fn {k, v}, conn ->
Plug.Conn.put_req_header(conn, k, v)
end)
Fdroid.store(repo, ctx(%{method: "POST", path: ["upload"], conn: conn}), build_apk())
end
defp app_metadata(repo) do
{:ok, _type, json} = Fdroid.index(repo, ctx(%{path: ["index-v2.json"]}))
Jason.decode!(json)["packages"]["org.neiam.gitgud.app"]["metadata"]
end
setup %{repo: repo} do
# The repository says something different, so it's clear which
# one the index used.
{:ok, repo} =
GitGud.Repositories.update_repository(repo, %{"description" => "The whole forge."})
%{repo: repo}
end
test "wins over the repository's, from the upload's headers", %{repo: repo} do
description = "Issues, pull requests and CI.\n\nFrom your phone."
assert {:ok, _, _} =
publish_with(repo, [
{"x-package-summary", "The forge, in your pocket"},
{"x-package-description", URI.encode(description, &URI.char_unreserved?/1)}
])
metadata = app_metadata(repo)
assert metadata["summary"]["en-US"] == "The forge, in your pocket"
assert metadata["description"]["en-US"] == description
{:ok, _type, json} = Fdroid.index(repo, ctx(%{path: ["index-v1.json"]}))
[app] = Jason.decode!(json)["apps"]
assert app["summary"] == "The forge, in your pocket"
end
test "set on the package later, applies to the published index", %{repo: repo} do
assert {:ok, _, _} = publish(repo, build_apk())
package = Packages.get_package(repo, "fdroid", "org.neiam.gitgud.app")
{:ok, _} = Packages.set_about(package, %{"summary" => "Set on the packages page"})
assert app_metadata(repo)["summary"]["en-US"] == "Set on the packages page"
# No explicit description: the repository's still stands in.
assert app_metadata(repo)["description"]["en-US"] == "The whole forge."
end
test "a summary that fails validation refuses the upload and says why", %{repo: repo} do
assert {:error, {400, message}} =
publish_with(repo, [{"x-package-summary", String.duplicate("x", 200)}])
assert message =~ "X-Package-Summary"
assert Packages.get_package(repo, "fdroid", "org.neiam.gitgud.app") == nil
end
end
describe "a summary that would only repeat the app's name" do
test "is left out", %{repo: repo} do
label = "GitGud"
{:ok, repo} = GitGud.Repositories.update_repository(repo, %{"description" => label})
assert {:ok, _, _} = publish(repo, build_apk())
{:ok, _type, json} = Fdroid.index(repo, ctx(%{path: ["index-v2.json"]}))
metadata = Jason.decode!(json)["packages"]["org.neiam.gitgud.app"]["metadata"]
assert metadata["name"]["en-US"] == label
refute Map.has_key?(metadata, "summary")
end
end
describe "an app whose repository has no description" do
test "omits the description rather than emitting an empty one", %{repo: repo} do
assert {:ok, _, _} = publish(repo, build_apk())
{:ok, _type, json} = Fdroid.index(repo, ctx(%{path: ["index-v2.json"]}))
metadata = Jason.decode!(json)["packages"]["org.neiam.gitgud.app"]["metadata"]
# A key holding null is a key a client has to interpret.
refute Map.has_key?(metadata, "description")
refute Map.has_key?(metadata, "summary")
# The links don't depend on a description and stay.
assert metadata["sourceCode"] =~ "/r/"
end
end
describe "the repository icon" do
setup %{repo: repo} do
{:ok, _, _} = publish(repo, build_apk())
:ok
end
test "is served as a PNG", %{repo: repo} do
assert {:ok, "image/png", png} = Fdroid.index(repo, ctx(%{path: ["icons", "icon.png"]}))
png = IO.iodata_to_binary(png)
assert <<137, 80, 78, 71, 13, 10, 26, 10, _::binary>> = png
# 480x480, from the IHDR that follows the signature.
assert <<_::binary-size(16), 480::32, 480::32, _::binary>> = png
end
test "is the namespace's avatar", %{repo: repo, user: user} do
{:ok, _, png} = Fdroid.index(repo, ctx(%{path: ["icons", "icon.png"]}))
# Through GitGud.Avatars, so it is whatever the profile shows —
# the identicon here, and an upload the moment there is one.
{:ok, _type, expected} = GitGud.Avatars.render(user)
assert IO.iodata_to_binary(png) == expected
end
test "follows an uploaded avatar, extension and all", %{repo: repo, user: user} do
# A 1x1 GIF. The index names the icon by filename, so the
# extension has to follow the bytes — a JPEG served at icon.png
# is a file some clients fetch and refuse to draw.
gif =
<<"GIF89a", 1::little-16, 1::little-16, 0x80, 0, 0, 0, 0, 0, 255, 255, 255, 0x21, 0xF9, 4,
1, 0, 0, 0, 0, 0x2C, 0, 0, 0, 0, 1::little-16, 1::little-16, 0, 2, 2, 0x44, 1, 0, 0x3B>>
{:ok, _} = GitGud.Avatars.put(user, gif)
assert {:ok, "image/gif", ^gif} = Fdroid.index(repo, ctx(%{path: ["icons", "icon.gif"]}))
assert {:error, {404, _}} = Fdroid.index(repo, ctx(%{path: ["icons", "icon.png"]}))
index = decode(Fdroid.index(repo, ctx(%{path: ["index-v2.json"]})))
assert index["repo"]["icon"]["en-US"]["name"] == "/icons/icon.gif"
end
test "index-v2 pins it by hash and size", %{repo: repo} do
index = decode(Fdroid.index(repo, ctx(%{path: ["index-v2.json"]})))
{:ok, _, png} = Fdroid.index(repo, ctx(%{path: ["icons", "icon.png"]}))
png = IO.iodata_to_binary(png)
icon = index["repo"]["icon"]["en-US"]
assert icon["name"] == "/icons/icon.png"
assert icon["size"] == byte_size(png)
assert icon["sha256"] == :sha256 |> :crypto.hash(png) |> Base.encode16(case: :lower)
end
test "index-v1 names it as a bare filename", %{repo: repo} do
# v1 resolves the icon against <address>/icons/, so it carries the
# filename rather than a path.
index = decode(Fdroid.index(repo, ctx(%{path: ["index-v1.json"]})))
assert index["repo"]["icon"] == "icon.png"
end
test "404s an icon that isn't the one", %{repo: repo} do
assert {:error, {404, _}} = Fdroid.index(repo, ctx(%{path: ["icons", "nope.png"]}))
end
end
describe "app icons" do
setup %{repo: repo} do
{:ok, _, _} = publish(repo, build_apk())
:ok
end
test "the launcher icon is served under its package and versionCode", %{repo: repo} do
assert {:ok, "image/png", @icon_png} =
Fdroid.index(repo, ctx(%{path: ["icons", "org.neiam.gitgud.app.1.png"]}))
end
test "picks the highest density, and never an adaptive layer", %{repo: repo} do
{:ok, _, png} = Fdroid.index(repo, ctx(%{path: ["icons", "org.neiam.gitgud.app.1.png"]}))
# ic_launcher_foreground is a glyph on transparency; shipping it
# as the icon looks like a bug rather than a design.
assert png == @icon_png
refute png == "a layer, not the icon"
refute png == "low density, should lose"
end
test "index-v2 pins it by hash and size", %{repo: repo} do
index = decode(Fdroid.index(repo, ctx(%{path: ["index-v2.json"]})))
icon = index["packages"]["org.neiam.gitgud.app"]["metadata"]["icon"]["en-US"]
assert icon["name"] == "/icons/org.neiam.gitgud.app.1.png"
assert icon["size"] == byte_size(@icon_png)
assert icon["sha256"] == :sha256 |> :crypto.hash(@icon_png) |> Base.encode16(case: :lower)
end
test "the app is named by its label, not its package name", %{repo: repo} do
{:ok, _type, json} = Fdroid.index(repo, ctx(%{path: ["index-v2.json"]}))
index = Jason.decode!(json)
metadata = index["packages"]["org.neiam.gitgud.app"]["metadata"]
assert metadata["name"]["en-US"] == "GitGud"
end
test "index-v1 carries the same name", %{repo: repo} do
{:ok, _type, json} = Fdroid.index(repo, ctx(%{path: ["index-v1.json"]}))
index = Jason.decode!(json)
assert [app] = index["apps"]
assert app["name"] == "GitGud"
assert app["packageName"] == "org.neiam.gitgud.app"
end
test "index-v1 names it relative to /icons/", %{repo: repo} do
index = decode(Fdroid.index(repo, ctx(%{path: ["index-v1.json"]})))
assert [app] = index["apps"]
assert app["icon"] == "org.neiam.gitgud.app.1.png"
end
test "an APK with no icon publishes fine and claims none", %{repo: repo} do
{:ok, other} =
GitGud.Repositories.create_repository(
GitGud.Repo.preload(repo, :owner).owner,
%{"name" => "iconless", "visibility" => "public"}
)
# No res/mipmap entries at all, and no resource table either.
bare =
GitGud.ApkFixtures.signed_zip([
{~c"AndroidManifest.xml", GitGud.ApkFixtures.compiled_manifest()}
])
assert {:ok, _, _} = publish(other, bare)
index = decode(Fdroid.index(other, ctx(%{path: ["index-v2.json"]})))
# Absent rather than null: an icon that isn't there shouldn't
# show up as a key a client has to interpret.
refute Map.has_key?(index["packages"]["org.neiam.gitgud.app"]["metadata"], "icon")
end
end
describe "the /repo suffix F-Droid insists on" do
setup %{repo: repo} do
{:ok, _, _} = publish(repo, build_apk())
:ok
end
test "serves the paths the client actually requests", %{repo: repo} do
# RepoUriGetter appends /fdroid/repo to any address not already
# ending in /repo or /archive, so these are the real request
# paths — not a courtesy.
for path <- [["entry.jar"], ["repo", "entry.jar"], ["fdroid", "repo", "entry.jar"]] do
assert {:ok, "application/java-archive", _} =
Fdroid.index(repo, ctx(%{path: path})),
"#{Enum.join(path, "/")} should serve entry.jar"
end
end
test "advertises an address the client won't rewrite", %{repo: repo} do
index = decode(Fdroid.index(repo, ctx(%{path: ["index-v2.json"]})))
# Ends in /repo, so `pathSegments.last() == "repo"` and the
# client uses it as given instead of appending /fdroid/repo.
assert String.ends_with?(index["repo"]["address"], "/repo")
end
test "index-v1 advertises the same address", %{repo: repo} do
index = decode(Fdroid.index(repo, ctx(%{path: ["index-v1.json"]})))
assert String.ends_with?(index["repo"]["address"], "/repo")
end
test "APKs are reachable under the prefix too", %{repo: repo} do
# The index points at /<file>.apk relative to an address that now
# ends in /repo, so this is the path the download uses.
assert {:blob, _} =
Fdroid.index(repo, ctx(%{path: ["repo", "org.neiam.gitgud.app_1.apk"]}))
end
end
describe "_all" do
test "aggregates every repository in the namespace", %{user: user, repo: repo} do
{:ok, other} =
GitGud.Repositories.create_repository(user, %{
"name" => "second-app",
"visibility" => "public"
})
{:ok, _, _} = publish(repo, build_apk())
{:ok, _, _} = publish(other, build_apk([{~c"extra.txt", "makes this a different apk"}]))
assert %{"packages" => packages} =
decode(
Fdroid.namespace_index(user, [repo, other], ctx(%{path: ["index-v2.json"]}))
)
# Same package name from two repositories: one app entry, two
# versions keyed by their own digests.
assert %{"org.neiam.gitgud.app" => app} = packages
assert map_size(app["versions"]) == 2
end
end
defp decode_raw({:ok, _type, body}), do: IO.iodata_to_binary(body)
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
26.7 KiB · text
History
b868907