package xfer
import (
"bytes"
"io"
"log"
"net"
"net/http"
"net/http/httptest"
"os/exec"
"strconv"
"strings"
"testing"
)
// childPort is the stub server's port, so the Fronter relays to it.
func childPort(t *testing.T, srv *httptest.Server) int {
t.Helper()
_, port, err := net.SplitHostPort(strings.TrimPrefix(srv.URL, "http://"))
if err != nil {
t.Fatal(err)
}
n, err := strconv.Atoi(port)
if err != nil {
t.Fatal(err)
}
return n
}
// A push the child refuses must say why in the forge log.
//
// The operator could see THAT a push failed and never WHY. Giving the
// child a voice assumed the reason travelled on stderr; it does not.
// `zit serve` reports a rejected card on the WIRE, as an error card,
// and says nothing on stderr — so a push that failed repeatedly left
// the log holding only the child's startup banner.
func TestARefusedPushSaysWhyInTheLog(t *testing.T) {
// A child that refuses the way zit does: an error card, nothing on
// stderr.
child := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(io.Discard, r.Body)
w.Header().Set("Content-Type", "application/x-fossil")
w.Write(errorReply("malformed file line"))
}))
defer child.Close()
var logged bytes.Buffer
old := log.Writer()
log.SetOutput(&logged)
defer log.SetOutput(old)
body, code := pushThrough(t, child)
if !strings.Contains(logged.String(), "malformed file line") {
t.Errorf("FAIL: the refusal is not in the log; the operator still cannot answer why.\nlog: %s",
logged.String())
}
if !strings.Contains(logged.String(), "alpha") {
t.Error("FAIL: the log line does not name the repository")
}
// And the client still gets the reply, byte for byte.
if code != http.StatusOK {
t.Errorf("FAIL: status %d", code)
}
if got := firstError(decodeContainer(body)); got != "malformed file line" {
t.Errorf("FAIL: the client's own error card changed: %q", got)
}
}
// A served clone is not an error message and must not be buffered or
// scanned: it streams through untouched.
func TestALargeReplyIsNotBufferedLookingForARefusal(t *testing.T) {
big := bytes.Repeat([]byte("x"), maxRefusalPeek*2)
child := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(io.Discard, r.Body)
w.Write(big)
}))
defer child.Close()
body, code := pushThrough(t, child)
if code != http.StatusOK {
t.Fatalf("FAIL: status %d", code)
}
if len(body) != len(big) {
t.Errorf("FAIL: the relay delivered %d bytes of %d — a large reply was truncated",
len(body), len(big))
}
if !bytes.Equal(body, big) {
t.Error("FAIL: the relayed bytes differ from what the child sent")
}
}
// pushThrough drives one push request through a Fronter pointed at the
// given child, and returns the body the client received.
func pushThrough(t *testing.T, child *httptest.Server) ([]byte, int) {
t.Helper()
f := New()
defer f.Close()
f.Register("alpha", t.TempDir())
// Point the backend at the stub instead of starting a real child.
// A non-nil cmd with no ProcessState makes ensure() dial rather
// than spawn; the dial succeeds against the stub, so no real
// `zit serve` is started and Process stays nil for stop().
f.mu.Lock()
f.repos["alpha"].port = childPort(t, child)
f.repos["alpha"].cmd = &exec.Cmd{}
f.mu.Unlock()
mux := http.NewServeMux()
mux.Handle("POST /r/{repo}", f)
srv := httptest.NewServer(mux)
defer srv.Close()
resp, err := http.Post(srv.URL+"/r/alpha", "application/x-fossil",
bytes.NewReader(encodeContainer([]byte(
"login a b c\npush aaaaaaaaaaaa bbbbbbbbbbbb\nfile x\n"))))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
got, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
return got, resp.StatusCode
}