diff --git a/classes/chat_context_test.go b/classes/chat_context_test.go new file mode 100644 index 0000000..843a834 --- /dev/null +++ b/classes/chat_context_test.go @@ -0,0 +1,107 @@ +package classes_test + +import ( + "context" + "errors" + "testing" + + "git.golem15.com/jakub/sm-bm-plugin/classes" + "gorm.io/gorm" +) + +// members is an injected group check: user 7 is in studenci, user 9 is in +// moderatorzy only, user 13 makes the check fail. +func members(_ context.Context, _ *gorm.DB, userID uint, group string) (bool, error) { + switch { + case userID == 13: + return false, errors.New("database down") + case group == "studenci": + return userID == 7, nil + case group == "moderatorzy": + return userID == 9 || userID == 7, nil + } + return false, nil +} + +func TestChatAccess(t *testing.T) { + db := func() *gorm.DB { return &gorm.DB{} } + access := classes.NewChatAccess(db, "studenci", members) + ctx := t.Context() + for _, c := range []struct { + name string + userID uint + contextType string + contextID uint + want bool + }{ + {"member of bm/1", 7, "bm", 1, true}, + {"other context type", 7, "channel", 1, false}, + {"other context id", 7, "bm", 2, false}, + {"non-member", 8, "bm", 1, false}, + {"moderator only", 9, "bm", 1, false}, + {"check fails", 13, "bm", 1, false}, + {"no user", 0, "bm", 1, false}, + } { + if got := access.CanAccessContext(ctx, c.userID, c.contextType, c.contextID); got != c.want { + t.Errorf("%s: CanAccessContext = %v, want %v", c.name, got, c.want) + } + if access.CanManageContext(ctx, c.userID, c.contextType, c.contextID) { + t.Errorf("%s: CanManageContext = true; nobody creates channels", c.name) + } + } + noDB := classes.NewChatAccess(func() *gorm.DB { return nil }, "studenci", members) + if noDB.CanAccessContext(ctx, 7, "bm", 1) { + t.Error("CanAccessContext without a database = true, want false") + } + nilFunc := classes.NewChatAccess(nil, "studenci", members) + if nilFunc.CanAccessContext(ctx, 7, "bm", 1) { + t.Error("CanAccessContext without a database func = true, want false") + } + noGroup := classes.NewChatAccess(db, "", members) + if noGroup.CanAccessContext(ctx, 7, "bm", 1) { + t.Error("CanAccessContext without a chat group = true, want false") + } +} + +func TestChatContext(t *testing.T) { + ctx := t.Context() + c := classes.NewChatContext(&gorm.DB{}, "studenci", "moderatorzy").WithMembership(members) + if c.ID() != classes.ChatContextID || c.ID() != 1 || c.Type() != "bm" { + t.Errorf("context = %s/%d, want bm/1", c.Type(), c.ID()) + } + if !c.ChatEnabled() || c.GroupName() != "ogólny" { + t.Errorf("ChatEnabled %v GroupName %q", c.ChatEnabled(), c.GroupName()) + } + if c.BroadcastChannel() != "chat:context:1" { + t.Errorf("BroadcastChannel = %q, want chat:context:1", c.BroadcastChannel()) + } + for _, id := range []uint{7, 9, 8} { + if ok, err := c.CanOverseeChats(ctx, id); ok || err != nil { + t.Errorf("CanOverseeChats(%d) = %v, %v; nobody oversees", id, ok, err) + } + } + if ok, err := c.HasMember(ctx, 7); !ok || err != nil { + t.Errorf("HasMember(7) = %v, %v, want true", ok, err) + } + if ok, err := c.HasMember(ctx, 9); ok || err != nil { + t.Errorf("HasMember(9) = %v, %v, want false (moderator group only)", ok, err) + } + if ok, _ := c.HasMember(ctx, 0); ok { + t.Error("HasMember(0) = true") + } + if _, err := c.HasMember(ctx, 13); err == nil { + t.Error("HasMember with a failing check returned no error") + } + if !c.CanActFor(ctx, 7, 7) || c.CanActFor(ctx, 7, 8) { + t.Error("CanActFor is not self-only") + } + + load := classes.ChatContextLoader(func() string { return "studenci" }, func() string { return "moderatorzy" }) + if got, err := load(ctx, &gorm.DB{}, 2); got != nil || err != nil { + t.Errorf("loader(2) = %v, %v, want nil, nil", got, err) + } + got, err := load(ctx, &gorm.DB{}, 1) + if err != nil || got == nil || got.ID() != 1 || got.Type() != "bm" { + t.Errorf("loader(1) = %v, %v, want the bm/1 context", got, err) + } +} diff --git a/controllers/api/chat_test.go b/controllers/api/chat_test.go new file mode 100644 index 0000000..0e368d0 --- /dev/null +++ b/controllers/api/chat_test.go @@ -0,0 +1,46 @@ +package api_test + +import ( + "net/http" + "testing" + + "git.golem15.com/golem15/summercms/modules/bouncer" + "git.golem15.com/jakub/sm-bm-plugin/controllers/api" + "gorm.io/gorm" +) + +// chatNoDB is a chat-enabled Handlers whose database is never published. +func chatNoDB() *api.Handlers { + return &api.Handlers{DB: func() *gorm.DB { return nil }, ChatGroup: "studenci", ChatModeratorGroup: "moderatorzy"} +} + +func TestChatBootstrapUnauthenticated(t *testing.T) { + code, body := serve(t, chatNoDB().Chat, http.MethodGet, "/", nil, nil) + if code != http.StatusUnauthorized || body["error"] != true { + t.Errorf("GET /chat without a principal = %d %v, want 401", code, body) + } +} + +func TestChatBootstrapNoDatabase(t *testing.T) { + code, body := serve(t, chatNoDB().Chat, http.MethodGet, "/", &bouncer.Principal{ID: 7}, nil) + if code != http.StatusServiceUnavailable || body["message"] != "Database unavailable." { + t.Errorf("GET /chat without a database = %d %v, want 503 Database unavailable.", code, body) + } + if _, ok := body["data"]; ok { + t.Errorf("GET /chat without a database carries data: %v", body) + } +} + +func TestChatMembersUnauthenticated(t *testing.T) { + code, body := serve(t, chatNoDB().ChatMembers, http.MethodGet, "/", nil, nil) + if code != http.StatusUnauthorized || body["error"] != true { + t.Errorf("GET /chat/members without a principal = %d %v, want 401", code, body) + } +} + +func TestChatMembersNoDatabase(t *testing.T) { + code, body := serve(t, chatNoDB().ChatMembers, http.MethodGet, "/", &bouncer.Principal{ID: 7}, nil) + if code != http.StatusServiceUnavailable { + t.Errorf("GET /chat/members without a database = %d %v, want 503", code, body) + } +} diff --git a/controllers/api/handlers_test.go b/controllers/api/handlers_test.go index 0832e4d..bae042d 100644 --- a/controllers/api/handlers_test.go +++ b/controllers/api/handlers_test.go @@ -55,6 +55,8 @@ func handlers(h *api.Handlers) map[string]http.HandlerFunc { "feed-put": h.UpdateCalendarFeed, "rotate": h.RotateCalendarFeed, "feed-del": h.DeleteCalendarFeed, + "chat": h.Chat, + "chat-mem": h.ChatMembers, } } diff --git a/plugin_test.go b/plugin_test.go index 3feecc5..2b4ca9e 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -77,8 +77,8 @@ func TestPluginActivates(t *testing.T) { if n := len(p.Models()); n != 11 { t.Errorf("Models = %d, want 11", n) } - if n := len(p.Migrations()); n != 14 { - t.Errorf("Migrations = %d, want 14", n) + if n := len(p.Migrations()); n != 15 { + t.Errorf("Migrations = %d, want 15", n) } if !slices.ContainsFunc(p.Commands(), func(c bonfire.Command) bool { return c.Name == "bm:seed" }) { t.Error("bm:seed is not registered")