im 服务端
This commit is contained in:
+362
-28
@@ -44,6 +44,9 @@ type testRelationsStore struct {
|
||||
friendRequests []types.FriendRequestListItem
|
||||
searchResults []types.UserSearchItem
|
||||
blocks []types.BlockListItem
|
||||
deleteTenant types.TenantID
|
||||
deleteUser types.Uid
|
||||
deletePeer types.Uid
|
||||
}
|
||||
|
||||
type testFavoritesStore struct {
|
||||
@@ -53,13 +56,22 @@ type testFavoritesStore struct {
|
||||
}
|
||||
|
||||
type testCallsStore struct {
|
||||
items []types.CallRecord
|
||||
topic string
|
||||
items []types.CallRecord
|
||||
topic string
|
||||
listed bool
|
||||
tenant types.TenantID
|
||||
user types.Uid
|
||||
limit int
|
||||
cursor string
|
||||
next string
|
||||
}
|
||||
|
||||
type testWorkbenchStore struct {
|
||||
modules []types.WorkbenchModule
|
||||
version int
|
||||
modules []types.WorkbenchModule
|
||||
version int
|
||||
tenantID types.TenantID
|
||||
uid types.Uid
|
||||
calledOnce bool
|
||||
}
|
||||
|
||||
type testOrganizationStore struct {
|
||||
@@ -109,7 +121,8 @@ type testAppReleaseStore struct {
|
||||
}
|
||||
|
||||
type testDevicePushTokenStore struct {
|
||||
token *types.DevicePushToken
|
||||
token *types.DevicePushToken
|
||||
tenantID types.TenantID
|
||||
}
|
||||
|
||||
type testSafetyStore struct {
|
||||
@@ -117,9 +130,18 @@ type testSafetyStore struct {
|
||||
reasons []types.AbuseReason
|
||||
abuseReport *types.AbuseReport
|
||||
feedback *types.FeedbackSubmission
|
||||
tenantID types.TenantID
|
||||
uid types.Uid
|
||||
idempotency string
|
||||
decision string
|
||||
actionTokenID string
|
||||
targetType string
|
||||
}
|
||||
|
||||
func (s *testWorkbenchStore) Get(_ types.TenantID, _ types.Uid) ([]types.WorkbenchModule, int, error) {
|
||||
func (s *testWorkbenchStore) Get(tenantID types.TenantID, uid types.Uid) ([]types.WorkbenchModule, int, error) {
|
||||
s.tenantID = tenantID
|
||||
s.uid = uid
|
||||
s.calledOnce = true
|
||||
if s.version == 0 {
|
||||
s.version = 1
|
||||
}
|
||||
@@ -369,23 +391,29 @@ func (s *testAppReleaseStore) Latest(_ types.TenantID, platform, channel string)
|
||||
return s.release, nil
|
||||
}
|
||||
|
||||
func (s *testDevicePushTokenStore) Upsert(_ types.TenantID, token *types.DevicePushToken) (*types.DevicePushToken, error) {
|
||||
func (s *testDevicePushTokenStore) Upsert(tenantID types.TenantID, token *types.DevicePushToken) (*types.DevicePushToken, error) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
token.CreatedAt = now
|
||||
token.UpdatedAt = now
|
||||
s.token = token
|
||||
s.tenantID = tenantID
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s *testSafetyStore) CreateRiskChallenge(_ types.TenantID, challenge *types.RiskChallenge) (*types.RiskChallenge, error) {
|
||||
func (s *testSafetyStore) CreateRiskChallenge(tenantID types.TenantID, challenge *types.RiskChallenge) (*types.RiskChallenge, error) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
challenge.CreatedAt = now
|
||||
challenge.UpdatedAt = now
|
||||
s.riskChallenge = challenge
|
||||
s.tenantID = tenantID
|
||||
return challenge, nil
|
||||
}
|
||||
|
||||
func (s *testSafetyStore) ConfirmRiskChallenge(_ types.TenantID, uid types.Uid, challengeID, decision, actionTokenID string, now time.Time) (*types.RiskChallenge, error) {
|
||||
func (s *testSafetyStore) ConfirmRiskChallenge(tenantID types.TenantID, uid types.Uid, challengeID, decision, actionTokenID string, now time.Time) (*types.RiskChallenge, error) {
|
||||
s.tenantID = tenantID
|
||||
s.uid = uid
|
||||
s.decision = decision
|
||||
s.actionTokenID = actionTokenID
|
||||
if s.riskChallenge == nil {
|
||||
s.riskChallenge = &types.RiskChallenge{ChallengeID: challengeID, UserID: uid, Action: "open_url", Level: "high", NoticeCode: "url_high", ExpiresAt: now.Add(time.Minute)}
|
||||
}
|
||||
@@ -399,23 +427,29 @@ func (s *testSafetyStore) ConfirmRiskChallenge(_ types.TenantID, uid types.Uid,
|
||||
return s.riskChallenge, nil
|
||||
}
|
||||
|
||||
func (s *testSafetyStore) AbuseReasons(_ types.TenantID, targetType string) ([]types.AbuseReason, error) {
|
||||
func (s *testSafetyStore) AbuseReasons(tenantID types.TenantID, targetType string) ([]types.AbuseReason, error) {
|
||||
s.tenantID = tenantID
|
||||
s.targetType = targetType
|
||||
if len(s.reasons) == 0 {
|
||||
s.reasons = []types.AbuseReason{{TargetType: targetType, ReasonCode: "fraud", Label: "诈骗欺诈", AllowsEvidence: true}}
|
||||
}
|
||||
return s.reasons, nil
|
||||
}
|
||||
|
||||
func (s *testSafetyStore) CreateAbuseReport(_ types.TenantID, report *types.AbuseReport, _ string) (*types.AbuseReport, error) {
|
||||
func (s *testSafetyStore) CreateAbuseReport(tenantID types.TenantID, report *types.AbuseReport, idempotencyKey string) (*types.AbuseReport, error) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
report.Status = types.AbuseReportStatusReceived
|
||||
report.CreatedAt = now
|
||||
report.UpdatedAt = now
|
||||
s.abuseReport = report
|
||||
s.tenantID = tenantID
|
||||
s.idempotency = idempotencyKey
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func (s *testSafetyStore) FeedbackConfig(types.TenantID, types.Uid) (*types.FeedbackConfig, error) {
|
||||
func (s *testSafetyStore) FeedbackConfig(tenantID types.TenantID, uid types.Uid) (*types.FeedbackConfig, error) {
|
||||
s.tenantID = tenantID
|
||||
s.uid = uid
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
return &types.FeedbackConfig{
|
||||
Categories: []string{"bug", "suggestion"},
|
||||
@@ -427,12 +461,14 @@ func (s *testSafetyStore) FeedbackConfig(types.TenantID, types.Uid) (*types.Feed
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *testSafetyStore) CreateFeedback(_ types.TenantID, feedback *types.FeedbackSubmission, _ string) (*types.FeedbackSubmission, error) {
|
||||
func (s *testSafetyStore) CreateFeedback(tenantID types.TenantID, feedback *types.FeedbackSubmission, idempotencyKey string) (*types.FeedbackSubmission, error) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
feedback.Status = types.FeedbackStatusReceived
|
||||
feedback.CreatedAt = now
|
||||
feedback.UpdatedAt = now
|
||||
s.feedback = feedback
|
||||
s.tenantID = tenantID
|
||||
s.idempotency = idempotencyKey
|
||||
return feedback, nil
|
||||
}
|
||||
|
||||
@@ -448,12 +484,26 @@ func (s *testCallsStore) Finish(_ types.TenantID, _ string, _ int, _ string, _ i
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testCallsStore) List(_ types.TenantID, _ types.Uid, topic, _ string, _ int) ([]types.CallRecord, string, error) {
|
||||
func (s *testCallsStore) List(tenantID types.TenantID, uid types.Uid, topic, cursor string, limit int) ([]types.CallRecord, string, error) {
|
||||
s.listed = true
|
||||
s.tenant = tenantID
|
||||
s.user = uid
|
||||
s.topic = topic
|
||||
return s.items, "", nil
|
||||
s.cursor = cursor
|
||||
s.limit = limit
|
||||
return s.items, s.next, nil
|
||||
}
|
||||
|
||||
func (s *testFavoritesStore) Create(_ types.TenantID, item *types.FavoriteItem) (*types.FavoriteItem, error) {
|
||||
for i := range s.items {
|
||||
existing := &s.items[i]
|
||||
if existing.UserID == item.UserID && existing.SourceTopic == item.SourceTopic && existing.SourceSeq == item.SourceSeq {
|
||||
return existing, nil
|
||||
}
|
||||
if item.IdempotencyKey != "" && existing.UserID == item.UserID && existing.IdempotencyKey == item.IdempotencyKey {
|
||||
return existing, nil
|
||||
}
|
||||
}
|
||||
s.created = item
|
||||
s.items = append(s.items, *item)
|
||||
return item, nil
|
||||
@@ -535,8 +585,11 @@ func (s *testRelationsStore) UnblockUser(_ types.TenantID, _, _ types.Uid) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *testRelationsStore) DeleteFriend(_ types.TenantID, _, _ types.Uid, _ time.Time) error {
|
||||
func (s *testRelationsStore) DeleteFriend(tenantID types.TenantID, userID, peerID types.Uid, _ time.Time) error {
|
||||
s.deleted = true
|
||||
s.deleteTenant = tenantID
|
||||
s.deleteUser = userID
|
||||
s.deletePeer = peerID
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1125,8 +1178,18 @@ func TestServeIMBootstrapIncludesHomeCapabilities(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldSecurity := store.Security
|
||||
store.Security = &testSecurityStore{}
|
||||
defer func() { store.Security = oldSecurity }()
|
||||
oldWorkbench := store.Workbench
|
||||
security := &testSecurityStore{realName: &types.RealNameVerification{Status: "verified"}}
|
||||
workbench := &testWorkbenchStore{version: 9, modules: []types.WorkbenchModule{
|
||||
{Code: "organization", Name: "组织架构", Route: "/organization", Enabled: true, Permission: "enabled"},
|
||||
{Code: "external", Name: "外链", Route: "https://example.com", Enabled: true, Permission: "enabled"},
|
||||
}}
|
||||
store.Security = security
|
||||
store.Workbench = workbench
|
||||
defer func() {
|
||||
store.Security = oldSecurity
|
||||
store.Workbench = oldWorkbench
|
||||
}()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/bootstrap", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
@@ -1141,6 +1204,23 @@ func TestServeIMBootstrapIncludesHomeCapabilities(t *testing.T) {
|
||||
if data["features"] == nil || data["permissions"] == nil || data["system_topic"] == nil || data["message_policy"] == nil {
|
||||
t.Fatalf("bootstrap missing home capabilities: %#v", data)
|
||||
}
|
||||
if security.realNameTenant != 7 || security.realNameUser != 42 {
|
||||
t.Fatalf("bootstrap realname lookup scope: tenant=%d uid=%s", security.realNameTenant, security.realNameUser)
|
||||
}
|
||||
if !workbench.calledOnce || workbench.tenantID != 7 || workbench.uid != 42 {
|
||||
t.Fatalf("bootstrap workbench lookup scope: store=%#v", workbench)
|
||||
}
|
||||
if data["workbench_version"].(float64) != 9 {
|
||||
t.Fatalf("unexpected bootstrap workbench version: %#v", data)
|
||||
}
|
||||
modules := data["modules"].([]any)
|
||||
if len(modules) != 1 || modules[0].(map[string]any)["code"] != "organization" {
|
||||
t.Fatalf("unexpected bootstrap modules: %#v", modules)
|
||||
}
|
||||
realname := data["security"].(map[string]any)["realname"].(map[string]any)
|
||||
if realname["status"] != "verified" {
|
||||
t.Fatalf("unexpected bootstrap realname status: %#v", realname)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMWorkbench(t *testing.T) {
|
||||
@@ -1706,7 +1786,7 @@ func TestServeIMDevicePushToken(t *testing.T) {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
if tokenStore.token == nil || tokenStore.token.DeviceID != "device-1" || tokenStore.token.Token != "push-token-1" {
|
||||
if tokenStore.tenantID != 7 || tokenStore.token == nil || tokenStore.token.UserID != 42 || tokenStore.token.DeviceID != "device-1" || tokenStore.token.Token != "push-token-1" {
|
||||
t.Fatalf("unexpected stored push token: %#v", tokenStore.token)
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
@@ -1763,10 +1843,76 @@ func TestServeIMConversationSearch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMConversationSearchNoDisclosureOnNoMatch(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldUsers := store.Users
|
||||
users := mock_store.NewMockUsersPersistenceInterface(ctrl)
|
||||
store.Users = users
|
||||
defer func() { store.Users = oldUsers }()
|
||||
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
bob := types.Subscription{
|
||||
ObjHeader: types.ObjHeader{UpdatedAt: now},
|
||||
Topic: "grpBob",
|
||||
ReadSeqId: 9,
|
||||
}
|
||||
bob.SetPublic(map[string]any{"fn": "Bob Group"})
|
||||
bob.SetSeqId(9)
|
||||
bob.SetTouchedAt(now)
|
||||
users.EXPECT().GetTopics(types.TenantID(7), types.Uid(42), gomock.Any()).Return([]types.Subscription{bob}, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/search/conversations?q=alice", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMConversationSearch(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if len(data["items"].([]any)) != 0 {
|
||||
t.Fatalf("conversation search leaked unmatched topic: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMConversationSearchExpiredTokenDoesNotQueryTopics(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
oldStore := store.Store
|
||||
oldUsers := store.Users
|
||||
mockStore := mock_store.NewMockPersistentStorageInterface(ctrl)
|
||||
tokenAuth := mock_auth.NewMockAuthHandler(ctrl)
|
||||
users := mock_store.NewMockUsersPersistenceInterface(ctrl)
|
||||
store.Store = mockStore
|
||||
store.Users = users
|
||||
defer func() {
|
||||
store.Store = oldStore
|
||||
store.Users = oldUsers
|
||||
}()
|
||||
|
||||
mockStore.EXPECT().GetLogicalAuthHandler("token").Return(tokenAuth)
|
||||
tokenAuth.EXPECT().IsInitialized().Return(true)
|
||||
tokenAuth.EXPECT().Authenticate(auth.AuthContext{}, []byte("expired-token")).Return(nil, nil, types.ErrExpired)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/search/conversations?q=alice", nil)
|
||||
req.Header.Set("Authorization", "Bearer expired-token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMConversationSearch(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusUnauthorized, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMSystemNotificationsListAndRead(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
oldSecurity := store.Security
|
||||
readAt := time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC)
|
||||
security := &testSecurityStore{notifications: []types.SystemNotification{{
|
||||
TenantID: 7,
|
||||
UserID: 42,
|
||||
@@ -1776,12 +1922,21 @@ func TestServeIMSystemNotificationsListAndRead(t *testing.T) {
|
||||
Summary: "iPhone 登录",
|
||||
Action: types.KVMap{"type": "security_center"},
|
||||
CreatedAt: time.Now().UTC().Round(time.Millisecond),
|
||||
}}}
|
||||
}, {
|
||||
TenantID: 7,
|
||||
UserID: 42,
|
||||
EventID: "evt-2",
|
||||
EventType: "system.notice",
|
||||
Title: "系统公告",
|
||||
Summary: "已读公告",
|
||||
ReadAt: &readAt,
|
||||
CreatedAt: readAt,
|
||||
}}, nextCursor: "cursor-2"}
|
||||
store.Security = security
|
||||
defer func() { store.Security = oldSecurity }()
|
||||
|
||||
expectIMBearer(t, ctrl, "token-list", 7, 42)
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/system-notifications", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/system-notifications?cursor=cursor-1&limit=2", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-list")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -1791,9 +1946,19 @@ func TestServeIMSystemNotificationsListAndRead(t *testing.T) {
|
||||
t.Fatalf("list status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if len(data["items"].([]any)) != 1 {
|
||||
items := data["items"].([]any)
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("unexpected list response: %#v", data)
|
||||
}
|
||||
if _, ok := items[0].(map[string]any)["read_at"]; ok {
|
||||
t.Fatalf("unread notification unexpectedly has read_at: %#v", items[0])
|
||||
}
|
||||
if items[1].(map[string]any)["read_at"] == nil {
|
||||
t.Fatalf("read notification missing read_at: %#v", items[1])
|
||||
}
|
||||
if data["next_cursor"] != "cursor-2" || security.listTenant != 7 || security.listUser != 42 || security.listCursor != "cursor-1" || security.listLimit != 2 {
|
||||
t.Fatalf("notification list scope/paging mismatch: data=%#v store=%#v", data, security)
|
||||
}
|
||||
|
||||
expectIMBearer(t, ctrl, "token-read", 7, 42)
|
||||
req = httptest.NewRequest(http.MethodPost, "/v1/im/system-notifications/evt-1/read", bytes.NewBufferString(`{}`))
|
||||
@@ -1806,7 +1971,7 @@ func TestServeIMSystemNotificationsListAndRead(t *testing.T) {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("read status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
if security.readNotificationID != "evt-1" || security.notifications[0].ReadAt == nil {
|
||||
if security.readTenant != 7 || security.readUser != 42 || security.readNotificationID != "evt-1" || security.notifications[0].ReadAt == nil {
|
||||
t.Fatalf("notification was not marked read: %#v", security)
|
||||
}
|
||||
}
|
||||
@@ -2446,6 +2611,58 @@ func TestServeIMFavoritesListDetailBatchDelete(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMFavoriteBatchCreateCountsExisting(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mockStore := expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
mockStore.EXPECT().GetUidString().Return("new-fav")
|
||||
|
||||
oldFavorites := store.Favorites
|
||||
oldMessages := store.Messages
|
||||
now := time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC)
|
||||
store.Favorites = &testFavoritesStore{items: []types.FavoriteItem{{
|
||||
TenantID: 7,
|
||||
FavoriteID: "fav_existing",
|
||||
UserID: 42,
|
||||
ItemType: "text",
|
||||
SourceTopic: "grpAlpha",
|
||||
SourceSeq: 3,
|
||||
Snapshot: map[string]any{"text": "hello"},
|
||||
Availability: "available",
|
||||
CollectedAt: now,
|
||||
}}}
|
||||
messages := mock_store.NewMockMessagesPersistenceInterface(ctrl)
|
||||
store.Messages = messages
|
||||
t.Cleanup(func() {
|
||||
store.Favorites = oldFavorites
|
||||
store.Messages = oldMessages
|
||||
})
|
||||
|
||||
messages.EXPECT().GetOne(types.TenantID(7), "grpAlpha", 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)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/favorites", bytes.NewBufferString(`{"messages":[{"topic":"grpAlpha","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) != 0 || data["existing_count"].(float64) != 1 {
|
||||
t.Fatalf("unexpected favorite counts: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMFavoriteForwardSavesMessages(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -2517,6 +2734,51 @@ func TestServeIMFavoriteForwardSavesMessages(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMFavoriteForwardRejectsUnauthorizedTarget(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
|
||||
now := time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC)
|
||||
store.Favorites = &testFavoritesStore{items: []types.FavoriteItem{{
|
||||
TenantID: 7,
|
||||
FavoriteID: "fav_1",
|
||||
UserID: 42,
|
||||
ItemType: "text",
|
||||
SourceTopic: "grpAlpha",
|
||||
SourceSeq: 3,
|
||||
Snapshot: map[string]any{"text": "hello"},
|
||||
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), "grpDenied", types.Uid(42), false).Return(nil, types.ErrPermissionDenied)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/favorites/forward", bytes.NewBufferString(`{"favorite_ids":["fav_1"],"target_topics":["grpDenied"]}`))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMFavoriteResource(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
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"] != "failed" || got["reason"] != imReasonPermissionDenied {
|
||||
t.Fatalf("unexpected forward response: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMSecurityURLCheck(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -2587,6 +2849,9 @@ func TestServeIMRiskChallengeConfirm(t *testing.T) {
|
||||
if data["state"] != types.RiskChallengeStateContinued || data["risk_action_token"] == "" {
|
||||
t.Fatalf("unexpected risk confirm response: %#v", data)
|
||||
}
|
||||
if safety := store.Safety.(*testSafetyStore); safety.tenantID != 7 || safety.uid != 42 || safety.decision != "continue" || safety.actionTokenID == "" {
|
||||
t.Fatalf("risk challenge scope mismatch: %#v", safety)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMAbuseReportsAndReasons(t *testing.T) {
|
||||
@@ -2612,6 +2877,9 @@ func TestServeIMAbuseReportsAndReasons(t *testing.T) {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("reasons status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if safety.tenantID != 7 || safety.targetType != "user" {
|
||||
t.Fatalf("abuse reason scope mismatch: %#v", safety)
|
||||
}
|
||||
|
||||
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"]}`))
|
||||
@@ -2622,7 +2890,7 @@ func TestServeIMAbuseReportsAndReasons(t *testing.T) {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("report status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if safety.abuseReport == nil || safety.abuseReport.TargetUserID != 43 || safety.abuseReport.ReasonCode != "fraud" {
|
||||
if safety.tenantID != 7 || safety.idempotency != "abuse-1" || safety.abuseReport == nil || safety.abuseReport.ReporterID != 42 || safety.abuseReport.TargetUserID != 43 || safety.abuseReport.ReasonCode != "fraud" {
|
||||
t.Fatalf("unexpected abuse report: %#v", safety.abuseReport)
|
||||
}
|
||||
}
|
||||
@@ -2643,6 +2911,9 @@ func TestServeIMFeedback(t *testing.T) {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("config status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if safety.tenantID != 7 || safety.uid != 42 {
|
||||
t.Fatalf("feedback config scope mismatch: %#v", safety)
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodPost, "/v1/im/feedback",
|
||||
bytes.NewBufferString(`{"category":"bug","content":"页面显示异常","contact":"me@example.com","attachment_file_ids":["file1"],"client_context":{"platform":"android"},"include_diagnostics":true}`))
|
||||
@@ -2653,7 +2924,7 @@ func TestServeIMFeedback(t *testing.T) {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("submit status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if safety.feedback == nil || safety.feedback.Category != "bug" || !safety.feedback.IncludeDiagnostics {
|
||||
if safety.tenantID != 7 || safety.idempotency != "feedback-1" || safety.feedback == nil || safety.feedback.UserID != 42 || safety.feedback.Category != "bug" || !safety.feedback.IncludeDiagnostics {
|
||||
t.Fatalf("unexpected feedback: %#v", safety.feedback)
|
||||
}
|
||||
}
|
||||
@@ -2752,15 +3023,15 @@ func TestServeIMTopicCalls(t *testing.T) {
|
||||
TenantID: 7, User: types.Uid(42).String(), Topic: "p2pTopic", ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic,
|
||||
}, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/topics/p2pTopic/calls", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/topics/p2pTopic/calls?cursor=c1&limit=1", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMTopicMessageAction(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("calls status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if calls.topic != "p2pTopic" {
|
||||
t.Fatalf("calls topic: got %q", calls.topic)
|
||||
if !calls.listed || calls.tenant != 7 || calls.user != 42 || calls.topic != "p2pTopic" || calls.cursor != "c1" || calls.limit != 1 {
|
||||
t.Fatalf("calls list scope mismatch: %#v", calls)
|
||||
}
|
||||
var resp imHTTPResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
@@ -2777,6 +3048,34 @@ func TestServeIMTopicCalls(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMTopicCallsRejectsUnauthorizedWithoutListing(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldSubs := store.Subs
|
||||
oldCalls := store.Calls
|
||||
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
|
||||
calls := &testCallsStore{}
|
||||
store.Subs = subs
|
||||
store.Calls = calls
|
||||
defer func() {
|
||||
store.Subs = oldSubs
|
||||
store.Calls = oldCalls
|
||||
}()
|
||||
subs.EXPECT().Get(types.TenantID(7), "p2pDenied", types.Uid(42), false).Return(nil, types.ErrPermissionDenied)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/topics/p2pDenied/calls", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMTopicMessageAction(rec, req)
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("calls status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if calls.listed {
|
||||
t.Fatalf("calls were listed despite missing read permission: %#v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMeQRCodesAndResolve(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -3128,6 +3427,41 @@ func TestServeIMFriendRemark(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMFriendDeleteReturnsCannotSendContext(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 1)
|
||||
oldRelations := store.Relations
|
||||
peer := types.Uid(2)
|
||||
relations := &testRelationsStore{context: &types.RelationshipContext{
|
||||
TenantID: 7,
|
||||
UserID: 1,
|
||||
PeerID: peer,
|
||||
Topic: types.Uid(1).P2PName(peer),
|
||||
Relationship: types.RelationshipNone,
|
||||
CanSend: false,
|
||||
}}
|
||||
store.Relations = relations
|
||||
defer func() { store.Relations = oldRelations }()
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/v1/im/friends/"+peer.UserId(), nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMFriendResource(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
if !relations.deleted || relations.deleteTenant != 7 || relations.deleteUser != 1 || relations.deletePeer != peer {
|
||||
t.Fatalf("friend delete scope mismatch: %#v", relations)
|
||||
}
|
||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||
if data["relationship"] != types.RelationshipNone || data["can_send"] != false {
|
||||
t.Fatalf("friend delete did not return blocked send context: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMUserSearch(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
Reference in New Issue
Block a user