package bm import ( "context" "errors" "net/http" "net/http/httptest" "testing" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/compass" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" "git.golem15.com/golem15/summercms/modules/wire" "gorm.io/gorm" ) // fakePresentations stands in for golem15.presentations: its owner guard // lets user 5 through and refuses everyone else. type fakePresentations struct{} func (fakePresentations) ID() string { return "golem15.presentations" } func (fakePresentations) Requires() []string { return nil } func (fakePresentations) Register(*backpack.App) error { return nil } func (fakePresentations) Boot(*backpack.App) error { return nil } func (fakePresentations) Middlewares() map[string]pact.Middleware { return map[string]pact.Middleware{ "presentations.realtime-owner": func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if u, ok := bouncer.User(r.Context()); !ok || u.ID != 5 { w.Header().Set("Cache-Control", "no-cache, private") wire.WriteJSON(w, http.StatusForbidden, map[string]string{"error": "Forbidden"}) return } next.ServeHTTP(w, r) }) }, } } // chatGroupMember is the injected group check: user 7 is in the chat // group, user 13 makes the check fail. func chatGroupMember(_ context.Context, _ *gorm.DB, userID uint, group string) (bool, error) { if userID == 13 { return false, errors.New("database down") } return group == "studenci" && userID == 7, nil } // accessPlugin builds jz.bm on an app with the given active plugin ids, a // published (fake) database unless noDB, and golem15.presentations in the // activated list when withOwnerGuard. func accessPlugin(t *testing.T, ids []string, noDB, withOwnerGuard bool) *Plugin { t.Helper() cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Environ: []string{"SUMMER_ENV=testing"}}) if err != nil { t.Fatal(err) } app := backpack.New(cfg) app.SetPlugins(ids) if !noDB { if err := app.Publish[*gorm.DB](&gorm.DB{}); err != nil { t.Fatal(err) } } activated := party.Activated{} if withOwnerGuard { activated = append(activated, fakePresentations{}) } if err := app.Publish(activated); err != nil { t.Fatal(err) } return &Plugin{app: app, chatMember: chatGroupMember} } func TestRealtimeAccess(t *testing.T) { chatOnly := []string{"golem15.user", "golem15.chat", "jz.bm"} both := []string{"golem15.user", "golem15.chat", "jz.bm", "golem15.presentations"} presOnly := []string{"golem15.user", "jz.bm", "golem15.presentations"} for _, c := range []struct { name string ids []string noDB, guard bool userID uint want int }{ {"no user", both, false, true, 0, http.StatusUnauthorized}, {"chat member", chatOnly, false, false, 7, http.StatusOK}, {"chat member with presentations", both, false, true, 7, http.StatusOK}, {"non-member without presentations", chatOnly, false, false, 8, http.StatusForbidden}, {"presentation owner", both, false, true, 5, http.StatusOK}, {"presentation owner without chat", presOnly, false, true, 5, http.StatusOK}, {"neither member nor owner", both, false, true, 8, http.StatusForbidden}, {"presentations guard not found", both, false, false, 5, http.StatusForbidden}, {"no database", both, true, true, 7, http.StatusServiceUnavailable}, {"membership check fails", both, false, true, 13, http.StatusServiceUnavailable}, {"no consumer", []string{"golem15.user", "jz.bm"}, false, false, 7, http.StatusForbidden}, } { p := accessPlugin(t, c.ids, c.noDB, c.guard) mw, ok := p.Middlewares()[MiddlewareRealtimeAccess] if !ok { t.Fatalf("%s is not exported", MiddlewareRealtimeAccess) } h := mw(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) req := httptest.NewRequest(http.MethodGet, "/api/realtime/token", nil) if c.userID != 0 { req = req.WithContext(bouncer.WithUser(req.Context(), &bouncer.Principal{ID: c.userID})) } rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != c.want { t.Errorf("%s: %d %s, want %d", c.name, rec.Code, rec.Body.String(), c.want) continue } if c.want != http.StatusOK { if got := rec.Header().Get("Cache-Control"); got != "no-cache, private" { t.Errorf("%s: Cache-Control %q, want no-cache, private", c.name, got) } if body := rec.Body.String(); body == "" || body[0] != '{' { t.Errorf("%s: body %q is not an error object", c.name, body) } } } }