vitrine

forward_test.go

package main

import (
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// echoBackend is a test backend that reports the path and query it saw,
// so tests can verify prefix stripping and query pass-through in one read.
func echoBackend(t *testing.T) *httptest.Server {
	t.Helper()
	b := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "path=%s query=%s", r.URL.Path, r.URL.RawQuery)
	}))
	t.Cleanup(b.Close)
	return b
}

// forwardServer builds a vitrine server with the given forwards (and no exhibits).
func forwardServer(t *testing.T, forwards map[string]string) *server {
	t.Helper()
	fwds := make(map[string]forward, len(forwards))
	for name, to := range forwards {
		u, err := parseForwardTarget(to)
		if err != nil {
			t.Fatal(err)
		}
		fwds[name] = forward{To: to, url: u}
	}
	return newServer(&config{Forwards: fwds}, theme{})
}

func TestForwardStripsPrefixKeepsQuery(t *testing.T) {
	b := echoBackend(t)
	srv := forwardServer(t, map[string]string{"game": b.URL})
	rec := get(t, srv, "/game/world/sector?fmt=raw&x=1")
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d", rec.Code)
	}
	if got, want := rec.Body.String(), "path=/world/sector query=fmt=raw&x=1"; got != want {
		t.Errorf("backend saw %q, want %q", got, want)
	}
}

func TestForwardMountRootRedirectsToSlash(t *testing.T) {
	b := echoBackend(t)
	srv := forwardServer(t, map[string]string{"game": b.URL})
	rec := get(t, srv, "/game")
	if rec.Code != http.StatusMovedPermanently || rec.Header().Get("Location") != "/game/" {
		t.Fatalf("status = %d, location = %q; want redirect to /game/", rec.Code, rec.Header().Get("Location"))
	}
	rec = get(t, srv, "/game/")
	if got, want := rec.Body.String(), "path=/ query="; got != want {
		t.Errorf("backend saw %q, want %q", got, want)
	}
}

func TestForwardTargetPathPrepended(t *testing.T) {
	b := echoBackend(t)
	srv := forwardServer(t, map[string]string{"game": b.URL + "/base"})
	rec := get(t, srv, "/game/world")
	if got, want := rec.Body.String(), "path=/base/world query="; got != want {
		t.Errorf("backend saw %q, want %q", got, want)
	}
}

func TestForwardRootCatchesUnclaimed(t *testing.T) {
	b := echoBackend(t)
	srv := forwardServer(t, map[string]string{"": b.URL})
	for url, wantPath := range map[string]string{
		"/":       "/",
		"/world":  "/world",
		"/events": "/events",
		"/app.js": "/app.js",
	} {
		rec := get(t, srv, url)
		if rec.Code != http.StatusOK || rec.Body.String() != "path="+wantPath+" query=" {
			t.Errorf("%s: status = %d, body = %q", url, rec.Code, rec.Body.String())
		}
	}
}

// TestForwardRootYieldsToMounts pins the dispatch precedence:
// exhibits and their ancestor listings stay reachable over a root forward;
// only genuinely unclaimed URL space falls through to it.
func TestForwardRootYieldsToMounts(t *testing.T) {
	dir := t.TempDir()
	if err := os.MkdirAll(filepath.Join(dir, "research"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, "research/notes.md"), []byte("# exhibit wins\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	b := echoBackend(t)
	u, err := parseForwardTarget(b.URL)
	if err != nil {
		t.Fatal(err)
	}
	srv := newServer(&config{
		Exhibits: map[string]exhibit{"meta/research": {Path: filepath.Join(dir, "research")}},
		Forwards: map[string]forward{"": {To: b.URL, url: u}},
	}, theme{})

	// Inside the exhibit: the tree serves, not the backend.
	if body := get(t, srv, "/meta/research/notes.md?fmt=raw").Body.String(); body != "# exhibit wins\n" {
		t.Errorf("exhibit path served %q", body)
	}
	// Strict ancestor of the exhibit: synthetic listing, not the backend.
	if body := get(t, srv, "/meta/").Body.String(); !strings.Contains(body, `href="research/"`) {
		t.Errorf("ancestor listing missing; got %q", body)
	}
	// The root itself: the backend, NOT the root listing.
	if body := get(t, srv, "/").Body.String(); body != "path=/ query=" {
		t.Errorf("root served %q, want the root forward", body)
	}
	// Unclaimed space: the backend.
	if body := get(t, srv, "/world").Body.String(); body != "path=/world query=" {
		t.Errorf("unclaimed path served %q, want the root forward", body)
	}
	// Reserved assets still win over the root forward.
	if ct := get(t, srv, "/-/style.css").Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/css") {
		t.Errorf("/-/style.css content-type = %q, want vitrine's own stylesheet", ct)
	}
}

func TestForwardBackendDownIs502(t *testing.T) {
	// A listener that is opened and closed again yields a port that is
	// known-free right now: connecting to it fails fast.
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	addr := l.Addr().String()
	l.Close()
	srv := forwardServer(t, map[string]string{"game": "http://" + addr})
	rec := get(t, srv, "/game/world")
	if rec.Code != http.StatusBadGateway {
		t.Fatalf("status = %d, want 502", rec.Code)
	}
}

func TestForwardNamesAppearInListings(t *testing.T) {
	b := echoBackend(t)
	srv := forwardServer(t, map[string]string{"svc/game": b.URL})
	if body := get(t, srv, "/").Body.String(); !strings.Contains(body, `href="svc/"`) {
		t.Errorf("root listing missing forward; got %q", body)
	}
	if body := get(t, srv, "/svc/").Body.String(); !strings.Contains(body, `href="game/"`) {
		t.Errorf("svc/ listing missing forward; got %q", body)
	}
}

forward_test.go · 5.3 KiB · 06f4ceb908