feat: add meeting notes backend
This commit is contained in:
+436
-7
@@ -269,6 +269,14 @@ type testMeetingsStore struct {
|
||||
responseVersion int
|
||||
}
|
||||
|
||||
type testMeetingNotesStore struct {
|
||||
meetings []types.Meeting
|
||||
files []types.MeetingNoteFile
|
||||
query string
|
||||
cursor string
|
||||
limit int
|
||||
}
|
||||
|
||||
type testDailyReportsStore struct {
|
||||
report *types.DailyReport
|
||||
saved bool
|
||||
@@ -658,6 +666,43 @@ func (s *testMeetingsStore) Respond(_ types.TenantID, uid types.Uid, _ string, r
|
||||
return s.meeting, nil
|
||||
}
|
||||
|
||||
func (s *testMeetingNotesStore) List(_ types.TenantID, _ types.Uid, query, cursor string, limit int) ([]types.Meeting, string, error) {
|
||||
s.query = query
|
||||
s.cursor = cursor
|
||||
s.limit = limit
|
||||
return s.meetings, "note-next", nil
|
||||
}
|
||||
|
||||
func (s *testMeetingNotesStore) ListFiles(_ types.TenantID, _ types.Uid, _ string) ([]types.MeetingNoteFile, error) {
|
||||
return s.files, nil
|
||||
}
|
||||
|
||||
func (s *testMeetingNotesStore) CreateFile(_ types.TenantID, uid types.Uid, meetingID, noteFileID, fileID, idempotencyKey string) (*types.MeetingNoteFile, error) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
return &types.MeetingNoteFile{
|
||||
NoteFileID: noteFileID, MeetingID: meetingID, FileID: fileID, UploaderID: uid,
|
||||
FileName: fileID, Status: types.MeetingNoteFileStatusActive, PreviewStatus: "ready", PreviewMode: "native",
|
||||
IdempotencyKey: idempotencyKey, Version: 1, CreatedAt: now, UpdatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *testMeetingNotesStore) ReplaceFile(_ types.TenantID, uid types.Uid, meetingID, noteFileID, fileID, idempotencyKey string, version int) (*types.MeetingNoteFile, error) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
return &types.MeetingNoteFile{
|
||||
NoteFileID: noteFileID, MeetingID: meetingID, FileID: fileID, UploaderID: uid,
|
||||
FileName: fileID, Status: types.MeetingNoteFileStatusActive, PreviewStatus: "ready", PreviewMode: "native",
|
||||
IdempotencyKey: idempotencyKey, Version: version + 1, CreatedAt: now, UpdatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *testMeetingNotesStore) DeleteFile(_ types.TenantID, uid types.Uid, meetingID, noteFileID, _ string, version int) (*types.MeetingNoteFile, error) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
return &types.MeetingNoteFile{
|
||||
NoteFileID: noteFileID, MeetingID: meetingID, UploaderID: uid,
|
||||
Status: types.MeetingNoteFileStatusDeleted, Version: version + 1, DeletedBy: uid, DeletedAt: &now, CreatedAt: now, UpdatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *testDailyReportsStore) List(_ types.TenantID, _ types.Uid, _, _, _, _, _ string, _ int) ([]types.DailyReport, string, error) {
|
||||
if s.report == nil {
|
||||
return nil, "", nil
|
||||
@@ -1772,6 +1817,51 @@ func TestServeIMPublicUploadsAllowsGroupAvatar(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMPublicUploadsAllowsUserAvatar(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
oldStore := store.Store
|
||||
oldTenantEntry := store.TenantEntry
|
||||
entry := &testTenantEntryStore{}
|
||||
handler := &testPublicMediaHandler{}
|
||||
mockStore := mock_store.NewMockPersistentStorageInterface(ctrl)
|
||||
mockStore.EXPECT().GetMediaHandler().Return(handler).AnyTimes()
|
||||
store.Store = mockStore
|
||||
store.TenantEntry = entry
|
||||
defer func() {
|
||||
store.Store = oldStore
|
||||
store.TenantEntry = oldTenantEntry
|
||||
}()
|
||||
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
if err := writer.WriteField("purpose", types.PublicUploadPurposeUserAvatar); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
part, err := writer.CreateFormFile("file", "avatar.png")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := part.Write([]byte("\x89PNG\r\n\x1a\n" + strings.Repeat("\x00", 64))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/public/uploads", &body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMPublicUploads(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if entry.upload == nil || entry.upload.Purpose != types.PublicUploadPurposeUserAvatar {
|
||||
t.Fatalf("unexpected upload: %#v", entry.upload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMVerificationCodesCreatesChallenge(t *testing.T) {
|
||||
oldTenants := store.Tenants
|
||||
oldVerifications := store.Verifications
|
||||
@@ -2733,6 +2823,61 @@ func TestServeIMMeetingsList(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMeetingGetResolvesDisplayNames(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
viewer := types.Uid(42)
|
||||
attendee := types.Uid(43)
|
||||
expectIMBearer(t, ctrl, "token-1", 7, viewer)
|
||||
oldMeetings := store.Meetings
|
||||
oldUsers := store.Users
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
meetings := &testMeetingsStore{meeting: &types.Meeting{
|
||||
MeetingID: "mtg_test",
|
||||
OrganizerID: viewer,
|
||||
Title: "周会",
|
||||
StartAt: now.Add(time.Hour),
|
||||
EndAt: now.Add(2 * time.Hour),
|
||||
State: types.MeetingStateScheduled,
|
||||
Version: 1,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Participants: []types.MeetingParticipant{
|
||||
{MeetingID: "mtg_test", UserID: viewer, Response: types.MeetingResponseAccepted},
|
||||
{MeetingID: "mtg_test", UserID: attendee, Response: types.MeetingResponsePending},
|
||||
},
|
||||
}}
|
||||
store.Meetings = meetings
|
||||
users := mock_store.NewMockUsersPersistenceInterface(ctrl)
|
||||
organizer := types.User{Public: map[string]any{"fn": "张三"}}
|
||||
organizer.SetUid(viewer)
|
||||
participant := types.User{Public: map[string]any{"fn": "李四"}}
|
||||
participant.SetUid(attendee)
|
||||
users.EXPECT().GetAll(types.TenantID(7), viewer, attendee).Return([]types.User{organizer, participant}, nil)
|
||||
store.Users = users
|
||||
defer func() {
|
||||
store.Meetings = oldMeetings
|
||||
store.Users = oldUsers
|
||||
}()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/meetings/mtg_test", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMMeetingResource(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["organizer_display_name"] != "张三" {
|
||||
t.Fatalf("unexpected organizer display name: %#v", data)
|
||||
}
|
||||
participants := data["participants"].([]any)
|
||||
if participants[0].(map[string]any)["display_name"] != "张三" || participants[1].(map[string]any)["display_name"] != "李四" {
|
||||
t.Fatalf("unexpected participant names: %#v", participants)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMeetingMyResponse(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -2772,6 +2917,100 @@ func TestServeIMMeetingMyResponse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMeetingNotesListAndDetail(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
viewer := types.Uid(42)
|
||||
expectIMBearer(t, ctrl, "token-1", 7, viewer)
|
||||
expectIMBearer(t, ctrl, "token-1", 7, viewer)
|
||||
oldMeetings := store.Meetings
|
||||
oldMeetingNotes := store.MeetingNotes
|
||||
oldUsers := store.Users
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
meeting := types.Meeting{
|
||||
MeetingID: "mtg_test",
|
||||
OrganizerID: viewer,
|
||||
RecorderID: viewer,
|
||||
Title: "复盘会",
|
||||
StartAt: now.Add(-2 * time.Hour),
|
||||
EndAt: now.Add(-time.Hour),
|
||||
State: types.MeetingStateEnded,
|
||||
Version: 2,
|
||||
CreatedAt: now.Add(-3 * time.Hour),
|
||||
UpdatedAt: now,
|
||||
Participants: []types.MeetingParticipant{
|
||||
{MeetingID: "mtg_test", UserID: viewer, Response: types.MeetingResponseAccepted},
|
||||
},
|
||||
}
|
||||
file := types.MeetingNoteFile{
|
||||
NoteFileID: "mnote_test", MeetingID: "mtg_test", FileID: "file_test", UploaderID: viewer,
|
||||
FileName: "file_test", MIMEType: "application/pdf", FileSize: 123, Status: types.MeetingNoteFileStatusActive,
|
||||
PreviewStatus: "ready", PreviewMode: "native", Version: 1, CreatedAt: now, UpdatedAt: now,
|
||||
}
|
||||
store.Meetings = &testMeetingsStore{meeting: &meeting}
|
||||
notes := &testMeetingNotesStore{meetings: []types.Meeting{meeting}, files: []types.MeetingNoteFile{file}}
|
||||
store.MeetingNotes = notes
|
||||
store.Users = nil
|
||||
defer func() {
|
||||
store.Meetings = oldMeetings
|
||||
store.MeetingNotes = oldMeetingNotes
|
||||
store.Users = oldUsers
|
||||
}()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/meeting-notes?query=%E5%A4%8D%E7%9B%98&limit=20", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMMeetingNotes(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("list status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
items := data["items"].([]any)
|
||||
item := items[0].(map[string]any)
|
||||
if notes.query != "复盘" || item["note_status"] != types.MeetingNoteStatusAvailable || item["file_count"].(float64) != 1 || data["has_more"] != true {
|
||||
t.Fatalf("unexpected meeting notes list: data=%#v store=%#v", data, notes)
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/v1/im/meetings/mtg_test/notes", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec = httptest.NewRecorder()
|
||||
serveIMMeetingResource(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("detail status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
data = decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
files := data["files"].([]any)
|
||||
if data["recorder_user_id"] != viewer.UserId() || len(files) != 1 || files[0].(map[string]any)["file_id"] != "file_test" {
|
||||
t.Fatalf("unexpected meeting notes detail: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMeetingNoteFilesCreate(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
viewer := types.Uid(42)
|
||||
mockStore := expectIMBearer(t, ctrl, "token-1", 7, viewer)
|
||||
mockStore.EXPECT().GetUidString().Return("note-1")
|
||||
oldMeetingNotes := store.MeetingNotes
|
||||
store.MeetingNotes = &testMeetingNotesStore{}
|
||||
defer func() {
|
||||
store.MeetingNotes = oldMeetingNotes
|
||||
}()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/meetings/mtg_test/notes/files", strings.NewReader(`{"file_id":"file_test"}`))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
req.Header.Set("Idempotency-Key", "note-file-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMMeetingResource(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["note_file_id"] != "mnote_note-1" || data["file_id"] != "file_test" || data["version"].(float64) != 1 {
|
||||
t.Fatalf("unexpected note file payload: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMDailyReportsCreateAndList(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -3697,9 +3936,23 @@ func TestServeIMGroupList(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldUsers := store.Users
|
||||
oldGroupBusiness := store.GroupBusiness
|
||||
users := mock_store.NewMockUsersPersistenceInterface(ctrl)
|
||||
groupBusiness := &testGroupBusinessStore{settings: &types.GroupSettings{
|
||||
Topic: "grpAlpha",
|
||||
OwnerUserID: types.Uid(42),
|
||||
Name: "Alpha",
|
||||
AvatarFileID: "file-1",
|
||||
MemberCount: 3,
|
||||
Version: 1,
|
||||
UpdatedAt: time.Date(2026, 8, 12, 10, 0, 0, 0, time.UTC),
|
||||
}}
|
||||
store.Users = users
|
||||
defer func() { store.Users = oldUsers }()
|
||||
store.GroupBusiness = groupBusiness
|
||||
defer func() {
|
||||
store.Users = oldUsers
|
||||
store.GroupBusiness = oldGroupBusiness
|
||||
}()
|
||||
sub := types.Subscription{
|
||||
TenantID: 7,
|
||||
User: types.Uid(42).String(),
|
||||
@@ -3708,8 +3961,8 @@ func TestServeIMGroupList(t *testing.T) {
|
||||
ModeGiven: types.ModeCFull,
|
||||
ObjHeader: types.ObjHeader{UpdatedAt: time.Date(2026, 8, 12, 9, 0, 0, 0, time.UTC)},
|
||||
}
|
||||
sub.SetPublic(map[string]any{"name": "Alpha", "avatar_file_id": "file-1"})
|
||||
sub.SetSubCnt(3)
|
||||
sub.SetPublic(map[string]any{"name": "grpAlpha"})
|
||||
sub.SetSubCnt(0)
|
||||
users.EXPECT().GetSubs(types.TenantID(7), types.Uid(42)).Return([]types.Subscription{sub}, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/groups?category=owned&query=alp", nil)
|
||||
@@ -3723,7 +3976,7 @@ func TestServeIMGroupList(t *testing.T) {
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
items := data["items"].([]any)
|
||||
if len(items) != 1 || items[0].(map[string]any)["name"] != "Alpha" || items[0].(map[string]any)["my_role"] != "owner" {
|
||||
if len(items) != 1 || items[0].(map[string]any)["name"] != "Alpha" || items[0].(map[string]any)["member_count"].(float64) != 3 || items[0].(map[string]any)["my_role"] != "owner" {
|
||||
t.Fatalf("unexpected group list response: %#v", data)
|
||||
}
|
||||
}
|
||||
@@ -4086,7 +4339,8 @@ func TestServeIMGroupLeave(t *testing.T) {
|
||||
func TestServeIMGroupInvitationAddsNonMember(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
mockStore := expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
mockStore.EXPECT().GetUidString().Return("test")
|
||||
oldSubs := store.Subs
|
||||
oldGroupBusiness := store.GroupBusiness
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
@@ -4102,6 +4356,7 @@ func TestServeIMGroupInvitationAddsNonMember(t *testing.T) {
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpAlpha", owner, false).Return(&types.Subscription{
|
||||
TenantID: 7, User: owner.String(), Topic: "grpAlpha", ModeWant: types.ModeCFull, ModeGiven: types.ModeCFull,
|
||||
}, nil)
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpAlpha", target, false).Return(nil, types.ErrNotFound)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/groups/grpAlpha/invitations",
|
||||
bytes.NewBufferString(`{"user_ids":["`+target.UserId()+`"],"message":"welcome"}`))
|
||||
@@ -4115,10 +4370,10 @@ func TestServeIMGroupInvitationAddsNonMember(t *testing.T) {
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
items := data["items"].([]any)
|
||||
if len(items) != 1 || items[0].(map[string]any)["status"] != "joined" {
|
||||
if len(items) != 1 || items[0].(map[string]any)["status"] != types.GroupInvitationStatusPending {
|
||||
t.Fatalf("unexpected invitation response: %#v", data)
|
||||
}
|
||||
if len(groupStore.invitations) != 1 || groupStore.invitations[0].TargetUserID != target {
|
||||
if len(groupStore.invitations) != 1 || groupStore.invitations[0].TargetUserID != target || groupStore.invitations[0].InvitationID != "ginv_test" {
|
||||
t.Fatalf("invitation was not persisted: %#v", groupStore.invitations)
|
||||
}
|
||||
}
|
||||
@@ -4461,6 +4716,8 @@ func TestServeIMFavoritesListDetailBatchDelete(t *testing.T) {
|
||||
Return(&auth.Rec{TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth}, nil, nil).Times(3)
|
||||
store.Store = mockStore
|
||||
oldFavorites := store.Favorites
|
||||
oldSubs := store.Subs
|
||||
oldMessages := store.Messages
|
||||
now := time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC)
|
||||
favorites := &testFavoritesStore{items: []types.FavoriteItem{{
|
||||
TenantID: 7,
|
||||
@@ -4474,11 +4731,36 @@ func TestServeIMFavoritesListDetailBatchDelete(t *testing.T) {
|
||||
CollectedAt: now,
|
||||
}}}
|
||||
store.Favorites = favorites
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
messages := mock_store.NewMockMessagesPersistenceInterface(ctrl)
|
||||
store.Subs = subs
|
||||
store.Messages = messages
|
||||
defer func() {
|
||||
store.Store = oldStore
|
||||
store.Favorites = oldFavorites
|
||||
store.Subs = oldSubs
|
||||
store.Messages = oldMessages
|
||||
}()
|
||||
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpAlpha", types.Uid(42), false).Return(&types.Subscription{
|
||||
TenantID: 7,
|
||||
User: types.Uid(42).String(),
|
||||
Topic: "grpAlpha",
|
||||
ModeWant: types.ModeCPublic,
|
||||
ModeGiven: types.ModeCPublic,
|
||||
}, nil).Times(2)
|
||||
messages.EXPECT().GetOne(types.TenantID(7), "grpAlpha", int64(3)).Return(&types.Message{
|
||||
TenantID: 7,
|
||||
Topic: "grpAlpha",
|
||||
SeqId: 3,
|
||||
From: types.Uid(42).String(),
|
||||
Head: types.KVMap{"type": "text"},
|
||||
Content: map[string]any{"text": "hello"},
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
},
|
||||
}, nil).Times(2)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/favorites?type=text", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -4516,6 +4798,7 @@ func TestServeIMFavoriteBatchCreateCountsExisting(t *testing.T) {
|
||||
mockStore.EXPECT().GetUidString().Return("new-fav")
|
||||
|
||||
oldFavorites := store.Favorites
|
||||
oldSubs := store.Subs
|
||||
oldMessages := store.Messages
|
||||
now := time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC)
|
||||
store.Favorites = &testFavoritesStore{items: []types.FavoriteItem{{
|
||||
@@ -4529,13 +4812,23 @@ func TestServeIMFavoriteBatchCreateCountsExisting(t *testing.T) {
|
||||
Availability: "available",
|
||||
CollectedAt: now,
|
||||
}}}
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
messages := mock_store.NewMockMessagesPersistenceInterface(ctrl)
|
||||
store.Subs = subs
|
||||
store.Messages = messages
|
||||
t.Cleanup(func() {
|
||||
store.Favorites = oldFavorites
|
||||
store.Subs = oldSubs
|
||||
store.Messages = oldMessages
|
||||
})
|
||||
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpAlpha", types.Uid(42), false).Return(&types.Subscription{
|
||||
TenantID: 7,
|
||||
User: types.Uid(42).String(),
|
||||
Topic: "grpAlpha",
|
||||
ModeWant: types.ModeCPublic,
|
||||
ModeGiven: types.ModeCPublic,
|
||||
}, nil)
|
||||
messages.EXPECT().GetOne(types.TenantID(7), "grpAlpha", int64(3)).Return(&types.Message{
|
||||
TenantID: 7,
|
||||
Topic: "grpAlpha",
|
||||
@@ -4561,6 +4854,104 @@ func TestServeIMFavoriteBatchCreateCountsExisting(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMFavoriteBatchCreateRejectsUnreadableSource(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
|
||||
oldFavorites := store.Favorites
|
||||
oldSubs := store.Subs
|
||||
oldMessages := store.Messages
|
||||
store.Favorites = &testFavoritesStore{}
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
messages := mock_store.NewMockMessagesPersistenceInterface(ctrl)
|
||||
store.Subs = subs
|
||||
store.Messages = messages
|
||||
t.Cleanup(func() {
|
||||
store.Favorites = oldFavorites
|
||||
store.Subs = oldSubs
|
||||
store.Messages = oldMessages
|
||||
})
|
||||
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpSecret", types.Uid(42), false).Return(nil, types.ErrNotFound)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/favorites", bytes.NewBufferString(`{"messages":[{"topic":"grpSecret","seq":"1"}]}`))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMFavorites(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("create status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["created_count"].(float64) != 0 || data["existing_count"].(float64) != 0 {
|
||||
t.Fatalf("unexpected favorite counts: %#v", data)
|
||||
}
|
||||
failed := data["failed"].([]any)
|
||||
if len(failed) != 1 || failed[0].(map[string]any)["reason"] != "NOT_FOUND" {
|
||||
t.Fatalf("unexpected failed response: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMFavoriteBatchCreateNormalizesP2PUserTopic(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockStore := expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
mockStore.EXPECT().GetUidString().Return("new-fav")
|
||||
|
||||
actor := types.Uid(42)
|
||||
peer := types.Uid(43)
|
||||
normalizedTopic := actor.P2PName(peer)
|
||||
oldFavorites := store.Favorites
|
||||
oldSubs := store.Subs
|
||||
oldMessages := store.Messages
|
||||
favorites := &testFavoritesStore{}
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
messages := mock_store.NewMockMessagesPersistenceInterface(ctrl)
|
||||
store.Favorites = favorites
|
||||
store.Subs = subs
|
||||
store.Messages = messages
|
||||
t.Cleanup(func() {
|
||||
store.Favorites = oldFavorites
|
||||
store.Subs = oldSubs
|
||||
store.Messages = oldMessages
|
||||
})
|
||||
|
||||
now := time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC)
|
||||
subs.EXPECT().Get(types.TenantID(7), normalizedTopic, actor, false).Return(&types.Subscription{
|
||||
TenantID: 7,
|
||||
User: actor.String(),
|
||||
Topic: normalizedTopic,
|
||||
ModeWant: types.ModeCPublic,
|
||||
ModeGiven: types.ModeCPublic,
|
||||
}, nil)
|
||||
messages.EXPECT().GetOne(types.TenantID(7), normalizedTopic, int64(3)).Return(&types.Message{
|
||||
TenantID: 7,
|
||||
Topic: normalizedTopic,
|
||||
SeqId: 3,
|
||||
From: peer.String(),
|
||||
Head: types.KVMap{"type": "text"},
|
||||
Content: map[string]any{"text": "hello"},
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
},
|
||||
}, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/favorites", bytes.NewBufferString(`{"messages":[{"topic":"`+peer.UserId()+`","seq":"3"}]}`))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMFavorites(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("create status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["created_count"].(float64) != 1 || data["existing_count"].(float64) != 0 {
|
||||
t.Fatalf("unexpected favorite counts: %#v", data)
|
||||
}
|
||||
if favorites.created == nil || favorites.created.SourceTopic != normalizedTopic {
|
||||
t.Fatalf("favorite source topic was not normalized: %#v", favorites.created)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMFavoriteForwardSavesMessages(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -5149,6 +5540,14 @@ func TestServeIMAbuseReportsAndReasons(t *testing.T) {
|
||||
t.Fatalf("abuse reason scope mismatch: %#v", safety)
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/v1/im/reports/abuse/reasons?target_type=group_member", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec = httptest.NewRecorder()
|
||||
serveIMAbuseReasons(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("unsupported reasons status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodPost, "/v1/im/reports/abuse",
|
||||
bytes.NewBufferString(`{"target":{"type":"user","user_id":"usrKwAAAAAAAAA"},"reason_code":"fraud","description":"疑似诈骗","evidence_file_ids":["file1"]}`))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
@@ -5306,6 +5705,36 @@ func TestServeIMTopicReadActionsNormalizeP2PUserTopic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageSearchPayloadMatchesUseSnippetRuneOffsets(t *testing.T) {
|
||||
msg := types.Message{
|
||||
TenantID: 7,
|
||||
Topic: "grpAlpha",
|
||||
SeqId: 1,
|
||||
Version: 2,
|
||||
From: types.Uid(42).String(),
|
||||
Head: types.KVMap{"type": "text"},
|
||||
Content: map[string]any{"txt": "你们已成为好友,现在可以开始聊天了。", "fmt": []any{}},
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
payload := messageSearchPayload(msg, "好友")
|
||||
if payload["snippet"] != "你们已成为好友,现在可以开始聊天了。" {
|
||||
t.Fatalf("unexpected snippet: %#v", payload["snippet"])
|
||||
}
|
||||
matches := payload["matches"].([]map[string]any)
|
||||
if len(matches) != 1 || matches[0]["start"] != 5 || matches[0]["end"] != 7 {
|
||||
t.Fatalf("unexpected matches: %#v", matches)
|
||||
}
|
||||
if payload["version"] != 2 {
|
||||
t.Fatalf("unexpected version: %#v", payload["version"])
|
||||
}
|
||||
snapshot := payload["snapshot"].(map[string]any)
|
||||
if snapshot["text"] != "你们已成为好友,现在可以开始聊天了。" || snapshot["version"] != 2 {
|
||||
t.Fatalf("unexpected snapshot: %#v", snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMTopicCalls(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
Reference in New Issue
Block a user