fix: handle backend issue list regressions

This commit is contained in:
richjohnrambo
2026-09-30 15:35:03 +08:00
parent 7685fb0c02
commit 99ae6328ff
8 changed files with 207 additions and 5 deletions
+21
View File
@@ -4,6 +4,7 @@ package mysql
import ( import (
"database/sql" "database/sql"
"os"
"strings" "strings"
"testing" "testing"
@@ -150,3 +151,23 @@ func TestTenantShardSQL(t *testing.T) {
t.Fatalf("tenant shard query rewrote index name: %s", query) t.Fatalf("tenant shard query rewrote index name: %s", query)
} }
} }
func TestSharedDBGroupFileLinkIncludesTenantID(t *testing.T) {
source, err := os.ReadFile("tenant_groups.go")
if err != nil {
t.Fatal(err)
}
body := sourceBetween(t, string(source), "func (a *adapter) TenantGroupFileCreate", "\nfunc (a *adapter) enrichGroupFileFromUpload")
for _, required := range []string{
"INSERT INTO im_filemsglinks(tenant_id,createdat,fileid,topic)",
"SELECT ?,?,?,? WHERE NOT EXISTS",
"SELECT 1 FROM im_filemsglinks WHERE tenant_id=? AND fileid=? AND topic=?)",
} {
if !strings.Contains(body, required) {
t.Fatalf("TenantGroupFileCreate shared-db file link must include tenant_id; missing %q", required)
}
}
if strings.Contains(body, "INSERT INTO im_filemsglinks(createdat,fileid,topic)") {
t.Fatal("TenantGroupFileCreate must not rely on im_filemsglinks.tenant_id default")
}
}
+4 -4
View File
@@ -1132,10 +1132,10 @@ func (a *adapter) TenantGroupFileCreate(id t.TenantID, file *t.GroupFile) (*t.Gr
SELECT 1 FROM im_filemsglinks WHERE tenant_id=? AND fileid=? AND topic=?)`, SELECT 1 FROM im_filemsglinks WHERE tenant_id=? AND fileid=? AND topic=?)`,
id, now, decodeUID(fid), file.Topic, id, decodeUID(fid), file.Topic) id, now, decodeUID(fid), file.Topic, id, decodeUID(fid), file.Topic)
} else { } else {
_, err = tx.ExecContext(ctx, `INSERT INTO im_filemsglinks(createdat,fileid,topic) _, err = tx.ExecContext(ctx, `INSERT INTO im_filemsglinks(tenant_id,createdat,fileid,topic)
SELECT ?,?,? WHERE NOT EXISTS ( SELECT ?,?,?,? WHERE NOT EXISTS (
SELECT 1 FROM im_filemsglinks WHERE fileid=? AND topic=?)`, SELECT 1 FROM im_filemsglinks WHERE tenant_id=? AND fileid=? AND topic=?)`,
now, decodeUID(fid), file.Topic, decodeUID(fid), file.Topic) id, now, decodeUID(fid), file.Topic, id, decodeUID(fid), file.Topic)
} }
if err != nil { if err != nil {
return nil, err return nil, err
+4
View File
@@ -2637,6 +2637,10 @@ func serveIMSystemNotificationRead(wrt http.ResponseWriter, req *http.Request) {
return return
} }
readAt = parsed.UTC().Round(time.Millisecond) readAt = parsed.UTC().Round(time.Millisecond)
if readAt.After(time.Now().UTC().Round(time.Millisecond)) {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "read_at", "reason": "future_read_at"})
return
}
} }
notification, err := store.Security.MarkSystemNotificationRead(principal.TenantID, principal.Uid, eventID, readAt) notification, err := store.Security.MarkSystemNotificationRead(principal.TenantID, principal.Uid, eventID, readAt)
if err != nil { if err != nil {
+12
View File
@@ -3166,6 +3166,18 @@ func TestServeIMSystemNotificationsListAndRead(t *testing.T) {
if readAgainData["id"] != "evt-2" || readAgainData["read_at"] != readAt.UTC().Format(time.RFC3339Nano) { if readAgainData["id"] != "evt-2" || readAgainData["read_at"] != readAt.UTC().Format(time.RFC3339Nano) {
t.Fatalf("read again response should keep persisted read_at: %#v", readAgainData) t.Fatalf("read again response should keep persisted read_at: %#v", readAgainData)
} }
expectIMBearer(t, ctrl, "token-read-future", 7, 42)
req = httptest.NewRequest(http.MethodPost, "/v1/im/system-notifications/evt-1/read", bytes.NewBufferString(`{"read_at":"2099-01-01T00:00:00Z"}`))
req.Header.Set("Authorization", "Bearer token-read-future")
req.Header.Set("Idempotency-Key", "read-future")
rec = httptest.NewRecorder()
serveIMSystemNotificationRead(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("future read_at status: got %d, want %d; body=%s", rec.Code, http.StatusBadRequest, rec.Body.String())
}
} }
func TestServeIMMessageReceipts(t *testing.T) { func TestServeIMMessageReceipts(t *testing.T) {
+19 -1
View File
@@ -1411,7 +1411,8 @@ func (r *redisGroupMessageRuntime) hotSeqIDs(tenantID types.TenantID, topic stri
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
if len(raw) < limit { expected := hotSeqExpectedCount(opts, limit)
if len(raw) < expected {
return nil, false, nil return nil, false, nil
} }
seqs := make([]int64, 0, len(raw)) seqs := make([]int64, 0, len(raw))
@@ -1425,6 +1426,23 @@ func (r *redisGroupMessageRuntime) hotSeqIDs(tenantID types.TenantID, topic stri
return seqs, true, nil return seqs, true, nil
} }
func hotSeqExpectedCount(opts *types.QueryOpt, limit int) int {
if limit <= 0 {
return 0
}
if opts == nil || opts.Since <= 0 || opts.Before <= opts.Since {
return limit
}
expected := opts.Before - opts.Since
if expected <= 0 {
return limit
}
if expected > int64(limit) {
return limit
}
return int(expected)
}
func expandRanges(ranges []types.Range, limit int) []int64 { func expandRanges(ranges []types.Range, limit int) []int64 {
if limit <= 0 { if limit <= 0 {
return nil return nil
@@ -488,6 +488,21 @@ func TestTopicAndSubscriptionCacheTTLIsShort(t *testing.T) {
} }
} }
func TestHotSeqExpectedCountUsesRangeSizeWhenSmallerThanLimit(t *testing.T) {
if got := hotSeqExpectedCount(&types.QueryOpt{Since: 41, Before: 54, Limit: 20}, 20); got != 13 {
t.Fatalf("expected range-sized count, got %d", got)
}
if got := hotSeqExpectedCount(&types.QueryOpt{Since: 41, Before: 80, Limit: 20}, 20); got != 20 {
t.Fatalf("expected limit-sized count, got %d", got)
}
if got := hotSeqExpectedCount(&types.QueryOpt{Since: 0, Before: 54, Limit: 20}, 20); got != 20 {
t.Fatalf("expected fallback limit count, got %d", got)
}
if got := hotSeqExpectedCount(nil, 20); got != 20 {
t.Fatalf("expected nil opts limit count, got %d", got)
}
}
func TestInvalidateSubscriptionCacheWithZeroUserDeletesTopicPrefix(t *testing.T) { func TestInvalidateSubscriptionCacheWithZeroUserDeletesTopicPrefix(t *testing.T) {
oldRuntime := groupMsgRuntime oldRuntime := groupMsgRuntime
runtime := newTestGroupMessageRuntime() runtime := newTestGroupMessageRuntime()
+59
View File
@@ -3535,6 +3535,13 @@ func (t *Topic) replyGetData(sess *Session, asUid types.Uid, asChan bool, req *M
} else if ok { } else if ok {
messages = filterHotMessagesForUser(hotMessages, asUid) messages = filterHotMessagesForUser(hotMessages, asUid)
} }
} else if shouldTryHotMessageSupplement(t.cat, opts, len(messages)) {
hotMessages, ok, cacheErr := store.GetHotMessages(t.tenantID, t.name, opts, hotMessageRangeLimit(opts))
if cacheErr != nil {
logs.Warn.Printf("get.data hot message supplement failed topic=%s user=%s: %v", t.name, asUid.UserId(), cacheErr)
} else if ok {
messages = mergeHotMessageSupplement(messages, filterHotMessagesForUser(hotMessages, asUid), opts)
}
} }
// Push the list of messages to the client as {data}. // Push the list of messages to the client as {data}.
@@ -3595,6 +3602,28 @@ func shouldTryHotMessageFallback(cat types.TopicCat, lastID int64, opts *types.Q
return lastID <= 0 || opts.Since >= floor return lastID <= 0 || opts.Since >= floor
} }
func shouldTryHotMessageSupplement(cat types.TopicCat, opts *types.QueryOpt, got int) bool {
if cat != types.TopicCatGrp || opts == nil || opts.IfModifiedSince != nil ||
!opts.User.IsZero() || opts.Topic != "" || len(opts.IdRanges) > 0 ||
opts.Since <= 0 || opts.Before <= opts.Since {
return false
}
expected := hotMessageRangeLimit(opts)
return expected > 0 && got > 0 && got < expected
}
func hotMessageRangeLimit(opts *types.QueryOpt) int {
if opts == nil || opts.Since <= 0 || opts.Before <= opts.Since {
return hotMessageFallbackLimit(opts)
}
expected := opts.Before - opts.Since
limit := hotMessageFallbackLimit(opts)
if limit <= 0 || int64(limit) > expected {
return int(expected)
}
return limit
}
func hotMessageFallbackLimit(opts *types.QueryOpt) int { func hotMessageFallbackLimit(opts *types.QueryOpt) int {
if opts != nil && opts.Limit > 0 { if opts != nil && opts.Limit > 0 {
return opts.Limit return opts.Limit
@@ -3602,6 +3631,36 @@ func hotMessageFallbackLimit(opts *types.QueryOpt) int {
return 1 return 1
} }
func mergeHotMessageSupplement(messages, hotMessages []types.Message, opts *types.QueryOpt) []types.Message {
if len(messages) == 0 || len(hotMessages) == 0 {
return messages
}
bySeq := make(map[int64]types.Message, len(hotMessages)+len(messages))
for _, msg := range hotMessages {
bySeq[msg.SeqId] = msg
}
for _, msg := range messages {
bySeq[msg.SeqId] = msg
}
merged := make([]types.Message, 0, len(bySeq))
for _, msg := range bySeq {
if opts != nil {
if opts.Since > 0 && msg.SeqId < opts.Since {
continue
}
if opts.Before > 0 && msg.SeqId >= opts.Before {
continue
}
}
merged = append(merged, msg)
}
sort.Slice(merged, func(i, j int) bool { return merged[i].SeqId > merged[j].SeqId })
if opts != nil && opts.Limit > 0 && len(merged) > opts.Limit {
merged = merged[:opts.Limit]
}
return merged
}
func filterHotMessagesForUser(messages []types.Message, asUid types.Uid) []types.Message { func filterHotMessagesForUser(messages []types.Message, asUid types.Uid) []types.Message {
if len(messages) == 0 { if len(messages) == 0 {
return messages return messages
+73
View File
@@ -1272,6 +1272,55 @@ func TestShouldTryHotMessageFallback(t *testing.T) {
} }
} }
func TestShouldTryHotMessageSupplement(t *testing.T) {
opts := &types.QueryOpt{Since: 41, Before: 54, Limit: 20}
if !shouldTryHotMessageSupplement(types.TopicCatGrp, opts, 9) {
t.Fatal("expected hot message supplement for partial recent group range")
}
if shouldTryHotMessageSupplement(types.TopicCatGrp, opts, 13) {
t.Fatal("did not expect supplement when range is complete")
}
if shouldTryHotMessageSupplement(types.TopicCatP2P, opts, 9) {
t.Fatal("did not expect supplement for p2p topic")
}
if shouldTryHotMessageSupplement(types.TopicCatGrp, &types.QueryOpt{Since: 41, Before: 54, Limit: 20, IdRanges: []types.Range{{Low: 41, Hi: 54}}}, 9) {
t.Fatal("did not expect supplement for explicit id ranges")
}
}
func TestMergeHotMessageSupplementFillsRecentRangeGaps(t *testing.T) {
opts := &types.QueryOpt{Since: 41, Before: 54, Limit: 20}
messages := []types.Message{
{SeqId: 53, Content: "db-53"},
{SeqId: 52, Content: "db-52"},
{SeqId: 51, Content: "db-51"},
{SeqId: 50, Content: "db-50"},
{SeqId: 49, Content: "db-49"},
{SeqId: 47, Content: "db-47"},
{SeqId: 46, Content: "db-46"},
{SeqId: 42, Content: "db-42"},
{SeqId: 41, Content: "db-41"},
}
hotMessages := make([]types.Message, 0, 13)
for seq := int64(53); seq >= 41; seq-- {
hotMessages = append(hotMessages, types.Message{SeqId: seq, Content: "hot"})
}
merged := mergeHotMessageSupplement(messages, hotMessages, opts)
if len(merged) != 13 {
t.Fatalf("merged len = %d, want 13: %#v", len(merged), merged)
}
for i, msg := range merged {
wantSeq := int64(53 - i)
if msg.SeqId != wantSeq {
t.Fatalf("merged[%d].SeqId = %d, want %d", i, msg.SeqId, wantSeq)
}
if msg.SeqId == 53 && msg.Content != "db-53" {
t.Fatalf("database message should win duplicate seqs, got %#v", msg.Content)
}
}
}
func TestFilterHotMessagesForUserSkipsDeleted(t *testing.T) { func TestFilterHotMessagesForUserSkipsDeleted(t *testing.T) {
uid := types.Uid(1) uid := types.Uid(1)
now := time.Now() now := time.Now()
@@ -3794,6 +3843,30 @@ func TestMessageAttachmentURLsDeduplicatesExplicitAndContentFileID(t *testing.T)
} }
} }
func TestMessageAttachmentURLsFindsDraftyEntityRefs(t *testing.T) {
msg := &ClientComMessage{
Pub: &MsgClientPub{
Content: map[string]any{
"txt": "video",
"ent": []any{
map[string]any{
"tp": "EX",
"data": map[string]any{
"mime": "video/mp4",
"ref": "/v0/file/s/KJ0Qlrk4Gzw.mp4",
},
},
},
},
},
}
attachments := messageAttachmentURLs(msg)
if len(attachments) != 1 || attachments[0] != "/v0/file/s/KJ0Qlrk4Gzw" {
t.Fatalf("unexpected Drafty attachments: %#v", attachments)
}
}
func TestHandleBroadcastInfoChannelWithMultipleReaders(t *testing.T) { func TestHandleBroadcastInfoChannelWithMultipleReaders(t *testing.T) {
topicName := "grpTest" topicName := "grpTest"
chanName := "chnTest" chanName := "chnTest"