4.1 KiB · text 5af55d9
defmodule GitGudWeb.PackagesWebhookTest do
use GitGudWeb.ConnCase, async: false
alias GitGud.Packages
alias GitGud.Repositories.Storage
import GitGud.ForgeFixtures
setup do
{user, repo} = repository_fixture()
owner = Storage.owner_handle(user)
{:ok, user: user, repo: repo, owner: owner}
end
defp push_payload(owner, repo_name, image_path, version, opts \\ []) do
%{
"event" => "PUSH",
"repository" => Enum.join([owner, repo_name, image_path], "/"),
"reference" => version,
"digest" =>
Keyword.get(opts, :digest, "sha256:" <> String.duplicate("a", 64)),
"size" => Keyword.get(opts, :size, 12_345),
"mediaType" =>
Keyword.get(opts, :media_type, "application/vnd.oci.image.manifest.v1+json"),
"timestamp" => "2026-05-13T12:00:00Z"
}
end
describe "POST /_webhooks/zot" do
test "PUSH event creates package + version", %{conn: conn, owner: owner, repo: repo} do
conn = post(conn, ~p"/_webhooks/zot", push_payload(owner, repo.name, "web", "v1.0.0"))
assert conn.status == 204
assert [pkg] = Packages.list_packages(repo)
assert pkg.kind == "container"
assert pkg.name == "web"
assert pkg.latest_version == "v1.0.0"
[version] = Packages.list_versions(pkg)
assert version.version == "v1.0.0"
assert version.size == 12_345
end
test "PUSH for nested image path treats the trailing segments as the name", %{
conn: conn,
owner: owner,
repo: repo
} do
conn =
post(
conn,
~p"/_webhooks/zot",
push_payload(owner, repo.name, "site/db", "latest")
)
assert conn.status == 204
[pkg] = Packages.list_packages(repo)
assert pkg.name == "site/db"
end
test "re-pushing the same tag updates the row in place", %{
conn: conn,
owner: owner,
repo: repo
} do
payload =
push_payload(owner, repo.name, "web", "v1.0.0",
digest: "sha256:" <> String.duplicate("a", 64)
)
post(conn, ~p"/_webhooks/zot", payload)
updated =
push_payload(owner, repo.name, "web", "v1.0.0",
digest: "sha256:" <> String.duplicate("b", 64)
)
post(conn, ~p"/_webhooks/zot", updated)
[pkg] = Packages.list_packages(repo)
[version] = Packages.list_versions(pkg)
assert String.starts_with?(version.digest, "sha256:bbb")
end
test "DELETE event prunes the version", %{conn: conn, owner: owner, repo: repo} do
post(conn, ~p"/_webhooks/zot", push_payload(owner, repo.name, "web", "v1.0.0"))
post(conn, ~p"/_webhooks/zot", push_payload(owner, repo.name, "web", "v1.1.0"))
[pkg] = Packages.list_packages(repo)
assert length(Packages.list_versions(pkg)) == 2
delete_payload = %{
"event" => "DELETE",
"repository" => "#{owner}/#{repo.name}/web",
"reference" => "v1.0.0"
}
post(conn, ~p"/_webhooks/zot", delete_payload)
[_pkg] = Packages.list_packages(repo)
versions = Packages.list_versions(pkg)
assert length(versions) == 1
assert hd(versions).version == "v1.1.0"
end
test "unknown repo path returns 422", %{conn: conn} do
conn =
post(
conn,
~p"/_webhooks/zot",
push_payload("ghost", "missing-repo", "img", "v1")
)
assert conn.status == 422
end
test "respects shared_secret when configured", %{conn: conn, owner: owner, repo: repo} do
Application.put_env(:git_gud, GitGudWeb.PackagesWebhookController,
shared_secret: "topsecret"
)
on_exit(fn ->
Application.delete_env(:git_gud, GitGudWeb.PackagesWebhookController)
end)
payload = push_payload(owner, repo.name, "web", "v1.0.0")
# Without the bearer
conn1 = post(conn, ~p"/_webhooks/zot", payload)
assert conn1.status == 401
# With it
conn2 =
conn
|> put_req_header("authorization", "Bearer topsecret")
|> post(~p"/_webhooks/zot", payload)
assert conn2.status == 204
end
end
end