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 (
"database/sql"
"os"
"strings"
"testing"
@@ -150,3 +151,23 @@ func TestTenantShardSQL(t *testing.T) {
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=?)`,
id, now, decodeUID(fid), file.Topic, id, decodeUID(fid), file.Topic)
} else {
_, err = tx.ExecContext(ctx, `INSERT INTO im_filemsglinks(createdat,fileid,topic)
SELECT ?,?,? WHERE NOT EXISTS (
SELECT 1 FROM im_filemsglinks WHERE fileid=? AND topic=?)`,
now, decodeUID(fid), file.Topic, decodeUID(fid), file.Topic)
_, err = tx.ExecContext(ctx, `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=?)`,
id, now, decodeUID(fid), file.Topic, id, decodeUID(fid), file.Topic)
}
if err != nil {
return nil, err
+4
View File
@@ -2637,6 +2637,10 @@ func serveIMSystemNotificationRead(wrt http.ResponseWriter, req *http.Request) {
return
}
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)
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) {
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) {
+19 -1
View File
@@ -1411,7 +1411,8 @@ func (r *redisGroupMessageRuntime) hotSeqIDs(tenantID types.TenantID, topic stri
if err != nil {
return nil, false, err
}
if len(raw) < limit {
expected := hotSeqExpectedCount(opts, limit)
if len(raw) < expected {
return nil, false, nil
}
seqs := make([]int64, 0, len(raw))
@@ -1425,6 +1426,23 @@ func (r *redisGroupMessageRuntime) hotSeqIDs(tenantID types.TenantID, topic stri
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 {
if limit <= 0 {
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) {
oldRuntime := groupMsgRuntime
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 {
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}.
@@ -3595,6 +3602,28 @@ func shouldTryHotMessageFallback(cat types.TopicCat, lastID int64, opts *types.Q
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 {
if opts != nil && opts.Limit > 0 {
return opts.Limit
@@ -3602,6 +3631,36 @@ func hotMessageFallbackLimit(opts *types.QueryOpt) int {
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 {
if len(messages) == 0 {
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) {
uid := types.Uid(1)
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) {
topicName := "grpTest"
chanName := "chnTest"