serve_test.go
package main
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
// newTestServer builds a server around a throwaway tree.
func newTestServer(t *testing.T) *server {
t.Helper()
dir := t.TempDir()
files := map[string]string{
"README.md": "# Hello\n\nsome *markdown* & <angle> stuff\n",
"sub/notes.txt": "plain text here\n",
"sub/deeper.md": "nested doc\n",
"dup.md": "## Twice\n\n## Twice\n",
".git/config": "should be hidden\n"}
for name, content := range files {
p := filepath.Join(dir, filepath.FromSlash(name))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
srv, err := newServer(map[string]string{"demo": dir})
if err != nil {
t.Fatal(err)
}
return srv
}
// get performs a request and returns the response.
func get(t *testing.T, srv *server, url string) *httptest.ResponseRecorder {
t.Helper()
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, httptest.NewRequest("GET", url, nil))
return rec
}
func TestRawMarkdownIsPlaintextPassthrough(t *testing.T) {
rec := get(t, newTestServer(t), "/demo/README.md?fmt=raw")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d", rec.Code)
}
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/plain") {
t.Errorf("content-type = %q, want text/plain", ct)
}
if got := rec.Body.String(); got != "# Hello\n\nsome *markdown* & <angle> stuff\n" {
t.Errorf("body = %q; want exact file bytes", got)
}
}
func TestRenderedMarkdown(t *testing.T) {
rec := get(t, newTestServer(t), "/demo/README.md")
body := rec.Body.String()
if !strings.Contains(body, "<h1") || !strings.Contains(body, "<em>markdown</em>") {
t.Errorf("markdown not rendered; body:\n%s", body)
}
if !strings.Contains(body, `href="?fmt=raw"`) {
t.Errorf("missing raw link; body:\n%s", body)
}
}
func TestNonMarkdownGetsIframe(t *testing.T) {
rec := get(t, newTestServer(t), "/demo/sub/notes.txt")
if !strings.Contains(rec.Body.String(), `<iframe class="passthrough" src="?fmt=raw"`) {
t.Errorf("missing iframe; body:\n%s", rec.Body.String())
}
}
func TestDirListingHidesGit(t *testing.T) {
rec := get(t, newTestServer(t), "/demo/")
body := rec.Body.String()
if !strings.Contains(body, `href="sub/"`) || !strings.Contains(body, `href="README.md"`) {
t.Errorf("listing incomplete; body:\n%s", body)
}
if strings.Contains(body, ".git") {
t.Errorf(".git leaked into listing; body:\n%s", body)
}
}
func TestDirRedirectAddsSlash(t *testing.T) {
rec := get(t, newTestServer(t), "/demo/sub")
if rec.Code != http.StatusMovedPermanently || rec.Header().Get("Location") != "/demo/sub/" {
t.Errorf("got %d -> %q", rec.Code, rec.Header().Get("Location"))
}
}
func TestTraversalRejected(t *testing.T) {
for _, url := range []string{
"/demo/../../../etc/passwd",
"/demo/..%2f..%2fetc/passwd",
"/nosuchroot/x",
} {
rec := get(t, newTestServer(t), url)
if rec.Code != http.StatusNotFound && rec.Code != http.StatusMovedPermanently {
t.Errorf("%s: status = %d, want rejection", url, rec.Code)
}
if strings.Contains(rec.Body.String(), "root:") {
t.Errorf("%s: leaked /etc/passwd", url)
}
}
}
func TestRootIndexListsTrees(t *testing.T) {
rec := get(t, newTestServer(t), "/")
if !strings.Contains(rec.Body.String(), `href="demo/"`) {
t.Errorf("index missing tree link; body:\n%s", rec.Body.String())
}
}
// newNestedTestServer builds a server with a mount deeper than one URL segment,
// alongside a sibling subtree that is deliberately NOT mounted.
func newNestedTestServer(t *testing.T) *server {
t.Helper()
dir := t.TempDir()
for name, content := range map[string]string{
"research/latest/README.md": "# Fresh\n",
"research/old/secret.md": "# Stale\n",
} {
p := filepath.Join(dir, filepath.FromSlash(name))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
srv, err := newServer(map[string]string{
"meta/research/latest": filepath.Join(dir, "research/latest"),
})
if err != nil {
t.Fatal(err)
}
return srv
}
func TestNestedMountServesContent(t *testing.T) {
rec := get(t, newNestedTestServer(t), "/meta/research/latest/README.md")
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "Fresh") {
t.Errorf("status = %d; body:\n%s", rec.Code, rec.Body.String())
}
}
func TestSyntheticListingsAboveMount(t *testing.T) {
srv := newNestedTestServer(t)
for url, want := range map[string]string{
"/": `href="meta/"`,
"/meta/": `href="research/"`,
"/meta/research/": `href="latest/"`,
} {
rec := get(t, srv, url)
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), want) {
t.Errorf("%s: status = %d, want listing containing %s; body:\n%s", url, rec.Code, want, rec.Body.String())
}
}
}
func TestUnmountedSiblingsInvisible(t *testing.T) {
srv := newNestedTestServer(t)
rec := get(t, srv, "/meta/research/")
if strings.Contains(rec.Body.String(), "old") {
t.Errorf("unmounted sibling leaked into listing; body:\n%s", rec.Body.String())
}
rec = get(t, srv, "/meta/research/old/secret.md")
if rec.Code != http.StatusNotFound {
t.Errorf("unmounted sibling reachable: status = %d", rec.Code)
}
}
func TestNestedMountBreadcrumbsReachRoot(t *testing.T) {
rec := get(t, newNestedTestServer(t), "/meta/research/latest/README.md")
body := rec.Body.String()
for _, want := range []string{`href="/"`, `href="/meta/"`, `href="/meta/research/"`, `href="/meta/research/latest/"`} {
if !strings.Contains(body, want) {
t.Errorf("missing crumb %s; body:\n%s", want, body)
}
}
}
serve_test.go · 5.6 KiB · 1ed81d60bd