feat: add im group mention flows
This commit is contained in:
@@ -1115,6 +1115,93 @@ func serveIMFriends(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
writeIMSuccess(wrt, req, map[string]any{"items": payload, "next_cursor": nextCursor})
|
writeIMSuccess(wrt, req, map[string]any{"items": payload, "next_cursor": nextCursor})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serveIMGroupResource(wrt http.ResponseWriter, req *http.Request) {
|
||||||
|
topic, tail, ok := groupResourcePath(req.URL.Path)
|
||||||
|
if !ok {
|
||||||
|
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "group route not found", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tail == "members" {
|
||||||
|
serveIMGroupMembers(wrt, req, topic)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "group route not found", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic string) {
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if types.GetTopicCat(topic) != types.TopicCatGrp {
|
||||||
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selfSub, err := store.Subs.Get(principal.TenantID, topic, principal.Uid, false)
|
||||||
|
if err != nil {
|
||||||
|
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selfAccess := types.ModeNone
|
||||||
|
if selfSub != nil {
|
||||||
|
selfAccess = selfSub.ModeGiven & selfSub.ModeWant
|
||||||
|
}
|
||||||
|
if selfSub == nil || !selfAccess.IsReader() {
|
||||||
|
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "topic read permission required", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
limit := parseIMLimit(req.URL.Query().Get("limit"), 30, 100)
|
||||||
|
cursor, err := parseIMNonNegativeCursor(req.URL.Query().Get("cursor"))
|
||||||
|
if err != nil {
|
||||||
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "cursor"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query := strings.TrimSpace(req.URL.Query().Get("query"))
|
||||||
|
mentionableOnly := parseIMBool(req.URL.Query().Get("mentionable"))
|
||||||
|
members, err := store.Topics.GetUsers(principal.TenantID, topic, &types.QueryOpt{Limit: 1000})
|
||||||
|
if err != nil {
|
||||||
|
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]map[string]any, 0, limit)
|
||||||
|
seen := 0
|
||||||
|
nextCursor := ""
|
||||||
|
for _, member := range members {
|
||||||
|
access := member.ModeGiven & member.ModeWant
|
||||||
|
mentionable := access.IsJoiner() && access.IsReader()
|
||||||
|
if mentionableOnly && !mentionable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item := groupMemberPayload(member, mentionable)
|
||||||
|
if query != "" && !groupMemberMatches(item, query) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if seen < cursor {
|
||||||
|
seen++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(items) == limit {
|
||||||
|
nextCursor = strconv.Itoa(seen)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
seen++
|
||||||
|
}
|
||||||
|
permissions := map[string]any{
|
||||||
|
"can_mention_all": selfAccess.IsAdmin(),
|
||||||
|
}
|
||||||
|
writeIMSuccess(wrt, req, map[string]any{
|
||||||
|
"topic": topic,
|
||||||
|
"items": items,
|
||||||
|
"next_cursor": nextCursor,
|
||||||
|
"permissions": permissions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func serveIMFriendRequestAction(wrt http.ResponseWriter, req *http.Request) {
|
func serveIMFriendRequestAction(wrt http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != http.MethodPost {
|
if req.Method != http.MethodPost {
|
||||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||||
@@ -1525,6 +1612,27 @@ func parseIMLimit(raw string, fallback, max int) int {
|
|||||||
return limit
|
return limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseIMNonNegativeCursor(raw string) (int, error) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
cursor, err := strconv.Atoi(raw)
|
||||||
|
if err != nil || cursor < 0 {
|
||||||
|
return 0, errors.New("invalid cursor")
|
||||||
|
}
|
||||||
|
return cursor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIMBool(raw string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||||
|
case "1", "true", "yes", "y":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func conversationMatches(sub types.Subscription, query string) bool {
|
func conversationMatches(sub types.Subscription, query string) bool {
|
||||||
if strings.Contains(strings.ToLower(sub.Topic), query) {
|
if strings.Contains(strings.ToLower(sub.Topic), query) {
|
||||||
return true
|
return true
|
||||||
@@ -1560,6 +1668,45 @@ func receiptMemberPayload(sub types.Subscription) map[string]any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func groupMemberPayload(sub types.Subscription, mentionable bool) map[string]any {
|
||||||
|
access := sub.ModeGiven & sub.ModeWant
|
||||||
|
userID := types.ParseUserId(sub.User)
|
||||||
|
user := &types.User{ObjHeader: types.ObjHeader{Id: userID.String()}, Public: sub.GetPublic(), Trusted: sub.GetTrusted()}
|
||||||
|
data := userSummaryPayload(user, "")
|
||||||
|
data["role"] = groupMemberRole(access)
|
||||||
|
data["mentionable"] = mentionable
|
||||||
|
data["acs"] = map[string]any{
|
||||||
|
"mode": access.String(),
|
||||||
|
"want": sub.ModeWant.String(),
|
||||||
|
"given": sub.ModeGiven.String(),
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupMemberRole(access types.AccessMode) string {
|
||||||
|
switch {
|
||||||
|
case access.IsOwner():
|
||||||
|
return "owner"
|
||||||
|
case access.IsAdmin():
|
||||||
|
return "admin"
|
||||||
|
default:
|
||||||
|
return "member"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupMemberMatches(item map[string]any, query string) bool {
|
||||||
|
query = strings.ToLower(strings.TrimSpace(query))
|
||||||
|
if query == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, key := range []string{"display_name", "user_id"} {
|
||||||
|
if value, ok := item[key]; ok && strings.Contains(strings.ToLower(fmt.Sprint(value)), query) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func authorizeIMMessageMutation(principal *imPrincipal, topic string, seq int, allowAdmin bool) (*types.Message, bool, error) {
|
func authorizeIMMessageMutation(principal *imPrincipal, topic string, seq int, allowAdmin bool) (*types.Message, bool, error) {
|
||||||
if principal == nil {
|
if principal == nil {
|
||||||
return nil, false, types.ErrPermissionDenied
|
return nil, false, types.ErrPermissionDenied
|
||||||
@@ -2068,6 +2215,20 @@ func conversationContextTopicFromPath(path string) string {
|
|||||||
return strings.TrimSuffix(strings.TrimPrefix(path[idx:], prefix), suffix)
|
return strings.TrimSuffix(strings.TrimPrefix(path[idx:], prefix), suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func groupResourcePath(path string) (string, string, bool) {
|
||||||
|
const prefix = "/v1/im/groups/"
|
||||||
|
idx := strings.Index(path, prefix)
|
||||||
|
if idx < 0 {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
rest := strings.TrimPrefix(path[idx:], prefix)
|
||||||
|
topic, tail, ok := strings.Cut(rest, "/")
|
||||||
|
if !ok || topic == "" || tail == "" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return topic, strings.Trim(tail, "/"), true
|
||||||
|
}
|
||||||
|
|
||||||
func friendRequestActionFromPath(path string) (string, string) {
|
func friendRequestActionFromPath(path string) (string, string) {
|
||||||
const prefix = "/v1/im/friend-requests/"
|
const prefix = "/v1/im/friend-requests/"
|
||||||
idx := strings.Index(path, prefix)
|
idx := strings.Index(path, prefix)
|
||||||
|
|||||||
@@ -821,6 +821,53 @@ func TestServeIMMessageReceipts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServeIMGroupMembers(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||||
|
oldSubs := store.Subs
|
||||||
|
oldTopics := store.Topics
|
||||||
|
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||||
|
topics := mock_store.NewMockTopicsPersistenceInterface(ctrl)
|
||||||
|
store.Subs = subs
|
||||||
|
store.Topics = topics
|
||||||
|
defer func() {
|
||||||
|
store.Subs = oldSubs
|
||||||
|
store.Topics = oldTopics
|
||||||
|
}()
|
||||||
|
|
||||||
|
subs.EXPECT().Get(types.TenantID(7), "grpMention", types.Uid(42), false).Return(&types.Subscription{
|
||||||
|
TenantID: 7,
|
||||||
|
User: types.Uid(42).String(),
|
||||||
|
Topic: "grpMention",
|
||||||
|
ModeWant: types.ModeCFull,
|
||||||
|
ModeGiven: types.ModeCFull,
|
||||||
|
}, nil)
|
||||||
|
alice := types.Subscription{User: types.Uid(101).UserId(), ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic}
|
||||||
|
alice.SetPublic(map[string]any{"display_name": "Alice"})
|
||||||
|
bob := types.Subscription{User: types.Uid(102).UserId(), ModeWant: types.ModeJoin, ModeGiven: types.ModeJoin}
|
||||||
|
bob.SetPublic(map[string]any{"display_name": "Bob"})
|
||||||
|
topics.EXPECT().GetUsers(types.TenantID(7), "grpMention", gomock.Any()).Return([]types.Subscription{alice, bob}, nil)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/v1/im/groups/grpMention/members?query=ali&mentionable=true", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token-1")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
serveIMGroupResource(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||||
|
items := data["items"].([]any)
|
||||||
|
if len(items) != 1 || items[0].(map[string]any)["display_name"] != "Alice" || items[0].(map[string]any)["mentionable"] != true {
|
||||||
|
t.Fatalf("unexpected members response: %#v", data)
|
||||||
|
}
|
||||||
|
if data["permissions"].(map[string]any)["can_mention_all"] != true {
|
||||||
|
t.Fatalf("expected can_mention_all permission: %#v", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServeIMMessageRecall(t *testing.T) {
|
func TestServeIMMessageRecall(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|||||||
@@ -794,6 +794,7 @@ func main() {
|
|||||||
mux.HandleFunc(config.ApiPath+"v1/im/system-notifications/", serveIMSystemNotificationRead)
|
mux.HandleFunc(config.ApiPath+"v1/im/system-notifications/", serveIMSystemNotificationRead)
|
||||||
mux.HandleFunc(config.ApiPath+"v1/im/topics/", serveIMTopicMessageAction)
|
mux.HandleFunc(config.ApiPath+"v1/im/topics/", serveIMTopicMessageAction)
|
||||||
mux.HandleFunc(config.ApiPath+"v1/im/conversations/", serveIMConversationContext)
|
mux.HandleFunc(config.ApiPath+"v1/im/conversations/", serveIMConversationContext)
|
||||||
|
mux.HandleFunc(config.ApiPath+"v1/im/groups/", serveIMGroupResource)
|
||||||
mux.HandleFunc(config.ApiPath+"v1/im/friend-requests", serveIMFriendRequests)
|
mux.HandleFunc(config.ApiPath+"v1/im/friend-requests", serveIMFriendRequests)
|
||||||
mux.HandleFunc(config.ApiPath+"v1/im/friend-requests/", serveIMFriendRequestAction)
|
mux.HandleFunc(config.ApiPath+"v1/im/friend-requests/", serveIMFriendRequestAction)
|
||||||
mux.HandleFunc(config.ApiPath+"v1/im/blocks/", serveIMBlockUser)
|
mux.HandleFunc(config.ApiPath+"v1/im/blocks/", serveIMBlockUser)
|
||||||
|
|||||||
@@ -1052,6 +1052,14 @@ func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid,
|
|||||||
return types.ErrPermissionDenied
|
return types.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if t.cat == types.TopicCatGrp && head != nil && msg.sess != nil && msg.sess.authLvl != auth.LevelRoot {
|
||||||
|
normalized, err := t.validateAndNormalizeMentions(asUid, head)
|
||||||
|
if err != nil {
|
||||||
|
msg.sess.queueOut(ErrPermissionDenied(msg.Id, t.original(asUid), msg.Timestamp))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
head = normalized
|
||||||
|
}
|
||||||
if t.cat == types.TopicCatP2P && msg.sess != nil && msg.sess.authLvl != auth.LevelRoot &&
|
if t.cat == types.TopicCatP2P && msg.sess != nil && msg.sess.authLvl != auth.LevelRoot &&
|
||||||
store.Relations != nil && store.Store != nil && store.Store.IsOpen() {
|
store.Relations != nil && store.Store != nil && store.Store.IsOpen() {
|
||||||
peer := t.p2pOtherUser(asUid)
|
peer := t.p2pOtherUser(asUid)
|
||||||
@@ -1196,6 +1204,67 @@ func (t *Topic) handlePubBroadcast(msg *ClientComMessage) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Topic) validateAndNormalizeMentions(asUid types.Uid, head map[string]any) (map[string]any, error) {
|
||||||
|
mentionAll, _ := head["mention_all"].(bool)
|
||||||
|
rawMentions, hasMentions := head["mentions"]
|
||||||
|
if !mentionAll && !hasMentions {
|
||||||
|
return head, nil
|
||||||
|
}
|
||||||
|
pud := t.perUser[asUid]
|
||||||
|
access := pud.modeGiven & pud.modeWant
|
||||||
|
if mentionAll && !access.IsAdmin() {
|
||||||
|
return nil, types.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
validMembers := make(map[string]struct{}, len(t.perUser))
|
||||||
|
for uid, pud := range t.perUser {
|
||||||
|
mode := pud.modeGiven & pud.modeWant
|
||||||
|
if !pud.deleted && mode.IsJoiner() && mode.IsReader() {
|
||||||
|
validMembers[uid.UserId()] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalized := make(map[string]any, len(head))
|
||||||
|
for key, value := range head {
|
||||||
|
normalized[key] = value
|
||||||
|
}
|
||||||
|
if hasMentions {
|
||||||
|
filtered := filterValidMentions(rawMentions, validMembers)
|
||||||
|
if len(filtered) == 0 {
|
||||||
|
delete(normalized, "mentions")
|
||||||
|
} else {
|
||||||
|
normalized["mentions"] = filtered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterValidMentions(raw any, validMembers map[string]struct{}) []map[string]any {
|
||||||
|
mentions, ok := raw.([]any)
|
||||||
|
if !ok {
|
||||||
|
if typed, ok := raw.([]map[string]any); ok {
|
||||||
|
mentions = make([]any, 0, len(typed))
|
||||||
|
for _, item := range typed {
|
||||||
|
mentions = append(mentions, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(mentions) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
filtered := make([]map[string]any, 0, len(mentions))
|
||||||
|
for _, item := range mentions {
|
||||||
|
mention, ok := item.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userID, _ := mention["user_id"].(string)
|
||||||
|
if _, ok = validMembers[userID]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, mention)
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
// handleNoteBroadcast fans out {note} -> {info} messages to recipients in a master topic.
|
// handleNoteBroadcast fans out {note} -> {info} messages to recipients in a master topic.
|
||||||
// This is a NON-proxy broadcast (at master topic).
|
// This is a NON-proxy broadcast (at master topic).
|
||||||
func (t *Topic) handleNoteBroadcast(msg *ClientComMessage) {
|
func (t *Topic) handleNoteBroadcast(msg *ClientComMessage) {
|
||||||
|
|||||||
@@ -761,6 +761,73 @@ func TestHandleBroadcastDataRejectsUserSystemType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleBroadcastDataRejectsMentionAllByMember(t *testing.T) {
|
||||||
|
numUsers := 2
|
||||||
|
helper := TopicTestHelper{}
|
||||||
|
helper.setUp(t, numUsers, types.TopicCatGrp, "grp-test", true)
|
||||||
|
defer helper.tearDown()
|
||||||
|
pud := helper.topic.perUser[helper.uids[0]]
|
||||||
|
pud.modeWant = types.ModeCPublic
|
||||||
|
pud.modeGiven = types.ModeCPublic
|
||||||
|
helper.topic.perUser[helper.uids[0]] = pud
|
||||||
|
|
||||||
|
from := helper.uids[0].UserId()
|
||||||
|
msg := &ClientComMessage{
|
||||||
|
Id: "mention-all-1",
|
||||||
|
AsUser: from,
|
||||||
|
Pub: &MsgClientPub{
|
||||||
|
Topic: "grp-test",
|
||||||
|
Head: map[string]any{"mention_all": true},
|
||||||
|
Content: "hello all",
|
||||||
|
},
|
||||||
|
sess: helper.sessions[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.topic.handleClientMsg(msg)
|
||||||
|
helper.finish()
|
||||||
|
|
||||||
|
verifyResponseCodes(helper.results[0], []int{http.StatusForbidden}, t)
|
||||||
|
if helper.topic.lastID != 0 {
|
||||||
|
t.Fatalf("Topic.lastID: expected 0, got %d", helper.topic.lastID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleBroadcastDataFiltersInvalidMentions(t *testing.T) {
|
||||||
|
numUsers := 2
|
||||||
|
helper := TopicTestHelper{}
|
||||||
|
helper.setUp(t, numUsers, types.TopicCatGrp, "grp-test", true)
|
||||||
|
defer helper.tearDown()
|
||||||
|
helper.mm.EXPECT().Save(types.TenantID(1), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
|
DoAndReturn(func(_ types.TenantID, msg *types.Message, _ []string, _ bool) (*types.MessageSaveResult, error) {
|
||||||
|
mentions, ok := msg.Head["mentions"].([]map[string]any)
|
||||||
|
if !ok || len(mentions) != 1 || mentions[0]["user_id"] != helper.uids[1].UserId() {
|
||||||
|
t.Fatalf("unexpected normalized mentions: %#v", msg.Head["mentions"])
|
||||||
|
}
|
||||||
|
return &types.MessageSaveResult{SeqId: 1, MarkedReadBySender: true}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
from := helper.uids[0].UserId()
|
||||||
|
msg := &ClientComMessage{
|
||||||
|
Id: "mention-1",
|
||||||
|
AsUser: from,
|
||||||
|
Pub: &MsgClientPub{
|
||||||
|
Topic: "grp-test",
|
||||||
|
Head: map[string]any{"mentions": []any{
|
||||||
|
map[string]any{"user_id": helper.uids[1].UserId(), "offset": 0, "length": 2},
|
||||||
|
map[string]any{"user_id": types.Uid(99).UserId(), "offset": 3, "length": 2},
|
||||||
|
}},
|
||||||
|
Content: "hi",
|
||||||
|
NoEcho: true,
|
||||||
|
},
|
||||||
|
sess: helper.sessions[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.topic.handleClientMsg(msg)
|
||||||
|
helper.finish()
|
||||||
|
|
||||||
|
verifyResponseCodes(helper.results[0], []int{http.StatusAccepted}, t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleBroadcastDataInactiveTopic(t *testing.T) {
|
func TestHandleBroadcastDataInactiveTopic(t *testing.T) {
|
||||||
numUsers := 2
|
numUsers := 2
|
||||||
helper := TopicTestHelper{}
|
helper := TopicTestHelper{}
|
||||||
|
|||||||
Reference in New Issue
Block a user