serve_test.go
package main
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"vitrine/display"
)
// fixtureServer builds a server over the standard hermetic fixture repo,
// with a pinned identity so output stays deterministic.
func fixtureServer(t *testing.T) *server {
t.Helper()
return newServer(map[string]*repo{"fixturama": fixtureRepo(t)}, "", testFace(t))
}
// testFace is the pinned identity every test server wears: sitename (and thus seed) "test",
// so page chrome and sketch assets stay deterministic regardless of the machine.
func testFace(t *testing.T) face {
t.Helper()
fc, err := buildFace(&config{Sitename: "test"}, "")
if err != nil {
t.Fatalf("building the test face: %v", err)
}
return fc
}
// do runs one request through the handler and returns the recorded response.
// A non-nil body makes it a POST (with the given content type); otherwise a GET.
func do(s *server, target string, body []byte, hdr map[string]string) *httptest.ResponseRecorder {
method := http.MethodGet
var rd io.Reader
if body != nil {
method = http.MethodPost
rd = bytes.NewReader(body)
}
req := httptest.NewRequest(method, "http://socle.example"+target, rd)
for k, v := range hdr {
req.Header.Set(k, v)
}
rec := httptest.NewRecorder()
s.ServeHTTP(rec, req)
return rec
}
// fetchBody is a minimal protocol-v0 fetch request: want the tip, no haves, done.
func fetchBody() []byte {
var req bytes.Buffer
pktline(&req, "want "+fixtureShaMain+"\n")
req.WriteString("0000")
pktline(&req, "done\n")
return req.Bytes()
}
func TestDispatchStatuses(t *testing.T) {
t.Parallel()
s := fixtureServer(t)
for _, c := range []struct {
target string
body []byte // non-nil means POST
want int
location string // expected Location header, when a redirect
}{
{target: "/", want: 200},
{target: "/-/style.css", want: 200},
{target: "/fixturama", want: 301, location: "/fixturama/"},
{target: "/fixturama/", want: 200},
{target: "/fixturama.git/", want: 200},
{target: "/nosuch/", want: 404},
{target: "/fixturama/tree/main", want: 200},
{target: "/fixturama/tree/main/", want: 200},
{target: "/fixturama/tree/main/docs/", want: 200},
{target: "/fixturama/tree/main/docs/with%20space.txt", want: 200},
{target: "/fixturama/tree/bogus/", want: 404},
{target: "/fixturama/tree/main..dev/", want: 400},
{target: "/fixturama/tree/main/absent", want: 404},
{target: "/fixturama/raw/main/README.md", want: 200},
{target: "/fixturama/raw/main/docs", want: 404}, // a directory has no raw bytes
{target: "/fixturama/log/main", want: 200},
{target: "/fixturama/log/bogus", want: 404},
{target: "/fixturama/info/refs", want: 403},
{target: "/fixturama/info/refs?service=git-receive-pack", want: 403},
{target: "/fixturama/info/refs?service=git-upload-pack", want: 200},
{target: "/fixturama/git-upload-pack", body: fetchBody(), want: 200},
{target: "/fixturama/git-upload-pack", want: 405}, // fetch RPC is POST-only
{target: "/fixturama/tree/main/", body: []byte{}, want: 405}, // and nothing else accepts POST
{target: "/fixturama/README.md", want: 302, location: "/fixturama/tree/main/README.md"},
{target: "/fixturama/docs/pic.png", want: 302, location: "/fixturama/raw/main/docs/pic.png"},
} {
rec := do(s, c.target, c.body, nil)
if rec.Code != c.want {
t.Errorf("%s: status = %d, want %d (body: %.120s)", c.target, rec.Code, c.want, rec.Body.String())
}
if c.location != "" && rec.Header().Get("Location") != c.location {
t.Errorf("%s: Location = %q, want %q", c.target, rec.Header().Get("Location"), c.location)
}
}
}
func TestCacheHeaders(t *testing.T) {
t.Parallel()
s := fixtureServer(t)
const immutable = "public, max-age=31536000, immutable"
for _, c := range []struct {
target string
want string
}{
{"/fixturama/tree/" + fixtureShaMain + "/README.md", immutable},
{"/fixturama/raw/" + fixtureShaMain + "/README.md", immutable},
{"/fixturama/log/" + fixtureShaMain, immutable},
{"/fixturama/commit/" + fixtureShaMain, immutable},
{"/fixturama/commit/main", ""}, // same page by branch name: the name moves
{"/fixturama/tree/main/README.md", ""}, // a branch moves; never immutable
{"/fixturama/raw/v1.0/README.md", ""}, // a tag names a commit but is not one
{"/fixturama/info/refs?service=git-upload-pack", "no-cache"},
} {
rec := do(s, c.target, nil, nil)
if rec.Code != 200 {
t.Errorf("%s: status = %d, want 200", c.target, rec.Code)
}
if got := rec.Header().Get("Cache-Control"); got != c.want {
t.Errorf("%s: Cache-Control = %q, want %q", c.target, got, c.want)
}
}
}
func TestRawResponse(t *testing.T) {
t.Parallel()
s := fixtureServer(t)
rec := do(s, "/fixturama/raw/main/README.md", nil, nil)
if rec.Body.String() != fixtureReadme {
t.Errorf("raw body = %q, want the README", rec.Body.String())
}
if got := rec.Header().Get("Content-Type"); got != "text/plain; charset=utf-8" {
t.Errorf("Content-Type = %q, want text/plain (markdown must display, not download)", got)
}
if got, want := rec.Header().Get("Content-Length"), fmt.Sprint(len(fixtureReadme)); got != want {
t.Errorf("Content-Length = %q, want %s", got, want)
}
}
func TestInfoRefsProtocolVariants(t *testing.T) {
t.Parallel()
s := fixtureServer(t)
t.Run("v0 advertisement carries the service preamble and refs", func(t *testing.T) {
rec := do(s, "/fixturama/info/refs?service=git-upload-pack", nil, nil)
body := rec.Body.String()
if !strings.HasPrefix(body, "001e# service=git-upload-pack\n0000") {
t.Errorf("v0 advertisement should start with the service pkt-line, got %.60q", body)
}
if !strings.Contains(body, "refs/heads/main") || !strings.Contains(body, fixtureShaMain) {
t.Errorf("advertisement lacks main's ref line:\n%s", body)
}
if got := rec.Header().Get("Content-Type"); got != "application/x-git-upload-pack-advertisement" {
t.Errorf("Content-Type = %q", got)
}
})
t.Run("v2 advertisement has no preamble", func(t *testing.T) {
rec := do(s, "/fixturama/info/refs?service=git-upload-pack", nil, map[string]string{"Git-Protocol": "version=2"})
body := rec.Body.String()
if strings.Contains(body, "# service=") {
t.Errorf("v2 advertisement must not carry the v0 preamble, got %.60q", body)
}
if !strings.Contains(body, "version 2") {
t.Errorf("expected a v2 capability advertisement:\n%s", body)
}
})
}
func TestUploadPackRPC(t *testing.T) {
t.Parallel()
s := fixtureServer(t)
check := func(t *testing.T, rec *httptest.ResponseRecorder) {
t.Helper()
if rec.Code != 200 {
t.Fatalf("status = %d", rec.Code)
}
if !bytes.Contains(rec.Body.Bytes(), []byte("NAK")) || !bytes.Contains(rec.Body.Bytes(), []byte("PACK")) {
t.Errorf("expected NAK and a packfile in the response, got %d bytes", rec.Body.Len())
}
}
t.Run("plain body", func(t *testing.T) {
check(t, do(s, "/fixturama/git-upload-pack", fetchBody(), nil))
})
t.Run("gzip body", func(t *testing.T) {
var zbuf bytes.Buffer
zw := gzip.NewWriter(&zbuf)
zw.Write(fetchBody())
zw.Close()
check(t, do(s, "/fixturama/git-upload-pack", zbuf.Bytes(), map[string]string{"Content-Encoding": "gzip"}))
})
t.Run("malformed gzip is a clean 400", func(t *testing.T) {
rec := do(s, "/fixturama/git-upload-pack", []byte("not gzip"), map[string]string{"Content-Encoding": "gzip"})
if rec.Code != 400 {
t.Errorf("status = %d, want 400", rec.Code)
}
})
}
// TestFileViewModes covers the file-view decision ladder with blobs the standard
// fixture doesn't carry: an image, binary data, and an oversized text file.
// These shas are deliberately not pinned; nothing here snapshots output.
func TestFileViewModes(t *testing.T) {
t.Parallel()
dir := t.TempDir()
gitFixture(t, dir, "init", "-q", "-b", "main", "--object-format=sha1", ".")
writeFixtureFile(t, dir, "logo.png", "\x89PNG\r\n\x1a\n not actually a decodable image, which is fine: only the extension matters")
writeFixtureFile(t, dir, "data.bin", "binary\x00data")
writeFixtureFile(t, dir, "huge.txt", strings.Repeat("a", display.MaxRenderSize+1))
gitFixture(t, dir, "add", "-A")
gitFixture(t, dir, "commit", "-q", "-m", "media sampler")
rp, err := openRepo("media", repoConfig{Path: dir})
if err != nil {
t.Fatal(err)
}
s := newServer(map[string]*repo{"media": rp}, "", testFace(t))
for _, c := range []struct {
target string
want string
}{
{"/media/tree/main/logo.png", `<img class="file" src="/media/raw/main/logo.png"`},
{"/media/tree/main/data.bin", `binary data; <a href="/media/raw/main/data.bin">view raw</a>`},
{"/media/tree/main/huge.txt", `too large to display inline; <a href="/media/raw/main/huge.txt">view raw</a>`},
} {
rec := do(s, c.target, nil, nil)
if rec.Code != 200 {
t.Errorf("%s: status = %d", c.target, rec.Code)
}
if !strings.Contains(rec.Body.String(), c.want) {
t.Errorf("%s: body lacks %q:\n%.400s", c.target, c.want, rec.Body.String())
}
}
}
// TestCommitPageShapes covers the commit-page shapes the standard fixture doesn't carry:
// a merge commit (two parent links; the diff is taken against the first parent, so the
// merged-in edit shows with both its removed and added lines) and a multi-line message
// (the part past the subject renders in its own block).
// These shas are deliberately not pinned; nothing here snapshots output.
func TestCommitPageShapes(t *testing.T) {
t.Parallel()
dir := t.TempDir()
gitFixture(t, dir, "init", "-q", "-b", "main", "--object-format=sha1", ".")
writeFixtureFile(t, dir, "a.txt", "original line\n")
gitFixture(t, dir, "add", "-A")
gitFixture(t, dir, "commit", "-q", "-m", "base")
gitFixture(t, dir, "checkout", "-q", "-b", "side")
writeFixtureFile(t, dir, "a.txt", "revised line\n")
gitFixture(t, dir, "add", "-A")
gitFixture(t, dir, "commit", "-q", "-m", "revise a")
gitFixture(t, dir, "checkout", "-q", "main")
writeFixtureFile(t, dir, "b.txt", "unrelated\n")
gitFixture(t, dir, "add", "-A")
gitFixture(t, dir, "commit", "-q", "-m", "grow b")
gitFixture(t, dir, "merge", "-q", "--no-ff", "-m", "merge the side branch\n\nBecause the revision was worth having.", "side")
rp, err := openRepo("merger", repoConfig{Path: dir})
if err != nil {
t.Fatal(err)
}
s := newServer(map[string]*repo{"merger": rp}, "", testFace(t))
rec := do(s, "/merger/commit/main", nil, nil)
if rec.Code != 200 {
t.Fatalf("status = %d", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{
"<h1>merge the side branch</h1>",
"Because the revision was worth having.",
`<span class="d-del">-original line</span>`,
`<span class="d-add">+revised line</span>`,
} {
if !strings.Contains(body, want) {
t.Errorf("commit page lacks %q", want)
}
}
if got := strings.Count(body, "parent <a href="); got != 2 {
t.Errorf("merge commit shows %d parent links, want 2", got)
}
}
// TestGoGetMeta covers the vanity-import answers: with a configured baseurl, any GET
// under a repo carrying ?go-get=1 gets the meta-tag page (exact content asserted,
// since the go toolchain parses it); without one, the probe is inert -- fail closed,
// so socle never claims an import path for a domain nobody chose.
func TestGoGetMeta(t *testing.T) {
t.Parallel()
wantImport := `<meta name="go-import" content="code.example/fixturama git https://code.example/fixturama">`
wantSource := `<meta name="go-source" content="code.example/fixturama https://code.example/fixturama/ https://code.example/fixturama/tree/HEAD{/dir} https://code.example/fixturama/tree/HEAD{/dir}/{file}">`
s := newServer(map[string]*repo{"fixturama": fixtureRepo(t)}, "https://code.example", testFace(t))
for _, target := range []string{
"/fixturama?go-get=1", // the slashless home: no redirect detour (it would drop the query)
"/fixturama/?go-get=1", // the home itself
"/fixturama/sub/pkg?go-get=1", // go queries the full package path, not just the module root
"/fixturama/tree/main?go-get=1", // even paths that mean something else to socle answer the probe
} {
rec := do(s, target, nil, nil)
if rec.Code != 200 {
t.Fatalf("GET %s: status %d, want 200", target, rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, wantImport) {
t.Errorf("GET %s: body lacks the go-import tag\nwant: %s\ngot body:\n%s", target, wantImport, body)
}
if !strings.Contains(body, wantSource) {
t.Errorf("GET %s: body lacks the go-source tag\nwant: %s", target, wantSource)
}
}
// With a baseurl, the calling-card metas carry the canonical og:url.
if rec := do(s, "/fixturama/", nil, nil); !strings.Contains(rec.Body.String(), `<meta property="og:url" content="https://code.example/fixturama/">`) {
t.Errorf("repo home lacks the canonical og:url meta")
}
// The configured address also becomes the displayed clone URL.
if body := do(s, "/fixturama/", nil, nil).Body.String(); !strings.Contains(body, "git clone https://code.example/fixturama") {
t.Errorf("home page clone line does not use the configured baseurl")
}
// Without a baseurl: the probe must change nothing.
bare := fixtureServer(t)
if rec := do(bare, "/fixturama?go-get=1", nil, nil); rec.Code != 301 {
t.Errorf("no baseurl: slashless home with go-get=1: status %d, want the usual 301", rec.Code)
}
rec := do(bare, "/fixturama/?go-get=1", nil, nil)
if rec.Code != 200 {
t.Fatalf("no baseurl: home with go-get=1: status %d, want 200", rec.Code)
}
if body := rec.Body.String(); strings.Contains(body, "go-import") {
t.Errorf("no baseurl: home page must not carry a go-import tag")
}
}
// TestFaceIdentityResolution pins how the displayed identity and the theme seed resolve:
// an explicit sitename wins, a baseurl's host label is the fallback,
// and -theme-seed decouples the color without touching the name.
func TestFaceIdentityResolution(t *testing.T) {
t.Parallel()
for _, c := range []struct {
name string
cfg config
override string
wantName string
wantTheme string
}{
{name: "sitename wins over baseurl", cfg: config{Sitename: "parnassus", BaseURL: "https://code.example"}, wantName: "parnassus", wantTheme: "parnassus"},
{name: "baseurl label when no sitename", cfg: config{BaseURL: "https://code.example"}, wantName: "code", wantTheme: "code"},
{name: "seed override recolors but does not rename", cfg: config{Sitename: "parnassus"}, override: "zebra", wantName: "parnassus", wantTheme: "zebra"},
} {
t.Run(c.name, func(t *testing.T) {
fc, err := buildFace(&c.cfg, c.override)
if err != nil {
t.Fatal(err)
}
if fc.siteName != c.wantName {
t.Errorf("siteName = %q, want %q", fc.siteName, c.wantName)
}
if fc.theme.Name != c.wantTheme {
t.Errorf("theme seed = %q, want %q", fc.theme.Name, c.wantTheme)
}
})
}
}
serve_test.go · 14.5 KiB · 245d322a2e