package surf import ( "errors" "net/http" "net/http/httptest" "strings" "testing" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" ) // Smoke test for the in-process dispatch tracer of phase 19.7 (D-17): a // request carrying the bouncer dispatch marker runs a guarded route through // surf's compiled chain as the pre-resolved principal, while nothing an HTTP // request can carry is treated as one. // refusingGuard never authenticates a request and writes its own 401. type refusingGuard struct{} func (refusingGuard) AuthenticateCredential(*http.Request) (*bouncer.Principal, any, error) { return nil, nil, errors.New("acme: no token") } func (refusingGuard) WriteUnauthorized(w http.ResponseWriter, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":true,"message":"Unauthenticated"}`)) } // dispatchPlugin registers bouncer guard "acme" as middleware "acme.auth" // and one guarded route. type dispatchPlugin struct{} func (dispatchPlugin) ID() string { return "acme.items" } func (dispatchPlugin) Requires() []string { return nil } func (dispatchPlugin) Register(*backpack.App) error { return nil } func (dispatchPlugin) Boot(*backpack.App) error { return nil } func (p dispatchPlugin) Middlewares() map[string]pact.Middleware { guards := bouncer.NewRegistry() if err := guards.Register(p.ID(), "acme", refusingGuard{}); err != nil { panic(err) } auth, err := guards.Middleware("acme") if err != nil { panic(err) } return map[string]pact.Middleware{"acme.auth": auth} } func (dispatchPlugin) Routes(r pact.Router) error { r.Group("/acme", Use("acme.auth"), func(g pact.Router) { g.Get("/items/{id}", func(w http.ResponseWriter, req *http.Request) { user, _ := bouncer.User(req.Context()) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"user":` + itoa(user.ID) + `,"item":"` + req.PathValue("id") + `"}`)) }) }) return nil } func itoa(n uint) string { if n == 0 { return "0" } var b []byte for n > 0 { b = append([]byte{byte('0' + n%10)}, b...) n /= 10 } return string(b) } func TestDispatchSmoke(t *testing.T) { app := transportApp(t, "development", map[string]string{"http.yaml": transportBodyLimits}) router, err := BuildRouter(app, []party.Plugin{dispatchPlugin{}}) if err != nil { t.Fatal(err) } if _, ok := router.Handler("GET", "/acme/items/{id}/missing"); ok { t.Fatal("Handler found an unregistered route") } h, ok := router.Handler("GET", "/acme/items/{id}") if !ok || h == nil { t.Fatal("Handler did not find GET /acme/items/{id} after BuildRouter") } var nilRouter *Router if _, ok := nilRouter.Handler("GET", "/acme/items/{id}"); ok { t.Fatal("nil router found a handler") } serve := func(set func(*http.Request) *http.Request) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodGet, "/acme/items/7", nil) req.SetPathValue("id", "7") req.Pattern = "GET /acme/items/{id}" req = set(req) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec } marker := func(guards ...string) func(*http.Request) *http.Request { return func(req *http.Request) *http.Request { d := &bouncer.Dispatch{Guards: guards, Principal: &bouncer.Principal{ID: 42}} return req.WithContext(bouncer.WithDispatch(req.Context(), d)) } } rec := serve(marker("acme")) if rec.Code != http.StatusOK || strings.TrimSpace(rec.Body.String()) != `{"user":42,"item":"7"}` { t.Fatalf("dispatch with the marker = %d %s", rec.Code, rec.Body.String()) } if rec := serve(func(req *http.Request) *http.Request { return req }); rec.Code != http.StatusUnauthorized { t.Fatalf("request without the marker = %d %s", rec.Code, rec.Body.String()) } forged := serve(func(req *http.Request) *http.Request { req.Header.Set("X-Summer-Dispatch", "42") req.Header.Set("Authorization", "Dispatch 42") req.AddCookie(&http.Cookie{Name: "summer_dispatch", Value: "42"}) return req }) if forged.Code != http.StatusUnauthorized { t.Fatalf("forged headers = %d %s", forged.Code, forged.Body.String()) } if rec := serve(marker("other")); rec.Code != http.StatusUnauthorized { t.Fatalf("marker for another guard = %d %s", rec.Code, rec.Body.String()) } // The assembled handler serves the same route over HTTP unchanged. full, err := router.compile() if err != nil { t.Fatal(err) } rec = httptest.NewRecorder() full.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/acme/items/7", nil)) if rec.Code != http.StatusUnauthorized { t.Fatalf("HTTP request without a token = %d %s", rec.Code, rec.Body.String()) } }