fix im workbench and profile regressions
This commit is contained in:
+426
-19
@@ -307,6 +307,11 @@ type testMailboxStore struct {
|
||||
config *types.MailboxConfig
|
||||
submission *types.MailboxSubmission
|
||||
idempotencyKey string
|
||||
submitErr error
|
||||
}
|
||||
|
||||
type testUserSecurityStore struct {
|
||||
unreadByPrefix map[string]int
|
||||
}
|
||||
|
||||
type testProfilesStore struct {
|
||||
@@ -342,6 +347,7 @@ type testGroupBusinessStore struct {
|
||||
member *types.GroupMember
|
||||
joinRequests []types.GroupJoinRequest
|
||||
announcement *types.GroupAnnouncement
|
||||
announcements map[string]*types.GroupAnnouncement
|
||||
mutes []types.GroupMute
|
||||
files []types.GroupFile
|
||||
invitations []types.GroupInvitation
|
||||
@@ -472,6 +478,18 @@ func (s *testGroupBusinessStore) LatestAnnouncement(_ types.TenantID, topic stri
|
||||
return s.announcement, nil
|
||||
}
|
||||
|
||||
func (s *testGroupBusinessStore) GetAnnouncement(_ types.TenantID, _ string, announcementID string) (*types.GroupAnnouncement, error) {
|
||||
if s.announcements != nil {
|
||||
if ann := s.announcements[announcementID]; ann != nil {
|
||||
return ann, nil
|
||||
}
|
||||
}
|
||||
if s.announcement != nil && s.announcement.AnnouncementID == announcementID {
|
||||
return s.announcement, nil
|
||||
}
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *testGroupBusinessStore) PutAnnouncement(_ types.TenantID, ann *types.GroupAnnouncement, _ int) (*types.GroupAnnouncement, error) {
|
||||
ann.Version = 2
|
||||
if ann.PublishedAt.IsZero() {
|
||||
@@ -906,6 +924,9 @@ func (s *testMailboxStore) Config(_ types.TenantID, _ types.Uid) (*types.Mailbox
|
||||
}
|
||||
|
||||
func (s *testMailboxStore) Submit(_ types.TenantID, uid types.Uid, submission *types.MailboxSubmission, idempotencyKey string) (*types.MailboxSubmission, error) {
|
||||
if s.submitErr != nil {
|
||||
return nil, s.submitErr
|
||||
}
|
||||
s.idempotencyKey = idempotencyKey
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
submission.SenderID = uid
|
||||
@@ -917,6 +938,81 @@ func (s *testMailboxStore) Submit(_ types.TenantID, uid types.Uid, submission *t
|
||||
return submission, nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) GetAuthVersion(_ types.TenantID, _ types.Uid) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) BumpAuthVersion(_ types.TenantID, _ types.Uid, _ time.Time) (int, error) {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) GetRealName(_ types.TenantID, _ types.Uid) (*types.RealNameVerification, error) {
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) CreateRealNameSubmission(_ types.TenantID, _ *types.RealNameVerification) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) CreateRealNameAppeal(_ types.TenantID, appeal *types.RealNameAppeal) (*types.RealNameAppeal, error) {
|
||||
return appeal, nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) CreateActionToken(_ types.TenantID, _ *types.ActionToken) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) ConsumeActionToken(_ types.TenantID, _ types.Uid, _, _, _ string, _ time.Time) (*types.ActionToken, error) {
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) GetIndependentPassword(_ types.TenantID, _ types.Uid, _ string) (*types.IndependentPassword, error) {
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) UpsertIndependentPassword(_ types.TenantID, _ *types.IndependentPassword) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) UpdateIndependentPasswordAttempts(_ types.TenantID, _ types.Uid, _ string, _ int, _ *time.Time, _ time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) GetYouthMode(_ types.TenantID, _ types.Uid) (*types.YouthModeSettings, error) {
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) SetYouthMode(_ types.TenantID, uid types.Uid, enabled bool, policyVersion int, now time.Time) (*types.YouthModeSettings, error) {
|
||||
return &types.YouthModeSettings{UserID: uid, Enabled: enabled, PolicyVersion: policyVersion, UpdatedAt: now}, nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) CreateSystemNotification(_ types.TenantID, _ *types.SystemNotification) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) ListSystemNotifications(_ types.TenantID, _ types.Uid, _ string, _ int) ([]types.SystemNotification, string, error) {
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) CountUnreadSystemNotifications(_ types.TenantID, _ types.Uid) (int, error) {
|
||||
total := 0
|
||||
for _, count := range s.unreadByPrefix {
|
||||
total += count
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) CountUnreadSystemNotificationsByEventPrefix(_ types.TenantID, _ types.Uid, prefix string) (int, error) {
|
||||
if s.unreadByPrefix == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return s.unreadByPrefix[prefix], nil
|
||||
}
|
||||
|
||||
func (s *testUserSecurityStore) MarkSystemNotificationRead(_ types.TenantID, _ types.Uid, _ string, _ time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testProfilesStore) Get(_ types.TenantID, uid types.Uid) (*types.UserProfile, error) {
|
||||
if s.profile == nil {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
@@ -2577,14 +2673,18 @@ func TestServeIMWorkbench(t *testing.T) {
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldWorkbench := store.Workbench
|
||||
oldTenants := store.Tenants
|
||||
oldSecurity := store.Security
|
||||
store.Workbench = &testWorkbenchStore{version: 3, modules: []types.WorkbenchModule{
|
||||
{Code: "organization", Name: "部门框架", Route: "/organization", SortOrder: 40, Enabled: true, Permission: "enabled"},
|
||||
{Code: "meetings", Name: "会议", Route: "/meetings", SortOrder: 41, Enabled: true, Permission: "enabled"},
|
||||
{Code: "external", Name: "外部链接", Route: "https://example.com", Enabled: true, Permission: "enabled"},
|
||||
}}
|
||||
store.Tenants = testTenantStore{tenant: &types.Tenant{ID: 7, Code: "acme", State: types.TenantStateActive}}
|
||||
store.Security = &testUserSecurityStore{unreadByPrefix: map[string]int{"meeting.": 4}}
|
||||
defer func() {
|
||||
store.Workbench = oldWorkbench
|
||||
store.Tenants = oldTenants
|
||||
store.Security = oldSecurity
|
||||
}()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/workbench", nil)
|
||||
@@ -2601,13 +2701,17 @@ func TestServeIMWorkbench(t *testing.T) {
|
||||
t.Fatalf("unexpected layout version: %#v", data)
|
||||
}
|
||||
items := data["modules"].([]any)
|
||||
if len(items) != 1 {
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("expected external route to be filtered, got %#v", items)
|
||||
}
|
||||
item := items[0].(map[string]any)
|
||||
if item["code"] != "organization" || item["route"] != "/organization" || item["sort_order"].(float64) != 40 {
|
||||
t.Fatalf("unexpected module: %#v", item)
|
||||
}
|
||||
meetingItem := items[1].(map[string]any)
|
||||
if meetingItem["code"] != "meetings" || meetingItem["unread_count"].(float64) != 4 {
|
||||
t.Fatalf("workbench unread count not populated: %#v", meetingItem)
|
||||
}
|
||||
enterprise := data["enterprise"].(map[string]any)
|
||||
if enterprise["tenant_code"] != "acme" {
|
||||
t.Fatalf("tenant_code: got %#v, want acme", enterprise["tenant_code"])
|
||||
@@ -2811,10 +2915,31 @@ func TestServeIMMeetingsCreate(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldKafkaBus := kafkaBus
|
||||
oldPublisher := publishKafkaAuditEventEnvelope
|
||||
oldSecurity := store.Security
|
||||
oldInfrastructure := store.HTTPInfrastructure
|
||||
oldMeetings := store.Meetings
|
||||
meetings := &testMeetingsStore{}
|
||||
security := &testSecurityStore{}
|
||||
infrastructure := &testHTTPInfrastructureStore{}
|
||||
var auditPublished bool
|
||||
kafkaBus = &kafkaRuntime{}
|
||||
publishKafkaAuditEventEnvelope = func(_ context.Context, topicName, key string, envelope kafkaEnvelope) error {
|
||||
auditPublished = topicName == "audit_event" && key == envelope.BizID &&
|
||||
envelope.EventType == "audit.meeting.created" && envelope.BizType == "audit"
|
||||
return nil
|
||||
}
|
||||
store.Security = security
|
||||
store.HTTPInfrastructure = infrastructure
|
||||
store.Meetings = meetings
|
||||
defer func() { store.Meetings = oldMeetings }()
|
||||
defer func() {
|
||||
kafkaBus = oldKafkaBus
|
||||
publishKafkaAuditEventEnvelope = oldPublisher
|
||||
store.Security = oldSecurity
|
||||
store.HTTPInfrastructure = oldInfrastructure
|
||||
store.Meetings = oldMeetings
|
||||
}()
|
||||
|
||||
body := `{"title":"周会","start_at":"2026-08-14T09:00:00Z","end_at":"2026-08-14T10:00:00Z","location":"A101","attendee_user_ids":["` + types.Uid(43).UserId() + `"],"reminder_minutes":[10]}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/meetings", strings.NewReader(body))
|
||||
@@ -2833,6 +2958,12 @@ func TestServeIMMeetingsCreate(t *testing.T) {
|
||||
if len(meetings.attendeeIDs) != 1 || data["state"] != types.MeetingStateScheduled || data["my_response"] != types.MeetingResponseAccepted {
|
||||
t.Fatalf("unexpected meeting response: %#v attendees=%#v", data, meetings.attendeeIDs)
|
||||
}
|
||||
if !auditPublished {
|
||||
t.Fatalf("meeting create should still publish audit event")
|
||||
}
|
||||
if len(security.notifications) != 0 || len(infrastructure.events) != 0 {
|
||||
t.Fatalf("meeting create HTTP layer must not duplicate workbench notifications/events: notifications=%#v events=%#v", security.notifications, infrastructure.events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMeetingsList(t *testing.T) {
|
||||
@@ -3485,6 +3616,53 @@ func TestServeIMMailboxSubmission(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMailboxSubmissionRejectsTooLongContent(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldMailbox := store.Mailbox
|
||||
store.Mailbox = &testMailboxStore{}
|
||||
defer func() { store.Mailbox = oldMailbox }()
|
||||
|
||||
body := `{"subject":"建议","content":"` + strings.Repeat("a", 5001) + `","notice_version":"v1"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/executive-mailbox/submissions", strings.NewReader(body))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
req.Header.Set("Idempotency-Key", "mailbox-long")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMMailboxSubmissions(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
resp := decodeIMHTTPResponse(t, rec)
|
||||
if resp.Reason != imReasonValidationFailed {
|
||||
t.Fatalf("reason=%q body=%s", resp.Reason, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMailboxSubmissionMapsPolicyToValidationFailed(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldMailbox := store.Mailbox
|
||||
store.Mailbox = &testMailboxStore{submitErr: types.ErrPolicy}
|
||||
defer func() { store.Mailbox = oldMailbox }()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/executive-mailbox/submissions", strings.NewReader(`{"subject":"建议","content":"希望优化流程"}`))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
req.Header.Set("Idempotency-Key", "mailbox-policy")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMMailboxSubmissions(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
resp := decodeIMHTTPResponse(t, rec)
|
||||
if resp.Reason != imReasonValidationFailed {
|
||||
t.Fatalf("reason=%q body=%s", resp.Reason, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMeProfile(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -4580,21 +4758,33 @@ func TestServeIMGroupFiles(t *testing.T) {
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldSubs := store.Subs
|
||||
oldGroupBusiness := store.GroupBusiness
|
||||
oldFiles := store.Files
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
files := mock_store.NewMockFilePersistenceInterface(ctrl)
|
||||
groupStore := &testGroupBusinessStore{}
|
||||
store.Subs = subs
|
||||
store.GroupBusiness = groupStore
|
||||
store.Files = files
|
||||
defer func() {
|
||||
store.Subs = oldSubs
|
||||
store.GroupBusiness = oldGroupBusiness
|
||||
store.Files = oldFiles
|
||||
}()
|
||||
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.ModeCFull, ModeGiven: types.ModeCFull,
|
||||
}, nil).Times(3)
|
||||
|
||||
fileID := types.Uid(101).String()
|
||||
files.EXPECT().Get(types.TenantID(7), fileID).Return(&types.FileDef{
|
||||
ObjHeader: types.ObjHeader{Id: fileID},
|
||||
TenantID: 7,
|
||||
User: types.Uid(42).String(),
|
||||
Status: types.UploadCompleted,
|
||||
MimeType: "application/pdf",
|
||||
Size: 123,
|
||||
}, nil)
|
||||
createReq := httptest.NewRequest(http.MethodPost, "/v1/im/groups/grpAlpha/files",
|
||||
strings.NewReader(fmt.Sprintf(`{"file_id":"%s.pdf","file_name":"方案.pdf","file_type":"document","file_size":123}`, fileID)))
|
||||
strings.NewReader(fmt.Sprintf(`{"file_id":"%s.pdf","file_name":"方案.pdf","mime_type":"application/pdf","file_type":"document","file_size":123}`, fileID)))
|
||||
createReq.Header.Set("Authorization", "Bearer token-1")
|
||||
createReq.Header.Set("Idempotency-Key", "gfile-1")
|
||||
createRec := httptest.NewRecorder()
|
||||
@@ -4632,6 +4822,75 @@ func TestServeIMGroupFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMGroupFileCreateValidatesInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
setupFiles func(*mock_store.MockFilePersistenceInterface, string)
|
||||
wantReason string
|
||||
}{
|
||||
{
|
||||
name: "file name too long",
|
||||
body: `{"file_id":"%s.pdf","file_name":"` + strings.Repeat("a", 256) + `"}`,
|
||||
wantReason: imReasonValidationFailed,
|
||||
},
|
||||
{
|
||||
name: "negative size",
|
||||
body: `{"file_id":"%s.pdf","file_name":"方案.pdf","file_size":-1}`,
|
||||
wantReason: imReasonValidationFailed,
|
||||
},
|
||||
{
|
||||
name: "missing upload",
|
||||
body: `{"file_id":"%s.pdf","file_name":"方案.pdf"}`,
|
||||
setupFiles: func(files *mock_store.MockFilePersistenceInterface, fileID string) {
|
||||
files.EXPECT().Get(types.TenantID(7), fileID).Return(nil, nil)
|
||||
},
|
||||
wantReason: imReasonFileUnavailable,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldSubs := store.Subs
|
||||
oldGroupBusiness := store.GroupBusiness
|
||||
oldFiles := store.Files
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
files := mock_store.NewMockFilePersistenceInterface(ctrl)
|
||||
store.Subs = subs
|
||||
store.GroupBusiness = &testGroupBusinessStore{}
|
||||
store.Files = files
|
||||
defer func() {
|
||||
store.Subs = oldSubs
|
||||
store.GroupBusiness = oldGroupBusiness
|
||||
store.Files = oldFiles
|
||||
}()
|
||||
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.ModeCFull, ModeGiven: types.ModeCFull,
|
||||
}, nil)
|
||||
fileID := types.Uid(101).String()
|
||||
if tc.setupFiles != nil {
|
||||
tc.setupFiles(files, fileID)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/groups/grpAlpha/files",
|
||||
strings.NewReader(fmt.Sprintf(tc.body, fileID)))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
req.Header.Set("Idempotency-Key", "gfile-invalid")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMGroupResource(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest && rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
resp := decodeIMHTTPResponse(t, rec)
|
||||
if resp.Reason != tc.wantReason {
|
||||
t.Fatalf("reason: got %q, want %q; body=%s", resp.Reason, tc.wantReason, rec.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMGroupQRCodesAndResolve(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -4823,6 +5082,70 @@ func TestServeIMGroupAnnouncementLatestIncludesEditPermission(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMGroupAnnouncementGetUsesPathID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
oldStore := store.Store
|
||||
oldGroupBusiness := store.GroupBusiness
|
||||
oldSubs := store.Subs
|
||||
mockStore := expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
store.Store = mockStore
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
store.Subs = subs
|
||||
store.GroupBusiness = &testGroupBusinessStore{announcements: map[string]*types.GroupAnnouncement{
|
||||
"ann_old": {AnnouncementID: "ann_old", Topic: "grpAlpha", AuthorUserID: types.Uid(42), Content: "旧公告", Status: types.GroupAnnouncementStatusSuperseded, Version: 1, PublishedAt: time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC)},
|
||||
"ann_new": {AnnouncementID: "ann_new", Topic: "grpAlpha", AuthorUserID: types.Uid(42), Content: "新公告", Status: types.GroupAnnouncementStatusCurrent, Version: 2, PublishedAt: time.Date(2026, 8, 13, 10, 0, 0, 0, time.UTC)},
|
||||
}}
|
||||
defer func() {
|
||||
store.Store = oldStore
|
||||
store.Subs = oldSubs
|
||||
store.GroupBusiness = oldGroupBusiness
|
||||
}()
|
||||
selfSub := &types.Subscription{TenantID: 7, User: types.Uid(42).String(), Topic: "grpAlpha", ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic}
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpAlpha", types.Uid(42), false).Return(selfSub, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/groups/grpAlpha/announcements/ann_old", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMGroupResource(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("announcement get status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["announcement_id"] != "ann_old" || data["content"] != "旧公告" {
|
||||
t.Fatalf("announcement get ignored path id: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMGroupAnnouncementGetMissingReturnsNotFound(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldGroupBusiness := store.GroupBusiness
|
||||
oldSubs := store.Subs
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
store.Subs = subs
|
||||
store.GroupBusiness = &testGroupBusinessStore{}
|
||||
defer func() {
|
||||
store.Subs = oldSubs
|
||||
store.GroupBusiness = oldGroupBusiness
|
||||
}()
|
||||
selfSub := &types.Subscription{TenantID: 7, User: types.Uid(42).String(), Topic: "grpAlpha", ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic}
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpAlpha", types.Uid(42), false).Return(selfSub, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/groups/grpAlpha/announcements/ann_missing", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMGroupResource(rec, req)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("announcement missing status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
resp := decodeIMHTTPResponse(t, rec)
|
||||
if resp.Reason != imReasonResourceNotFound {
|
||||
t.Fatalf("reason: got %q, want %q", resp.Reason, imReasonResourceNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMGroupAdminsMutesAndOwnerTransfer(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -5319,6 +5642,7 @@ func TestServeIMFavoriteForwardSavesMessages(t *testing.T) {
|
||||
tokenAuth.EXPECT().IsInitialized().Return(true)
|
||||
tokenAuth.EXPECT().Authenticate(auth.AuthContext{}, []byte("token-1")).
|
||||
Return(&auth.Rec{TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth}, nil, nil)
|
||||
mockStore.EXPECT().GetUidString().Return("comment-mid")
|
||||
store.Store = mockStore
|
||||
oldFavorites := store.Favorites
|
||||
oldSubs := store.Subs
|
||||
@@ -5331,7 +5655,7 @@ func TestServeIMFavoriteForwardSavesMessages(t *testing.T) {
|
||||
ItemType: "text",
|
||||
SourceTopic: "grpAlpha",
|
||||
SourceSeq: 3,
|
||||
Snapshot: map[string]any{"text": "hello"},
|
||||
Snapshot: map[string]any{"head": map[string]any{"type": "text"}, "content": map[string]any{"text": "hello"}, "text": "hello"},
|
||||
Availability: "available",
|
||||
CollectedAt: now,
|
||||
}}}
|
||||
@@ -5352,20 +5676,29 @@ func TestServeIMFavoriteForwardSavesMessages(t *testing.T) {
|
||||
ModeWant: types.ModeCPublic,
|
||||
ModeGiven: types.ModeCPublic,
|
||||
}, nil)
|
||||
messages.EXPECT().Save(types.TenantID(7), gomock.Any(), gomock.Nil(), true).DoAndReturn(
|
||||
func(_ types.TenantID, msg *types.Message, _ []string, _ bool) (*types.MessageSaveResult, error) {
|
||||
if msg.Topic != "grpBeta" || msg.From != types.Uid(42).String() {
|
||||
t.Fatalf("unexpected forwarded message: %#v", msg)
|
||||
}
|
||||
if msg.Head["x-im-forwarded"] != true {
|
||||
t.Fatalf("forward marker not set: %#v", msg.Head)
|
||||
}
|
||||
content := msg.Content.(map[string]any)
|
||||
if content["favorite_id"] != "fav_1" || content["comment"] != "FYI" {
|
||||
t.Fatalf("unexpected forward content: %#v", content)
|
||||
}
|
||||
return &types.MessageSaveResult{SeqId: 11, MarkedReadBySender: true}, nil
|
||||
})
|
||||
gomock.InOrder(
|
||||
messages.EXPECT().Save(types.TenantID(7), gomock.Any(), gomock.Nil(), true).DoAndReturn(
|
||||
func(_ types.TenantID, msg *types.Message, _ []string, _ bool) (*types.MessageSaveResult, error) {
|
||||
if msg.Topic != "grpBeta" || msg.Head["x-im-type"] != "text" || msg.Content.(map[string]any)["text"] != "FYI" {
|
||||
t.Fatalf("unexpected forward comment message: %#v", msg)
|
||||
}
|
||||
return &types.MessageSaveResult{SeqId: 10, MarkedReadBySender: true}, nil
|
||||
}),
|
||||
messages.EXPECT().Save(types.TenantID(7), gomock.Any(), gomock.Nil(), true).DoAndReturn(
|
||||
func(_ types.TenantID, msg *types.Message, _ []string, _ bool) (*types.MessageSaveResult, error) {
|
||||
if msg.Topic != "grpBeta" || msg.From != types.Uid(42).String() {
|
||||
t.Fatalf("unexpected forwarded message: %#v", msg)
|
||||
}
|
||||
if msg.Head["x-im-forwarded"] != true {
|
||||
t.Fatalf("forward marker not set: %#v", msg.Head)
|
||||
}
|
||||
content := msg.Content.(map[string]any)
|
||||
if content["text"] != "hello" || content["comment"] != nil || content["snapshot"] != nil {
|
||||
t.Fatalf("unexpected forward content: %#v", content)
|
||||
}
|
||||
return &types.MessageSaveResult{SeqId: 11, MarkedReadBySender: true}, nil
|
||||
}),
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/favorites/forward", bytes.NewBufferString(`{"favorite_ids":["fav_1"],"target_topics":["grpBeta"],"comment":"FYI"}`))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
@@ -5375,7 +5708,7 @@ func TestServeIMFavoriteForwardSavesMessages(t *testing.T) {
|
||||
t.Fatalf("forward status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
items := decodeIMHTTPResponse(t, rec).Data.(map[string]any)["items"].([]any)
|
||||
if got := items[0].(map[string]any); got["status"] != "sent" || got["seq"].(float64) != 11 {
|
||||
if got := items[0].(map[string]any); got["status"] != "sent" || got["seq"].(float64) != 11 || got["comment_seq"].(float64) != 10 {
|
||||
t.Fatalf("unexpected forward response: %#v", got)
|
||||
}
|
||||
}
|
||||
@@ -5502,6 +5835,80 @@ func TestServeIMMessageForwardSeparateSavesMessages(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMessageForwardSeparateSendsCommentAsTextMessage(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockStore := expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
mockStore.EXPECT().GetUidString().Return("comment-mid")
|
||||
mockStore.EXPECT().GetUidString().Return("mid-1")
|
||||
|
||||
oldSubs := store.Subs
|
||||
oldMessages := store.Messages
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
messages := mock_store.NewMockMessagesPersistenceInterface(ctrl)
|
||||
store.Subs = subs
|
||||
store.Messages = messages
|
||||
t.Cleanup(func() {
|
||||
store.Subs = oldSubs
|
||||
store.Messages = oldMessages
|
||||
})
|
||||
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpTarget", types.Uid(42), false).Return(&types.Subscription{
|
||||
TenantID: 7, User: types.Uid(42).String(), Topic: "grpTarget", ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic,
|
||||
}, nil)
|
||||
subs.EXPECT().Get(types.TenantID(7), "grpSource", types.Uid(42), false).Return(&types.Subscription{
|
||||
TenantID: 7, User: types.Uid(42).String(), Topic: "grpSource", ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic,
|
||||
}, nil)
|
||||
messages.EXPECT().GetOne(types.TenantID(7), "grpSource", int64(3)).Return(&types.Message{
|
||||
TenantID: 7,
|
||||
Topic: "grpSource",
|
||||
SeqId: 3,
|
||||
From: types.Uid(99).String(),
|
||||
Head: types.KVMap{"x-im-type": "image"},
|
||||
Content: map[string]any{"file_id": "file-1", "mime": "image/png"},
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC),
|
||||
},
|
||||
}, nil)
|
||||
gomock.InOrder(
|
||||
messages.EXPECT().Save(types.TenantID(7), gomock.Any(), gomock.Nil(), true).DoAndReturn(
|
||||
func(_ types.TenantID, msg *types.Message, _ []string, _ bool) (*types.MessageSaveResult, error) {
|
||||
if msg.Head["x-im-type"] != "text" || msg.Content.(map[string]any)["text"] != "FYI" {
|
||||
t.Fatalf("unexpected comment message: %#v", msg)
|
||||
}
|
||||
return &types.MessageSaveResult{SeqId: 20, MarkedReadBySender: true}, nil
|
||||
}),
|
||||
messages.EXPECT().Save(types.TenantID(7), gomock.Any(), gomock.Nil(), true).DoAndReturn(
|
||||
func(_ types.TenantID, msg *types.Message, _ []string, _ bool) (*types.MessageSaveResult, error) {
|
||||
if msg.Head["x-im-type"] != "image" {
|
||||
t.Fatalf("forwarded message type changed: %#v", msg.Head)
|
||||
}
|
||||
content := msg.Content.(map[string]any)
|
||||
if content["file_id"] != "file-1" || content["snapshot"] != nil || content["comment"] != nil {
|
||||
t.Fatalf("forwarded message content should stay original: %#v", content)
|
||||
}
|
||||
return &types.MessageSaveResult{SeqId: 21, MarkedReadBySender: true}, nil
|
||||
}),
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/messages/forward", bytes.NewBufferString(`{"mode":"separate","sources":[{"topic":"grpSource","seq":"3"}],"target_topics":["grpTarget"],"comment":"FYI"}`))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMMessageForward(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("forward status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
items := data["items"].([]any)
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("unexpected forward response: %#v", data)
|
||||
}
|
||||
got := items[0].(map[string]any)
|
||||
if got["status"] != "sent" || got["seq"].(float64) != 21 || got["comment_seq"].(float64) != 20 {
|
||||
t.Fatalf("unexpected forward item: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMessageForwardAcceptsNumericSeqAndUserTarget(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
Reference in New Issue
Block a user