Files
sm-bm-plugin/classes/chat_context_test.go
Jakub Zych 8b81295458 fix: revoke chat access as soon as a user leaves the chat group
The BM chat context implements golem15.chat's LiveMembership, so every
participant check also requires chat-group membership: a removed user
loses # ogólny, their DMs, uploads, subscriptions and chat:message
delivery at once instead of after the next participant sync.
2026-10-26 19:33:52 +01:00

140 lines
4.9 KiB
Go

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.RequireLiveMembership() {
t.Error("RequireLiveMembership = false, want true: removal from the chat group must revoke access at once")
}
if !c.CanActFor(ctx, 7, 7) || c.CanActFor(ctx, 7, 8) {
t.Error("CanActFor is not self-only")
}
// Moderators delete other people's messages in group conversations
// (# ogólny) only, and only while they are chat-group members too
// (D-10, D-24).
for _, mc := range []struct {
name string
userID uint
convType string
want bool
}{
{"moderator in group", 7, "group", true},
{"moderator in dm", 7, "dm", false},
{"moderator, unknown type", 7, "", false},
{"non-moderator in group", 8, "group", false},
{"moderator outside the chat group", 9, "group", false},
{"no user", 0, "group", false},
} {
got, err := c.CanModerateMessage(ctx, mc.userID, mc.convType)
if err != nil || got != mc.want {
t.Errorf("%s: CanModerateMessage = %v, %v, want %v", mc.name, got, err, mc.want)
}
}
if _, err := c.CanModerateMessage(ctx, 13, "group"); err == nil {
t.Error("CanModerateMessage with a failing check returned no error")
}
noMods := classes.NewChatContext(&gorm.DB{}, "studenci", "").WithMembership(members)
if ok, err := noMods.CanModerateMessage(ctx, 7, "group"); ok || err != nil {
t.Errorf("CanModerateMessage without a moderator group = %v, %v, want false", ok, err)
}
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)
}
}