package serve import ( "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func setup(t *testing.T, token string) (http.Handler, string) { t.Helper() dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "feed.xml"), []byte(""), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "01 - One.mp3"), []byte("audio"), 0o644); err != nil { t.Fatal(err) } return Handler(Options{Dir: dir, Token: token}), dir } func get(h http.Handler, target string, bearer string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodGet, target, nil) if bearer != "" { req.Header.Set("Authorization", "Bearer "+bearer) } rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec } func TestServesFeedAndAudioWithoutToken(t *testing.T) { h, _ := setup(t, "") if rec := get(h, "/feed.xml", ""); rec.Code != http.StatusOK { t.Errorf("/feed.xml = %d, want 200", rec.Code) } if rec := get(h, "/audio/01%20-%20One.mp3", ""); rec.Code != http.StatusOK { t.Errorf("audio = %d, want 200", rec.Code) } } // The escaped URL the feed generator emits must actually resolve against the // server that serves those files — the two halves have to agree. func TestEscapedFilenameResolves(t *testing.T) { h, dir := setup(t, "") if err := os.WriteFile(filepath.Join(dir, "10 - Probability & Uncertainty.mp3"), []byte("x"), 0o644); err != nil { t.Fatal(err) } rec := get(h, "/audio/10%20-%20Probability%20&%20Uncertainty.mp3", "") if rec.Code != http.StatusOK { t.Errorf("escaped filename = %d, want 200", rec.Code) } } func TestTokenRequiredWhenSet(t *testing.T) { h, _ := setup(t, "s3cret") for _, target := range []string{"/feed.xml", "/audio/01%20-%20One.mp3", "/"} { if rec := get(h, target, ""); rec.Code != http.StatusNotFound { t.Errorf("%s without a token = %d, want 404", target, rec.Code) } } } func TestTokenAcceptedInQuery(t *testing.T) { h, _ := setup(t, "s3cret") if rec := get(h, "/feed.xml?k=s3cret", ""); rec.Code != http.StatusOK { t.Errorf("query token = %d, want 200", rec.Code) } } // Podcast clients cannot do interactive auth, so the query form is the one // that matters; the bearer header is for curl and for testing. func TestTokenAcceptedAsBearer(t *testing.T) { h, _ := setup(t, "s3cret") if rec := get(h, "/feed.xml", "s3cret"); rec.Code != http.StatusOK { t.Errorf("bearer token = %d, want 200", rec.Code) } } func TestWrongTokenRejected(t *testing.T) { h, _ := setup(t, "s3cret") if rec := get(h, "/feed.xml?k=wrong", ""); rec.Code != http.StatusNotFound { t.Errorf("wrong token = %d, want 404", rec.Code) } if rec := get(h, "/feed.xml", "wrong"); rec.Code != http.StatusNotFound { t.Errorf("wrong bearer = %d, want 404", rec.Code) } } // A gated server must not confirm that anything is there. func TestRejectionLooksLikeAbsence(t *testing.T) { h, _ := setup(t, "s3cret") rec := get(h, "/feed.xml", "") if rec.Code == http.StatusUnauthorized || rec.Code == http.StatusForbidden { t.Errorf("status %d advertises that something exists here", rec.Code) } if strings.Contains(strings.ToLower(rec.Body.String()), "token") { t.Error("the rejection body mentions a token, which tells a prober what to look for") } } // http.FileServer resolves .. before dispatch, but this is the kind of thing // worth a standing assertion rather than a memory of having checked once. func TestNoPathTraversal(t *testing.T) { h, dir := setup(t, "") secret := filepath.Join(filepath.Dir(dir), "secret.txt") if err := os.WriteFile(secret, []byte("do not serve"), 0o644); err != nil { t.Fatal(err) } defer func() { _ = os.Remove(secret) }() for _, target := range []string{ "/audio/../secret.txt", "/audio/..%2fsecret.txt", "/audio/%2e%2e/secret.txt", } { rec := get(h, target, "") if strings.Contains(rec.Body.String(), "do not serve") { t.Errorf("%s escaped the served directory", target) } } } func TestNewTokenIsRandomAndLongEnough(t *testing.T) { a, err := NewToken() if err != nil { t.Fatalf("NewToken: %v", err) } b, _ := NewToken() if a == b { t.Error("two generated tokens were identical") } if len(a) < 32 { t.Errorf("token is only %d chars; it is the only thing protecting the feed", len(a)) } } func TestUnknownPathIs404(t *testing.T) { h, _ := setup(t, "") if rec := get(h, "/nope", ""); rec.Code != http.StatusNotFound { t.Errorf("unknown path = %d, want 404", rec.Code) } }