package moderation import ( "context" "sort" "strings" "testing" "time" "github.com/tinode/chat/server/store" "github.com/tinode/chat/server/store/types" ) type testConfig struct { policy *types.ModerationPolicy words *types.SensitiveWordSet } func (c testConfig) Provider() (*types.ModerationProviderConfig, error) { return &types.ModerationProviderConfig{}, nil } func (c testConfig) Policy(string) (*types.ModerationPolicy, error) { return c.policy, nil } func (c testConfig) Words(string) (*types.SensitiveWordSet, error) { return c.words, nil } func (c testConfig) SecurityPolicy(string) (*types.SecurityPolicy, error) { return nil, nil } type testAudit struct{} func (testAudit) RecordLogin(types.TenantID, *types.LoginAuditEvent) error { return nil } func (testAudit) RecordLoginFailure(types.TenantID, *types.LoginFailureEvent) error { return nil } func (testAudit) CloseLogin(types.TenantID, string, time.Time) error { return nil } func (testAudit) ApplyMessageAction(types.TenantID, *types.MessageAdminAction) error { return nil } func (testAudit) RecordContentModeration(types.TenantID, *types.ContentModerationLog) error { return nil } func TestExtractText(t *testing.T) { content := map[string]any{"txt": "正文", "ent": []any{map[string]any{"data": map[string]any{"title": "查看详情", "url": "https://example.com"}}}, "meta": "ignore"} gotParts := strings.Split(ExtractText(content), "\n") sort.Strings(gotParts) wantParts := []string{"https://example.com", "查看详情", "正文"} sort.Strings(wantParts) if strings.Join(gotParts, "\n") != strings.Join(wantParts, "\n") { got := ExtractText(content) t.Fatalf("ExtractText()=%q", got) } } func TestReviewLocalBlock(t *testing.T) { oldConfig, oldAudit := store.ModerationConfig, store.SecurityAudit defer func() { store.ModerationConfig, store.SecurityAudit = oldConfig, oldAudit }() store.ModerationConfig = testConfig{policy: &types.ModerationPolicy{Enabled: true, LocalWordsEnabled: true, Scenes: map[string]bool{ScenePrivateMessage: true}, RiskActions: map[string]string{"high": ActionBlock}}, words: &types.SensitiveWordSet{Items: []*types.SensitiveWordRule{{Word: "违规词", MatchType: "contains", Scenes: []string{ScenePrivateMessage}, Action: ActionBlock, RiskLevel: "high"}}}} store.SecurityAudit = testAudit{} result, err := Review(context.Background(), Request{TenantID: 1, TenantCode: "default", Scene: ScenePrivateMessage, Text: "包含违规词的消息"}) if err != ErrRejected || result.Action != ActionBlock || result.Provider != "local" { t.Fatalf("result=%+v err=%v", result, err) } } func TestReviewOtherTenantUsesOwnPolicy(t *testing.T) { oldConfig, oldAudit := store.ModerationConfig, store.SecurityAudit defer func() { store.ModerationConfig, store.SecurityAudit = oldConfig, oldAudit }() store.ModerationConfig = testConfig{policy: &types.ModerationPolicy{Enabled: true, LocalWordsEnabled: true, Scenes: map[string]bool{ScenePrivateMessage: true}, RiskActions: map[string]string{"high": ActionBlock}}, words: &types.SensitiveWordSet{Items: []*types.SensitiveWordRule{{Word: "违规词", MatchType: "contains", Scenes: []string{ScenePrivateMessage}, Action: ActionBlock, RiskLevel: "high"}}}} store.SecurityAudit = testAudit{} result, err := Review(context.Background(), Request{TenantID: 2, TenantCode: "team", Scene: ScenePrivateMessage, Text: "违规词"}) if err != ErrRejected || result.Action != ActionBlock { t.Fatalf("result=%+v err=%v", result, err) } } func TestMatchLocalWordsPinyin(t *testing.T) { result := matchLocalWords("这里包含敏感内容", SceneGroupMessage, &types.SensitiveWordSet{Items: []*types.SensitiveWordRule{{Word: "mingan", MatchType: "pinyin", Scenes: []string{SceneGroupMessage}, RiskLevel: "medium"}}}) if result == nil || result.RiskLevel != "medium" { t.Fatalf("result=%+v", result) } }