1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
package server

import (
	"net/http"
	"net/http/httptest"
	"rss_parrot/shared"
	"strings"
	"testing"
)

type fakeGroup struct {
	prefix string
	defs   []handlerDef
	authed *[]string
}

func (g fakeGroup) Prefix() string          { return g.prefix }
func (g fakeGroup) GroupDefs() []handlerDef { return g.defs }
func (g fakeGroup) AuthMW() func(next http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			*g.authed = append(*g.authed, r.URL.Path)
			next.ServeHTTP(w, r)
		})
	}
}

// echo answers with its name and the path values it was given.
func echo(name string, keys ...string) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		out := name
		for _, k := range keys {
			out += " " + k + "=" + r.PathValue(k)
		}
		w.Write([]byte(out))
	}
}

func TestMuxRouting(t *testing.T) {
	var authed []string
	groups := []IHandlerGroup{
		fakeGroup{"/api", []handlerDef{
			{"DELETE", "/accounts/{account}", echo("delete", "account")},
		}, &authed},
		fakeGroup{"", []handlerDef{
			{"GET", "/u/{user}/status/{id}", echo("status", "user", "id")},
			{"POST", "/u/{user}/inbox", echo("inbox", "user")},
		}, &authed},
		fakeGroup{"/web", []handlerDef{
			{"GET", "/about", echo("about")},
			{"GET", rootPlacholder, echo("root")},
			{"GET", notFoundPlacholder, func(w http.ResponseWriter, r *http.Request) {
				http.Error(w, "custom 404", http.StatusNotFound)
			}},
		}, &authed},
	}
	router := trimSlashHandler(NewMux(groups, shared.NewLogger(&strings.Builder{}, "error")))

	for _, tc := range []struct {
		method, path string
		status       int
		body         string
	}{
		{"DELETE", "/api/accounts/foo.example", 200, "delete account=foo.example"},
		{"GET", "/u/birb/status/42", 200, "status user=birb id=42"},
		{"GET", "/u/birb/status/42/", 200, "status user=birb id=42"},
		{"POST", "/u/birb/inbox", 200, "inbox user=birb"},
		{"GET", "/web/about", 200, "about"},
		{"GET", "/", 200, "root"},
		// HEAD is a GET without the body.
		{"HEAD", "/web/about", 200, ""},
		// A known path asked with the wrong method.
		{"POST", "/web/about", 405, ""},
		{"OPTIONS", "/api/accounts/foo.example", 405, ""},
		// Any GET is caught by the static files, even on a POST-only path.
		{"GET", "/u/birb/inbox", 404, "custom 404"},
		// Unknown paths go to the static files, and from there to the 404 page.
		{"GET", "/nope", 404, "custom 404"},
		{"GET", "/assets/nope.css", 404, "custom 404"},
		{"POST", "/nope", 405, ""},
	} {
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, httptest.NewRequest(tc.method, tc.path, nil))
		if rec.Code != tc.status {
			t.Errorf("%s %s: status %d, want %d", tc.method, tc.path, rec.Code, tc.status)
			continue
		}
		if tc.body != "" && !strings.Contains(rec.Body.String(), tc.body) {
			t.Errorf("%s %s: body %q, want it to contain %q", tc.method, tc.path, rec.Body.String(), tc.body)
		}
		if tc.status == 200 && tc.path != "/" && rec.Header().Get(strCacheControlHdr) != "no-cache, no-store, must-revalidate" {
			t.Errorf("%s %s: Cache-Control %q, want no-cache", tc.method, tc.path, rec.Header().Get(strCacheControlHdr))
		}
	}

	// The root route is outside the groups and skips their middleware.
	for _, p := range authed {
		if p == "/" {
			t.Errorf("auth middleware ran for /")
		}
	}
	if len(authed) == 0 {
		t.Errorf("auth middleware never ran")
	}
}