From 06e08a65e4e8bde4ea98cbd6c40348637988490a Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Wed, 30 Sep 2026 10:50:35 +0800 Subject: [PATCH] fix: handle opaque attachment references --- loadtest/im-pressure/dbverify.go | 36 ++++++++++++++++++--------- loadtest/im-pressure/dbverify_test.go | 24 ++++++++++++++++++ server/store/file_test.go | 30 ++++++++++++++++++++++ server/store/store.go | 12 +++++++-- 4 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 loadtest/im-pressure/dbverify_test.go create mode 100644 server/store/file_test.go diff --git a/loadtest/im-pressure/dbverify.go b/loadtest/im-pressure/dbverify.go index a242938..9039363 100644 --- a/loadtest/im-pressure/dbverify.go +++ b/loadtest/im-pressure/dbverify.go @@ -41,8 +41,9 @@ func runDBVerify(ctx context.Context, coordinatorPath, output, tenantCode, dsnEn return err } var coordinator struct { - Start StartInfo `json:"start"` - Ledgers map[string][]AcceptedMessage `json:"ledgers"` + Start StartInfo `json:"start"` + Ledgers map[string][]AcceptedMessage `json:"ledgers"` + Observed map[string][]AcceptedMessage `json:"observed"` } if err = json.Unmarshal(raw, &coordinator); err != nil { return fmt.Errorf("decode coordinator: %w", err) @@ -77,17 +78,11 @@ func runDBVerify(ctx context.Context, coordinatorPath, output, tenantCode, dsnEn return err } - expected := make(map[string]AcceptedMessage) + expected := expectedDBMessages(coordinator.Start.RunID, coordinator.Ledgers, coordinator.Observed) months := make(map[string]struct{}) - for _, ledger := range coordinator.Ledgers { - for _, message := range ledger { - if message.RunID != coordinator.Start.RunID { - continue - } - expected[message.ClientMid] = message - if message.Kind == "group" { - months[message.SentAt.UTC().Format("200601")] = struct{}{} - } + for _, message := range expected { + if message.Kind == "group" { + months[message.SentAt.UTC().Format("200601")] = struct{}{} } } tables := []string{fmt.Sprintf("im_%03d_messages", tenantID)} @@ -167,3 +162,20 @@ func runDBVerify(ctx context.Context, coordinatorPath, output, tenantCode, dsnEn } return nil } + +func expectedDBMessages(runID string, ledgers, observed map[string][]AcceptedMessage) map[string]AcceptedMessage { + all := make([]AcceptedMessage, 0) + for _, messages := range observed { + all = append(all, messages...) + } + for _, messages := range ledgers { + all = append(all, messages...) + } + expected := make(map[string]AcceptedMessage, len(all)) + for _, message := range mergeMessageLedgers(nil, all) { + if message.RunID == runID && message.ClientMid != "" { + expected[message.ClientMid] = message + } + } + return expected +} diff --git a/loadtest/im-pressure/dbverify_test.go b/loadtest/im-pressure/dbverify_test.go new file mode 100644 index 0000000..280a71c --- /dev/null +++ b/loadtest/im-pressure/dbverify_test.go @@ -0,0 +1,24 @@ +package main + +import "testing" + +func TestExpectedDBMessagesIncludesObservedWithoutAck(t *testing.T) { + ledgers := map[string][]AcceptedMessage{ + "a": {{RunID: "run-1", ClientMid: "acked", Topic: "grp-one", Seq: 1}}, + } + observed := map[string][]AcceptedMessage{ + "a": { + {RunID: "run-1", ClientMid: "unacked", Topic: "grp-one", Seq: 2}, + {RunID: "other", ClientMid: "foreign", Topic: "grp-one", Seq: 3}, + }, + "b": {{RunID: "run-1", ClientMid: "unacked", Topic: "grp-one", Seq: 2}}, + } + + expected := expectedDBMessages("run-1", ledgers, observed) + if len(expected) != 2 { + t.Fatalf("expected messages=%d want=2: %+v", len(expected), expected) + } + if expected["acked"].Seq != 1 || expected["unacked"].Seq != 2 { + t.Fatalf("unexpected merged messages: %+v", expected) + } +} diff --git a/server/store/file_test.go b/server/store/file_test.go new file mode 100644 index 0000000..b1515e0 --- /dev/null +++ b/server/store/file_test.go @@ -0,0 +1,30 @@ +package store + +import ( + "testing" + + "github.com/tinode/chat/server/store/types" +) + +func TestFileLinkUserIDAllowsOpaqueTopicReferences(t *testing.T) { + tests := []struct { + name string + topic string + isUser bool + wantUID types.Uid + }{ + {name: "cultivate reference", topic: "cvpbzxw1aio4ni8x0"}, + {name: "short opaque reference", topic: "x"}, + {name: "empty reference", topic: ""}, + {name: "user reference", topic: types.Uid(42).UserId(), isUser: true, wantUID: types.Uid(42)}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gotUID, gotIsUser := fileLinkUserID(test.topic) + if gotIsUser != test.isUser || gotUID != test.wantUID { + t.Fatalf("fileLinkUserID(%q) = (%v, %v), want (%v, %v)", test.topic, gotUID, gotIsUser, test.wantUID, test.isUser) + } + }) + } +} diff --git a/server/store/store.go b/server/store/store.go index e353a81..051380d 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -4684,8 +4684,8 @@ func (fileMapper) LinkAttachments(tenantID types.TenantID, topic string, msgId t return err } userId := types.ZeroUid - if topic != "" && types.GetTopicCat(topic) == types.TopicCatMe { - userId = types.ParseUserId(topic) + if parsedUserID, ok := fileLinkUserID(topic); ok { + userId = parsedUserID topic = "" } return tenantAdp.TenantFileLinkAttachments(tenantID, topic, userId, msgId, fids) @@ -4693,6 +4693,14 @@ func (fileMapper) LinkAttachments(tenantID types.TenantID, topic string, msgId t return nil } +// File link topics may also be opaque retention keys, such as cultivate's cvp prefix. +func fileLinkUserID(topic string) (types.Uid, bool) { + if !strings.HasPrefix(topic, "usr") { + return types.ZeroUid, false + } + return types.ParseUserId(topic), true +} + func (fileMapper) UserHasAccess(tenantID types.TenantID, fid string, uid types.Uid) (bool, error) { if fid == "" || uid.IsZero() { return false, types.ErrMalformed