Skip to content
quarry Artifact [5891376fea]
Login

Artifact [5891376fea]

Artifact 5891376fea55a78729022d708a81f2041600d35036b88c2417b596a5299f0800:


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
}