feat: add safety reporting APIs
This commit is contained in:
+172
-1
@@ -112,6 +112,13 @@ type testDevicePushTokenStore struct {
|
||||
token *types.DevicePushToken
|
||||
}
|
||||
|
||||
type testSafetyStore struct {
|
||||
riskChallenge *types.RiskChallenge
|
||||
reasons []types.AbuseReason
|
||||
abuseReport *types.AbuseReport
|
||||
feedback *types.FeedbackSubmission
|
||||
}
|
||||
|
||||
func (s *testWorkbenchStore) Get(_ types.TenantID, _ types.Uid) ([]types.WorkbenchModule, int, error) {
|
||||
if s.version == 0 {
|
||||
s.version = 1
|
||||
@@ -370,6 +377,65 @@ func (s *testDevicePushTokenStore) Upsert(_ types.TenantID, token *types.DeviceP
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s *testSafetyStore) CreateRiskChallenge(_ types.TenantID, challenge *types.RiskChallenge) (*types.RiskChallenge, error) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
challenge.CreatedAt = now
|
||||
challenge.UpdatedAt = now
|
||||
s.riskChallenge = challenge
|
||||
return challenge, nil
|
||||
}
|
||||
|
||||
func (s *testSafetyStore) ConfirmRiskChallenge(_ types.TenantID, uid types.Uid, challengeID, decision, actionTokenID string, now time.Time) (*types.RiskChallenge, error) {
|
||||
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)}
|
||||
}
|
||||
s.riskChallenge.State = types.RiskChallengeStateCanceled
|
||||
if decision == "continue" {
|
||||
s.riskChallenge.State = types.RiskChallengeStateContinued
|
||||
}
|
||||
s.riskChallenge.Decision = decision
|
||||
s.riskChallenge.ActionTokenID = actionTokenID
|
||||
s.riskChallenge.UpdatedAt = now
|
||||
return s.riskChallenge, nil
|
||||
}
|
||||
|
||||
func (s *testSafetyStore) AbuseReasons(_ types.TenantID, targetType string) ([]types.AbuseReason, error) {
|
||||
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) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
report.Status = types.AbuseReportStatusReceived
|
||||
report.CreatedAt = now
|
||||
report.UpdatedAt = now
|
||||
s.abuseReport = report
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func (s *testSafetyStore) FeedbackConfig(types.TenantID, types.Uid) (*types.FeedbackConfig, error) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
return &types.FeedbackConfig{
|
||||
Categories: []string{"bug", "suggestion"},
|
||||
MaxLength: 1000,
|
||||
AttachmentPolicy: map[string]any{"max_files": float64(5), "purpose": "feedback"},
|
||||
AllowContact: true,
|
||||
DiagnosticsNotice: "仅在同意后上传诊断信息。",
|
||||
UpdatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *testSafetyStore) CreateFeedback(_ types.TenantID, feedback *types.FeedbackSubmission, _ string) (*types.FeedbackSubmission, error) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
feedback.Status = types.FeedbackStatusReceived
|
||||
feedback.CreatedAt = now
|
||||
feedback.UpdatedAt = now
|
||||
s.feedback = feedback
|
||||
return feedback, nil
|
||||
}
|
||||
|
||||
func (s *testCallsStore) Create(_ types.TenantID, _ *types.CallRecord) error {
|
||||
return nil
|
||||
}
|
||||
@@ -2435,6 +2501,7 @@ func TestServeIMSecurityURLCheck(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
oldStore := store.Store
|
||||
oldSafety := store.Safety
|
||||
mockStore := mock_store.NewMockPersistentStorageInterface(ctrl)
|
||||
tokenAuth := mock_auth.NewMockAuthHandler(ctrl)
|
||||
mockStore.EXPECT().GetLogicalAuthHandler("token").Return(tokenAuth).Times(2)
|
||||
@@ -2442,7 +2509,11 @@ func TestServeIMSecurityURLCheck(t *testing.T) {
|
||||
tokenAuth.EXPECT().Authenticate(auth.AuthContext{}, []byte("token-1")).
|
||||
Return(&auth.Rec{TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth}, nil, nil).Times(2)
|
||||
store.Store = mockStore
|
||||
defer func() { store.Store = oldStore }()
|
||||
store.Safety = &testSafetyStore{}
|
||||
defer func() {
|
||||
store.Store = oldStore
|
||||
store.Safety = oldSafety
|
||||
}()
|
||||
|
||||
allowReq := httptest.NewRequest(http.MethodPost, "/v1/im/security/urls/check", bytes.NewBufferString(`{"url":"https://example.com/a","source":"message"}`))
|
||||
allowReq.Header.Set("Authorization", "Bearer token-1")
|
||||
@@ -2467,6 +2538,106 @@ func TestServeIMSecurityURLCheck(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMRiskChallengeConfirm(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldSafety := store.Safety
|
||||
oldSecurity := store.Security
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
store.Safety = &testSafetyStore{riskChallenge: &types.RiskChallenge{
|
||||
ChallengeID: "risk-1", UserID: 42, Action: "open_url", Level: "high", NoticeCode: "url_high",
|
||||
State: types.RiskChallengeStatePending, ExpiresAt: now.Add(time.Minute), CreatedAt: now, UpdatedAt: now,
|
||||
}}
|
||||
store.Security = &testSecurityStore{}
|
||||
defer func() {
|
||||
store.Safety = oldSafety
|
||||
store.Security = oldSecurity
|
||||
}()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/security/risk-challenges/risk-1/confirm", bytes.NewBufferString(`{"decision":"continue"}`))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMRiskChallengeConfirm(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["state"] != types.RiskChallengeStateContinued || data["risk_action_token"] == "" {
|
||||
t.Fatalf("unexpected risk confirm response: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMAbuseReportsAndReasons(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldSafety := store.Safety
|
||||
oldUsers := store.Users
|
||||
safety := &testSafetyStore{}
|
||||
store.Safety = safety
|
||||
users := mock_store.NewMockUsersPersistenceInterface(ctrl)
|
||||
store.Users = users
|
||||
users.EXPECT().Get(types.TenantID(7), types.Uid(43)).Return(&types.User{}, nil).AnyTimes()
|
||||
defer func() {
|
||||
store.Safety = oldSafety
|
||||
store.Users = oldUsers
|
||||
}()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/reports/abuse/reasons?target_type=user", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMAbuseReasons(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("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")
|
||||
req.Header.Set("Idempotency-Key", "abuse-1")
|
||||
rec = httptest.NewRecorder()
|
||||
serveIMAbuseReports(rec, req)
|
||||
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" {
|
||||
t.Fatalf("unexpected abuse report: %#v", safety.abuseReport)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMFeedback(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
expectIMBearer(t, ctrl, "token-1", 7, 42)
|
||||
oldSafety := store.Safety
|
||||
safety := &testSafetyStore{}
|
||||
store.Safety = safety
|
||||
defer func() { store.Safety = oldSafety }()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/feedback/config", nil)
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
rec := httptest.NewRecorder()
|
||||
serveIMFeedbackConfig(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("config status: got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
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}`))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
req.Header.Set("Idempotency-Key", "feedback-1")
|
||||
rec = httptest.NewRecorder()
|
||||
serveIMFeedback(rec, req)
|
||||
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 {
|
||||
t.Fatalf("unexpected feedback: %#v", safety.feedback)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeIMMessageSearchContextAndMedia(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
Reference in New Issue
Block a user