package app import ( "encoding/json" "fmt" "net/http/httptest" "strings" "testing" ) // Voice and image messages can be limited to members from the admin console. // The switch has to hold on the real send path, not only in the UI. func TestMessageMembershipGateMySQL(t *testing.T) { db := isolatedIMDatabase(t) a := &App{db: db, hub: NewHub(), config: Config{Environment: "development", JWTSecret: "isolated-im-regression-secret-only"}} setGate := func(key, value string) { t.Helper() if _, err := db.Exec(`INSERT INTO system_configs(config_key,config_value,value_type,description) VALUES(?,?,'boolean','') ON DUPLICATE KEY UPDATE config_value=VALUES(config_value)`, key, value); err != nil { t.Fatal(err) } } for _, media := range []struct { url string kind string }{{"https://cdn.example.com/voice-1.m4a", "audio"}, {"https://cdn.example.com/image-1.jpg", "image"}} { if _, err := db.Exec(`INSERT INTO media_assets(owner_user_id,media_type,public_url,status,moderation_status) VALUES(1,?,?,1,1)`, media.kind, media.url); err != nil { t.Fatal(err) } } var conversation struct { ID int64 `json:"id"` } if err := json.Unmarshal(imTestCall(t, a.directConversation, 1, "POST", "/api/v1/im/conversations", `{"userId":2}`, 200), &conversation); err != nil { t.Fatal(err) } path := fmt.Sprintf("/api/v1/im/conversations/%d/messages", conversation.ID) voice := `{"clientMsgId":%q,"type":3,"content":{"url":"https://cdn.example.com/voice-1.m4a","duration":3}}` image := `{"clientMsgId":%q,"type":2,"content":{"url":"https://cdn.example.com/image-1.jpg"}}` send := func(payload, key string, wantStatus int) string { t.Helper() w := httptest.NewRecorder() a.sendMessageHTTP(w, imTestRequest(1, "POST", path, fmt.Sprintf(payload, key))) if w.Code != wantStatus { t.Fatalf("HTTP %d, want %d: %s", w.Code, wantStatus, w.Body.String()) } return w.Body.String() } member := func(active bool) { t.Helper() if _, err := db.Exec(`DELETE FROM subscriptions WHERE user_id=1`); err != nil { t.Fatal(err) } if !active { return } if _, err := db.Exec(`INSERT INTO subscriptions(user_id,plan_id,source,status,started_at,expires_at) VALUES(1,1,'test',1,NOW(3),DATE_ADD(NOW(3),INTERVAL 30 DAY))`); err != nil { t.Fatal(err) } } t.Run("both kinds are open while the switches are off", func(t *testing.T) { setGate("membership.voice_message_requires_vip", "false") setGate("membership.image_message_requires_vip", "false") send(voice, "open-voice", 200) send(image, "open-image", 200) }) t.Run("a non member is refused with its own code once the switch is on", func(t *testing.T) { setGate("membership.voice_message_requires_vip", "true") setGate("membership.image_message_requires_vip", "true") body := send(voice, "gated-voice", 402) if !strings.Contains(body, "30006") || !strings.Contains(body, "发送语音消息需要开通会员") { t.Fatalf("voice refusal = %s", body) } if body := send(image, "gated-image", 402); !strings.Contains(body, "发送图片消息需要开通会员") { t.Fatalf("image refusal = %s", body) } // Text is never gated: the two switches must not silence the chat. imTestCall(t, a.sendMessageHTTP, 1, "POST", path, `{"clientMsgId":"gated-text","type":1,"content":{"text":"仍然可以说话"}}`, 200) }) t.Run("each switch only gates its own kind", func(t *testing.T) { setGate("membership.voice_message_requires_vip", "true") setGate("membership.image_message_requires_vip", "false") send(voice, "half-voice", 402) send(image, "half-image", 200) }) t.Run("a member sends both kinds", func(t *testing.T) { setGate("membership.voice_message_requires_vip", "true") setGate("membership.image_message_requires_vip", "true") member(true) defer member(false) send(voice, "member-voice", 200) send(image, "member-image", 200) }) t.Run("the membership status tells the client before it records anything", func(t *testing.T) { setGate("membership.voice_message_requires_vip", "true") setGate("membership.image_message_requires_vip", "false") var status struct { Entitlements struct { VoiceRequiresVIP bool `json:"voiceMessageRequiresVip"` ImageRequiresVIP bool `json:"imageMessageRequiresVip"` CanSendVoice bool `json:"canSendVoiceMessage"` CanSendImage bool `json:"canSendImageMessage"` } `json:"entitlements"` } read := func(user int64) { t.Helper() if err := json.Unmarshal(imTestCall(t, a.membershipStatus, user, "GET", "/api/v1/membership/status", "", 200), &status); err != nil { t.Fatal(err) } } read(1) if !status.Entitlements.VoiceRequiresVIP || status.Entitlements.ImageRequiresVIP { t.Fatalf("switches not reported: %+v", status.Entitlements) } if status.Entitlements.CanSendVoice || !status.Entitlements.CanSendImage { t.Fatalf("non member capabilities: %+v", status.Entitlements) } member(true) defer member(false) read(1) if !status.Entitlements.CanSendVoice { t.Fatalf("member must be allowed to send voice: %+v", status.Entitlements) } }) }