package lighthouse import ( "context" "encoding/json" "net/http" "strings" "testing" "git.golem15.com/golem15/summercms/modules/pact" ) // recRouter records the groups and routes Mount registers. type recRouter struct { groups []recGroup routes []string } type recGroup struct { raw bool prefix string middleware []string routes []string } func (r *recRouter) group(raw bool, prefix string, mw []string, fn func(pact.Router)) { inner := &recRouter{} fn(inner) r.groups = append(r.groups, recGroup{raw: raw, prefix: prefix, middleware: append([]string(nil), mw...), routes: inner.routes}) } func (r *recRouter) Group(prefix string, mw []string, fn func(pact.Router)) { r.group(false, prefix, mw, fn) } func (r *recRouter) GroupRaw(prefix string, mw []string, fn func(pact.Router)) { r.group(true, prefix, mw, fn) } func (r *recRouter) add(m, p string) { r.routes = append(r.routes, m+" "+p) } func (r *recRouter) Get(p string, _ http.HandlerFunc, _ ...string) { r.add("GET", p) } func (r *recRouter) Post(p string, _ http.HandlerFunc, _ ...string) { r.add("POST", p) } func (r *recRouter) Put(p string, _ http.HandlerFunc, _ ...string) { r.add("PUT", p) } func (r *recRouter) Patch(p string, _ http.HandlerFunc, _ ...string) { r.add("PATCH", p) } func (r *recRouter) Delete(p string, _ http.HandlerFunc, _ ...string) { r.add("DELETE", p) } func (r *recRouter) Where(string, string) {} func (r *recRouter) WhereIn(string, ...string) {} // routeDriver is a driver that declares the given routes. type routeDriver struct{ routes []Route } func (d routeDriver) Name() string { return "acme" } func (d routeDriver) Routes() []Route { return d.routes } func (routeDriver) Publish(context.Context, string, string, json.RawMessage) error { return nil } func (routeDriver) Broadcast(context.Context, []string, string, json.RawMessage) error { return nil } func okHandler(http.ResponseWriter, *http.Request) {} // TestMountSurfaces covers D-13 and T-11-19: UserAuth and Public routes // mount in a group with the surface middleware then the shared middleware, // ServerToServer routes in a raw group, every method is registered, a // UserAuth route without a guard is refused before anything is mounted, // and a nil driver or the null driver mounts nothing. func TestMountSurfaces(t *testing.T) { s := Surfaces{UserAuth: []string{"jwt.auth"}, ServerToServer: []string{"acme.s2s"}, Public: []string{"acme.public"}, Middleware: []string{"throttle:ws-api"}} d := routeDriver{routes: []Route{ {Name: "token", Method: "get", Path: "/api/rt/token", Surface: UserAuth, Handler: okHandler}, {Name: "subscribe", Method: http.MethodPost, Path: "/api/rt/subscribe", Surface: ServerToServer, Handler: okHandler}, {Name: "put", Method: http.MethodPut, Path: "/api/rt/p", Surface: Public, Handler: okHandler}, {Name: "patch", Method: http.MethodPatch, Path: "/api/rt/p", Surface: Public, Handler: okHandler}, {Name: "delete", Method: http.MethodDelete, Path: "/api/rt/p", Surface: Public, Handler: okHandler}, }} r := &recRouter{} if err := Mount(r, d, s); err != nil { t.Fatal(err) } want := []recGroup{ {false, "/", []string{"jwt.auth", "throttle:ws-api"}, []string{"GET /api/rt/token"}}, {true, "/", []string{"acme.s2s", "throttle:ws-api"}, []string{"POST /api/rt/subscribe"}}, {false, "/", []string{"acme.public", "throttle:ws-api"}, []string{"PUT /api/rt/p"}}, {false, "/", []string{"acme.public", "throttle:ws-api"}, []string{"PATCH /api/rt/p"}}, {false, "/", []string{"acme.public", "throttle:ws-api"}, []string{"DELETE /api/rt/p"}}, } if len(r.groups) != len(want) { t.Fatalf("groups = %+v", r.groups) } for i, w := range want { g := r.groups[i] if g.raw != w.raw || g.prefix != w.prefix || strings.Join(g.middleware, ",") != strings.Join(w.middleware, ",") || strings.Join(g.routes, ",") != strings.Join(w.routes, ",") { t.Errorf("group %d = %+v, want %+v", i, g, w) } } if len(s.UserAuth) != 1 || s.UserAuth[0] != "jwt.auth" { t.Fatal("Mount mutated the caller's surface middleware") } refusals := []struct { name string route Route s Surfaces want string }{ {"user_route_without_guard", Route{Name: "token", Method: "GET", Path: "/t", Surface: UserAuth, Handler: okHandler}, Surfaces{Middleware: []string{"throttle"}}, "Surfaces.UserAuth is empty"}, {"no_handler", Route{Name: "x", Method: "GET", Path: "/t", Surface: Public}, s, "has no handler"}, {"relative_path", Route{Name: "x", Method: "GET", Path: "t", Surface: Public, Handler: okHandler}, s, "must be absolute"}, {"bad_method", Route{Name: "x", Method: "TRACE", Path: "/t", Surface: Public, Handler: okHandler}, s, "unsupported method"}, {"unknown_surface", Route{Name: "x", Method: "GET", Path: "/t", Surface: Surface(9), Handler: okHandler}, s, "unknown surface Surface(9)"}, } for _, c := range refusals { t.Run(c.name, func(t *testing.T) { r := &recRouter{} // A valid route first: nothing may be mounted when a later one // is invalid. valid := Route{Name: "ok", Method: "POST", Path: "/ok", Surface: ServerToServer, Handler: okHandler} err := Mount(r, routeDriver{routes: []Route{valid, c.route}}, c.s) if err == nil || !strings.Contains(err.Error(), c.want) || !strings.Contains(err.Error(), "driver acme route") { t.Fatalf("err = %v, want %q", err, c.want) } if len(r.groups) != 0 { t.Fatalf("mounted %d group(s) before refusing", len(r.groups)) } }) } if err := Mount(nil, d, s); err == nil { t.Fatal("nil router accepted") } r = &recRouter{} if err := Mount(r, nil, s); err != nil || len(r.groups) != 0 { t.Fatalf("nil driver: err %v, groups %d", err, len(r.groups)) } if err := Mount(r, nullDriver{}, Surfaces{}); err != nil || len(r.groups) != 0 { t.Fatalf("null driver: err %v, groups %d", err, len(r.groups)) } for surface, name := range map[Surface]string{UserAuth: "UserAuth", ServerToServer: "ServerToServer", Public: "Public", Surface(0): "Surface(0)"} { if surface.String() != name { t.Errorf("Surface(%d).String() = %q", int(surface), surface.String()) } } }