package githttp
import ( "bytes" "compress/gzip" "errors" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing"
"github.com/go-git/go-git/v5/storage/memory"
)
func TestInfoRefsBanner(t *testing.T) { rec := httptest.NewRecorder() if err := InfoRefs(rec, memory.NewStorage(), UploadPack); err != nil { t.Fatalf("InfoRefs: %v", err) } body := rec.Body.String() // pkt-line: 4-hex length ("001e") + "# service=git-upload-pack\n", then a // flush-pkt "0000". const want = "001e# service=git-upload-pack\n0000" if !strings.HasPrefix(body, want) { t.Fatalf("advertisement prefix = %q, want prefix %q", body[:min(len(body), 40)], want) } if ct := rec.Header().Get("Content-Type"); ct != "application/x-git-upload-pack-advertisement" { t.Errorf("Content-Type = %q", ct) } if cc := rec.Header().Get("Cache-Control"); cc != "no-cache" { t.Errorf("Cache-Control = %q", cc) } }
func TestInfoRefsReceivePackCapabilities(t *testing.T) { rec := httptest.NewRecorder() if err := InfoRefs(rec, memory.NewStorage(), ReceivePack); err != nil { t.Fatalf("InfoRefs: %v", err) } body := rec.Body.String() if !strings.HasPrefix(body, "001f# service=git-receive-pack\n0000") { t.Fatalf("receive-pack banner wrong: %q", body[:min(len(body), 40)]) } // The advertisement must offer the capabilities stock git needs to push and // delete branches, and to receive a status report. for _, cap := range []string{"report-status", "delete-refs"} { if !strings.Contains(body, cap) { t.Errorf("advertisement missing capability %q", cap) } } }
func TestRequestBodyGzip(t *testing.T) { var gz bytes.Buffer zw := gzip.NewWriter(&gz) const payload = "0009done\n" if _, err := zw.Write([]byte(payload)); err != nil { t.Fatal(err) } zw.Close()
r := httptest.NewRequest("POST", "/x/git-upload-pack", bytes.NewReader(gz.Bytes()))
r.Header.Set("Content-Encoding", "gzip")
body, err := requestBody(r)
if err != nil {
t.Fatalf("requestBody: %v", err)
}
defer body.Close()
got, err := io.ReadAll(body)
if err != nil {
t.Fatalf("read: %v", err)
}
if string(got) != payload {
t.Fatalf("gunzipped body = %q, want %q", got, payload)
}
}
// receivePackBody is a minimal create-branch request followed by payload. func receivePackBody(payload []byte) []byte { const ( oldOID = "0000000000000000000000000000000000000000" newOID = "1111111111111111111111111111111111111111" ) cmd := fmt.Sprintf("%s %s refs/heads/master\x00report-status\n", oldOID, newOID) var body bytes.Buffer fmt.Fprintf(&body, "%04x%s", len(cmd)+4, cmd) body.WriteString("0000") body.Write(payload) return body.Bytes() }
func TestDecodeReceivePackProbe(t *testing.T) { r := httptest.NewRequest("POST", "/git-receive-pack", strings.NewReader("0000")) if _, err := DecodeReceivePack(r); !errors.Is(err, ErrProbe) { t.Fatalf("probe: err = %v, want ErrProbe", err) }
// A real request still decodes, with its first pkt-line intact after the
// 4-byte peek.
r = httptest.NewRequest("POST", "/git-receive-pack", bytes.NewReader(receivePackBody([]byte("PACKDATA"))))
req, err := DecodeReceivePack(r)
if err != nil {
t.Fatalf("real request: %v", err)
}
if len(req.Commands) != 1 || req.Commands[0].Name != "refs/heads/master" {
t.Fatalf("commands = %+v", req.Commands)
}
rest, _ := io.ReadAll(req.Packfile)
if string(rest) != "PACKDATA" {
t.Errorf("pack stream = %q", rest)
}
// Empty and truncated bodies are errors, not probes.
for _, body := range []string{"", "00"} {
r = httptest.NewRequest("POST", "/git-receive-pack", strings.NewReader(body))
if _, err := DecodeReceivePack(r); err == nil || errors.Is(err, ErrProbe) {
t.Errorf("body %q: err = %v", body, err)
}
}
}
func TestDecodeReceivePackProbeGzip(t *testing.T) { var gz bytes.Buffer zw := gzip.NewWriter(&gz) zw.Write([]byte("0000")) zw.Close() r := httptest.NewRequest("POST", "/git-receive-pack", &gz) r.Header.Set("Content-Encoding", "gzip") if _, err := DecodeReceivePack(r); !errors.Is(err, ErrProbe) { t.Fatalf("gzipped probe: err = %v, want ErrProbe", err) } }
// TestDecodeReceivePackChunked drives a chunked body through a real // http.Server, which is what git sends once a request exceeds // http.postBuffer (default 1 MiB). Kept from docs/FR-large-push-400.md. func TestDecodeReceivePackChunked(t *testing.T) { var pack bytes.Buffer pack.WriteString("PACK") pack.Write([]byte{0, 0, 0, 2, 0, 0, 0, 0}) pack.Write(bytes.Repeat([]byte{0xAB}, 6<<20)) // past postBuffer pack.Write(make([]byte, 20)) body := receivePackBody(pack.Bytes())
errCh := make(chan error, 1)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ContentLength != -1 {
t.Errorf("ContentLength = %d, want -1 (chunked)", r.ContentLength)
}
req, err := DecodeReceivePack(r)
if err == nil && req.Packfile != nil {
_, err = io.Copy(io.Discard, req.Packfile)
}
errCh <- err
}))
defer srv.Close()
hreq, _ := http.NewRequest(http.MethodPost, srv.URL+"/git-receive-pack", io.NopCloser(bytes.NewReader(body)))
hreq.ContentLength = -1 // force chunked
resp, err := http.DefaultClient.Do(hreq)
if err != nil {
t.Fatalf("request failed: %v", err)
}
resp.Body.Close()
if err := <-errCh; err != nil {
t.Fatalf("chunked body rejected: %v", err)
}
}