forward_test.go
package main
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"vitrine/display"
)
// 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}, display.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}},
}, display.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 · c714b15828