defmodule GitGudWeb.PrereceiveScriptTest do
@moduledoc """
`priv/scripts/gitgud-prereceive`, run for real against a stand-in
backend: a socket that answers whatever status this test says.
What it covers is when `GITGUD_PREHOOK_FAIL_OPEN=1` may let a push
through. Only when the backend can't be reached (or fails with 5xx) —
never on an answer it gave: a 403 from a missing or wrong hook secret
used to read as "unreachable" (`curl -f`), so with fail-open on, a
misconfigured install pushed straight past branch protection.
"""
use ExUnit.Case, async: true
@script Path.expand("../../priv/scripts/gitgud-prereceive", __DIR__)
@zero String.duplicate("0", 40)
@stdin "#{@zero} #{String.duplicate("a", 40)} refs/heads/main\n"
setup do
cond do
System.find_executable("curl") == nil -> {:skip, "curl not on PATH"}
System.find_executable("sh") == nil -> {:skip, "sh not on PATH"}
true -> :ok
end
end
describe "the backend answers" do
test "OK lets the push through" do
url = serve(200, "OK\n\n")
assert {_, 0} = run(url)
end
test "DENY refuses it, with the reason" do
url = serve(200, "DENY\nrefs/heads/main: force-push blocked by branch protection\n")
assert {out, 1} = run(url)
assert out =~ "force-push blocked"
end
test "a 403 refuses it, fail-open or not" do
url = serve(403, "DENY forbidden\n")
assert {out, 1} = run(url, %{"GITGUD_PREHOOK_FAIL_OPEN" => "1"})
assert out =~ "403"
refute out =~ "allowing push"
end
test "a 404 refuses it, fail-open or not" do
url = serve(404, "unknown repo\n")
assert {_, 1} = run(url, %{"GITGUD_PREHOOK_FAIL_OPEN" => "1"})
end
test "a 500 refuses it, unless fail-open is on" do
assert {_, 1} = run(serve(500, "boom"))
assert {out, 0} = run(serve(500, "boom"), %{"GITGUD_PREHOOK_FAIL_OPEN" => "1"})
assert out =~ "WITHOUT branch protection"
end
end
describe "the backend can't be reached" do
test "refuses the push, unless fail-open is on" do
url = closed_url()
assert {out, 1} = run(url)
assert out =~ "unreachable"
assert {_, 0} = run(url, %{"GITGUD_PREHOOK_FAIL_OPEN" => "1"})
end
end
test "with no hook secret it refuses without asking, fail-open or not" do
url = closed_url()
assert {out, 1} = run(url, %{"GITGUD_HOOK_SECRET" => "", "GITGUD_PREHOOK_FAIL_OPEN" => "1"})
assert out =~ "GITGUD_HOOK_SECRET"
end
# ── helpers ─────────────────────────────────────────────────────────
defp run(url, env \\ %{}) do
dir = Path.join(System.tmp_dir!(), "gitgud-prerecv-#{System.unique_integer([:positive])}")
File.mkdir_p!(dir)
on_exit(fn -> File.rm_rf!(dir) end)
stdin = Path.join(dir, "stdin")
File.write!(stdin, @stdin)
env =
Map.merge(
%{
"GITGUD_PREHOOK_URL" => url,
"GITGUD_HOOK_SECRET" => "a-secret-of-some-length",
"GITGUD_PREHOOK_FAIL_OPEN" => "",
"GIT_DIR" => dir
},
env
)
System.cmd("sh", ["-c", ~s(exec "$0" < "$1"), @script, stdin],
env: Enum.to_list(env),
stderr_to_stdout: true
)
end
# A one-shot HTTP server on a free port: reads the request (headers,
# then content-length bytes of body) and answers `status` with `body`.
defp serve(status, body) do
{:ok, listen} = :gen_tcp.listen(0, [:binary, active: false, reuseaddr: true])
{:ok, port} = :inet.port(listen)
pid =
spawn_link(fn ->
{:ok, sock} = :gen_tcp.accept(listen, 10_000)
_ = read_request(sock, "")
:ok =
:gen_tcp.send(
sock,
"HTTP/1.1 #{status} X\r\ncontent-length: #{byte_size(body)}\r\n" <>
"connection: close\r\n\r\n" <> body
)
:gen_tcp.close(sock)
:gen_tcp.close(listen)
end)
on_exit(fn -> Process.exit(pid, :kill) end)
"http://127.0.0.1:#{port}/_hooks/pre-receive"
end
defp read_request(sock, acc) do
case :binary.split(acc, "\r\n\r\n") do
[head, rest] ->
len =
case Regex.run(~r/content-length:\s*(\d+)/i, head) do
[_, n] -> String.to_integer(n)
_ -> 0
end
read_body(sock, rest, len)
[_] ->
{:ok, data} = :gen_tcp.recv(sock, 0, 10_000)
read_request(sock, acc <> data)
end
end
defp read_body(_sock, body, len) when byte_size(body) >= len, do: body
defp read_body(sock, body, len) do
{:ok, data} = :gen_tcp.recv(sock, 0, 10_000)
read_body(sock, body <> data, len)
end
# A port nothing listens on.
defp closed_url do
{:ok, listen} = :gen_tcp.listen(0, [:binary, active: false])
{:ok, port} = :inet.port(listen)
:gen_tcp.close(listen)
"http://127.0.0.1:#{port}/_hooks/pre-receive"
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
4.9 KiB · text
History
b868907