feat: 接入消息加密与实名认证能力

This commit is contained in:
richjohnrambo
2026-09-21 09:53:39 +08:00
parent 841c0c77cd
commit e3b93113f1
33 changed files with 1747 additions and 99 deletions
+138 -24
View File
@@ -226,6 +226,24 @@ func (s testThirdPartyConfigStore) Get(_ string, keys ...string) (map[string]str
return result, nil
}
type testRealNameConfigStore struct {
policy *types.RealNamePolicy
err error
}
func (s testRealNameConfigStore) Policy(tenantID types.TenantID) (*types.RealNamePolicy, error) {
if s.err != nil {
return nil, s.err
}
if s.policy == nil {
return &types.RealNamePolicy{TenantID: tenantID, ReviewPolicy: types.RealNameReviewPolicyNone, ProviderMode: "id_card_2factor", AllowSkip: true, RemindAfter: "24h"}, nil
}
policy := *s.policy
policy.TenantID = tenantID
policy.Normalize()
return &policy, nil
}
type testConversationsStore struct {
topic string
settings *types.ConversationSettings
@@ -2096,6 +2114,48 @@ func TestServeIMRealNameSubmissionCreatesPending(t *testing.T) {
}
}
func TestServeIMRealNameSubmissionAutoReviewFallsBackToManual(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
expectIMBearer(t, ctrl, "token-1", 7, 42)
oldSecurity := store.Security
oldRealNameConfig := store.RealNameConfig
oldThirdParty := store.ThirdPartyConfig
security := &testSecurityStore{}
store.Security = security
store.RealNameConfig = testRealNameConfigStore{policy: &types.RealNamePolicy{
ReviewPolicy: types.RealNameReviewPolicyAutoManual,
ProviderMode: "id_card_2factor",
AllowSkip: true,
RemindAfter: "24h",
}}
store.ThirdPartyConfig = testThirdPartyConfigStore{}
defer func() {
store.Security = oldSecurity
store.RealNameConfig = oldRealNameConfig
store.ThirdPartyConfig = oldThirdParty
}()
req := httptest.NewRequest(http.MethodPost, "/v1/im/security/realname/submissions",
bytes.NewBufferString(`{"name":"张三","id_type":"cn_id","id_number":"110101199003071234"}`))
req.Header.Set("Authorization", "Bearer token-1")
req.Header.Set("Idempotency-Key", "idem-1")
rec := httptest.NewRecorder()
serveIMRealNameSubmissions(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
if security.realName == nil || security.realName.Status != "manual_review" || security.realName.ReviewReason == nil {
t.Fatalf("unexpected stored realname: %#v", security.realName)
}
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if data["status"] != "pending" || data["review_status"] != "manual_review" {
t.Fatalf("unexpected response: %#v", data)
}
}
func TestServeIMRealNameAppealCreatesPending(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -5087,6 +5147,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("fav-mid")
mockStore.EXPECT().GetUidString().Return("comment-mid")
store.Store = mockStore
oldFavorites := store.Favorites
@@ -5137,6 +5198,9 @@ func TestServeIMFavoriteForwardSavesMessages(t *testing.T) {
if msg.Head["x-im-forwarded"] != true {
t.Fatalf("forward marker not set: %#v", msg.Head)
}
if msg.Head["x-im-client-mid"] != "fwd_fav-mid" {
t.Fatalf("forward client mid 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)
@@ -5153,11 +5217,82 @@ 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 || got["comment_seq"].(float64) != 10 {
if got := items[0].(map[string]any); got["status"] != "sent" || got["seq"].(float64) != 11 || got["comment_seq"].(float64) != 10 || got["client_mid"] != "fwd_fav-mid" {
t.Fatalf("unexpected forward response: %#v", got)
}
}
func TestServeIMFavoriteForwardMediaUsesNewClientMid(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
oldStore := store.Store
mockStore := mock_store.NewMockPersistentStorageInterface(ctrl)
tokenAuth := mock_auth.NewMockAuthHandler(ctrl)
mockStore.EXPECT().GetLogicalAuthHandler("token").Return(tokenAuth)
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("media-mid")
store.Store = mockStore
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_img",
UserID: 42,
ItemType: "image",
SourceTopic: "grpAlpha",
SourceSeq: 17,
Snapshot: map[string]any{
"head": map[string]any{"type": "image", "x-im-type": "image", "x-im-client-mid": "origin-mid"},
"content": map[string]any{"file_id": "file-img", "mime": "image/png"},
},
Availability: "available",
CollectedAt: now,
}}}
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), "grpBeta", types.Uid(42), false).Return(&types.Subscription{
TenantID: 7, User: types.Uid(42).String(), Topic: "grpBeta", 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.Head["x-im-type"] != "image" {
t.Fatalf("unexpected media forward message: %#v", msg)
}
if msg.Head["x-im-client-mid"] != "fwd_media-mid" {
t.Fatalf("media forward reused original client mid: %#v", msg.Head)
}
content := msg.Content.(map[string]any)
if content["file_id"] != "file-img" || content["mime"] != "image/png" {
t.Fatalf("unexpected media forward content: %#v", content)
}
return &types.MessageSaveResult{SeqId: 22, MarkedReadBySender: true}, nil
})
req := httptest.NewRequest(http.MethodPost, "/v1/im/favorites/forward", bytes.NewBufferString(`{"favorite_ids":["fav_img"],"target_topics":["grpBeta"]}`))
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"] != "sent" || got["seq"].(float64) != 22 || got["client_mid"] != "fwd_media-mid" {
t.Fatalf("unexpected media forward response: %#v", got)
}
}
func TestServeIMFavoriteForwardRejectsUnauthorizedTarget(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -5511,7 +5646,7 @@ func TestServeIMMessageForwardKafkaAssignsSeqAndBroadcasts(t *testing.T) {
t.Fatalf("unexpected forward response: %#v", data)
}
got := items[0].(map[string]any)
if got["target_topic"] != targetTopic || got["seq"].(float64) != 35 {
if got["target_topic"] != targetTopic || got["seq"].(float64) != 35 || got["status"] != "pending" || got["reason"] != "MESSAGE_PERSIST_PENDING" {
t.Fatalf("unexpected forward item: %#v", got)
}
if failed := data["failed"].([]any); len(failed) != 0 {
@@ -5525,29 +5660,8 @@ func TestServeIMMessageForwardKafkaAssignsSeqAndBroadcasts(t *testing.T) {
}
select {
case delivered := <-globals.hub.routeSrv:
if delivered.RcptTo != targetTopic || delivered.Data == nil || delivered.Data.SeqId != 35 {
t.Fatalf("unexpected broadcast message: %#v", delivered)
}
t.Fatalf("pending forward should not broadcast immediately: %#v", delivered)
default:
t.Fatal("expected forwarded message to be broadcast")
}
presByUser := make(map[string]*MsgServerPres)
for range 2 {
select {
case delivered := <-globals.hub.routeSrv:
if delivered.Pres == nil {
t.Fatalf("unexpected non-presence message: %#v", delivered)
}
presByUser[delivered.RcptTo] = delivered.Pres
default:
t.Fatal("expected forwarded message presence notifications")
}
}
if pres := presByUser[types.Uid(42).UserId()]; pres == nil || pres.Topic != "me" || pres.What != "msg" || pres.Src != targetUser.UserId() || pres.SeqId != 35 || pres.AcsActor != "" || pres.SkipTopic != targetTopic {
t.Fatalf("unexpected sender presence: %#v", pres)
}
if pres := presByUser[targetUser.UserId()]; pres == nil || pres.Topic != "me" || pres.What != "msg" || pres.Src != types.Uid(42).UserId() || pres.SeqId != 35 || pres.AcsActor != types.Uid(42).UserId() || pres.SkipTopic != targetTopic {
t.Fatalf("unexpected receiver presence: %#v", pres)
}
}