Files
backend/internal/pkg/jpush/vendor/payload_compat_test.go
lafay 821e066446
All checks were successful
Build Backend / build (push) Successful in 3m4s
Build Backend / build-docker (push) Successful in 1m15s
feat(jpush): add vendor-specific message classification support for push notifications
- Add OPPO category/notify_level support (2024.11.20 regulation: category required when notify_level present)
- Add Honor importance field (NORMAL=service/LOW=marketing) for notification classification
- Add vivo category field (IM/ACCOUNT) for message scenario identification
- Set Xiaomi channel/template defaults in code (system=153609, chat=153608, templates P10761/M10289)
- Add classification option to JPush push payloads (0=operation, 1=system)
- Update xiaomi template keywords mapping to match actual template placeholders
- Add keyword search support for GetFollowers and GetFollowing endpoints
- Bind new config fields to environment variables for OPPO/Honor/vivo
2026-06-18 19:41:32 +08:00

327 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package vendor
import (
"encoding/json"
"strings"
"testing"
"with_you/internal/config"
)
// TestPayloadEmptyConfigMatchesLegacy 验证:未配置任何 channel/模板时,
// Factory 产出的推送 payload 与改动前 BuildNotification 行为一致。
//
// 改动前 BuildNotification(title, body, "message", extras) 产出:
// notification: { alert, android:{alert,title,extras:{notification_type}+extras,priority:1}, ios:{alert:{title,body},sound:default,badge:+1,extras} }
// options: { time_to_live: 86400, apns_production } // 无 third_party_channel
func TestPayloadEmptyConfigMatchesLegacy(t *testing.T) {
factory := NewFactory()
cfg := config.ChannelConfig{} // 全空配置
params := MessageParams{
Scene: SceneChat,
Title: "张三",
Body: "你好",
MessageType: "message",
Extra: map[string]any{
"message_id": "m1",
"conversation_id": "c1",
"sender_id": "u1",
"category": "chat",
},
}
payload := factory.Build(params, cfg)
// 1. TPC 必须为 nil无 third_party_channel 下发,与改动前一致)
if payload.TPC != nil {
t.Fatalf("TPC should be nil when all channel config empty, got: %+v", payload.TPC)
}
notif := payload.Notification
if notif == nil || notif.Android == nil || notif.IOS == nil {
t.Fatalf("notification/android/ios must not be nil")
}
// 2. Android 字段对比
if notif.Android.Alert != "你好" || notif.Android.Title != "张三" || notif.Android.Priority != 1 {
t.Errorf("Android basic fields mismatch: %+v", notif.Android)
}
if notif.Android.ChannelID != "" {
t.Errorf("Android.ChannelID = %q, want empty", notif.Android.ChannelID)
}
if notif.Android.Extras["notification_type"] != "message" || notif.Android.Extras["message_id"] != "m1" {
t.Errorf("Android.Extras mismatch: %+v", notif.Android.Extras)
}
// 3. iOS 字段对比
if notif.IOS.Sound != "default" || notif.IOS.Badge != "+1" {
t.Errorf("IOS basic fields mismatch: %+v", notif.IOS)
}
if notif.IOS.ThreadID != "" {
t.Errorf("IOS.ThreadID = %q, want empty", notif.IOS.ThreadID)
}
if notif.IOS.Extras["notification_type"] != "message" {
t.Errorf("IOS.Extras[notification_type] mismatch: %+v", notif.IOS.Extras)
}
// 4. 序列化 JSON 确认厂商私有字段不出现
jsonBytes, err := json.Marshal(notif)
if err != nil {
t.Fatalf("marshal: %v", err)
}
jsonStr := string(jsonBytes)
for _, key := range []string{`"channel_id"`, `"thread-id"`, `"third_party_channel"`, `"mi_template"`, `"private_msg_template_id"`} {
if strings.Contains(jsonStr, key) {
t.Errorf("notification JSON should not contain %s when config empty, got: %s", key, jsonStr)
}
}
t.Logf("notification JSON: %s", jsonStr)
// 5. classification 必须为系统消息1聊天即时消息统一归系统消息避免限速
if payload.Classification != ClassificationSystem {
t.Errorf("Classification = %d, want %d (system)", payload.Classification, ClassificationSystem)
}
}
// TestClassificationByScene 验证system 与 chat 场景的 classification 均为 1系统消息
// 聊天即时消息归为系统消息,避免厂商运营消息限速影响送达
func TestClassificationByScene(t *testing.T) {
factory := NewFactory()
cfg := config.ChannelConfig{}
for _, scene := range []string{SceneSystem, SceneChat} {
params := MessageParams{Scene: scene, Title: "T", Body: "B"}
payload := factory.Build(params, cfg)
if payload.Classification != ClassificationSystem {
t.Errorf("scene=%s: Classification = %d, want %d", scene, payload.Classification, ClassificationSystem)
}
}
}
// TestPayloadWithChannelConfig 验证:配置 channel_id 后正确下发,且不影响通用通知字段
func TestPayloadWithChannelConfig(t *testing.T) {
factory := NewFactory()
cfg := config.ChannelConfig{
System: "sys_channel",
Chat: "chat_channel",
}
params := MessageParams{Scene: SceneChat, Title: "T", Body: "B", MessageType: "chat_message"}
payload := factory.Build(params, cfg)
// 通用通知仍正常
if payload.Notification.Android.Title != "T" {
t.Errorf("Android.Title = %q, want T", payload.Notification.Android.Title)
}
// Android channel_id 跟随场景chat
if payload.Notification.Android.ChannelID != "chat_channel" {
t.Errorf("Android.ChannelID = %q, want chat_channel", payload.Notification.Android.ChannelID)
}
// TPC 各厂商 channel_id 下发(华为/VIVO 等回退全局)
if payload.TPC == nil {
t.Fatal("TPC should not be nil when channel config present")
}
if payload.TPC.Huawei["channel_id"] != "chat_channel" {
t.Errorf("Huawei channel_id = %v, want chat_channel", payload.TPC.Huawei["channel_id"])
}
t.Logf("TPC JSON: %s", mustJSON(payload.TPC))
}
// TestPayloadXiaomiTemplateOptional 验证:仅配 channel_id 不配模板时,小米不下发模板字段
func TestPayloadXiaomiTemplateOptional(t *testing.T) {
factory := NewFactory()
cfg := config.ChannelConfig{
Vendor: config.VendorChannel{
Xiaomi: config.VendorChannelID{Chat: "mi_chat"},
},
}
params := MessageParams{Scene: SceneChat, Title: "T", Body: "B"}
payload := factory.Build(params, cfg)
if payload.TPC == nil || payload.TPC.Xiaomi == nil {
t.Fatal("Xiaomi TPC should exist")
}
if _, ok := payload.TPC.Xiaomi["mi_template_id"]; ok {
t.Error("Xiaomi should NOT contain mi_template_id when template not configured")
}
if payload.TPC.Xiaomi["channel_id"] != "mi_chat" {
t.Errorf("Xiaomi channel_id = %v, want mi_chat", payload.TPC.Xiaomi["channel_id"])
}
}
// TestOppoCategoryNotifyLevel 验证OPPO category/notify_level 按场景区分,且 notify_level 必须伴 category
func TestOppoCategoryNotifyLevel(t *testing.T) {
factory := NewFactory()
// 场景1chat 用 IM + 16强提醒
cfgChat := config.ChannelConfig{
Vendor: config.VendorChannel{
OPPO: config.VendorChannelID{
Chat: "oppo_chat",
OppoChatCategory: "IM",
OppoChatNotifyLevel: 16,
},
},
}
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfgChat)
if payload.TPC == nil || payload.TPC.OPPO == nil {
t.Fatal("OPPO TPC should exist for chat scene")
}
if payload.TPC.OPPO["category"] != "IM" {
t.Errorf("OPPO chat category = %v, want IM", payload.TPC.OPPO["category"])
}
if payload.TPC.OPPO["notify_level"] != 16 {
t.Errorf("OPPO chat notify_level = %v, want 16", payload.TPC.OPPO["notify_level"])
}
// 场景2system 用 ACCOUNT + 2通知栏+锁屏)
cfgSys := config.ChannelConfig{
Vendor: config.VendorChannel{
OPPO: config.VendorChannelID{
System: "oppo_sys",
OppoSystemCategory: "ACCOUNT",
OppoSystemNotifyLevel: 2,
},
},
}
payload = factory.Build(MessageParams{Scene: SceneSystem, Title: "T", Body: "B"}, cfgSys)
if payload.TPC.OPPO["category"] != "ACCOUNT" {
t.Errorf("OPPO system category = %v, want ACCOUNT", payload.TPC.OPPO["category"])
}
if payload.TPC.OPPO["notify_level"] != 2 {
t.Errorf("OPPO system notify_level = %v, want 2", payload.TPC.OPPO["notify_level"])
}
}
// TestOppoNotifyLevelRequiresCategory 验证notify_level 配置但 category 未配置时,跳过 notify_level避免 OPPO 校验失败)
func TestOppoNotifyLevelRequiresCategory(t *testing.T) {
factory := NewFactory()
cfg := config.ChannelConfig{
Vendor: config.VendorChannel{
OPPO: config.VendorChannelID{
Chat: "oppo_chat",
OppoChatNotifyLevel: 16, // 只配 notify_level不配 category
},
},
}
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfg)
if payload.TPC == nil || payload.TPC.OPPO == nil {
t.Fatal("OPPO TPC should exist (has channel_id)")
}
if _, ok := payload.TPC.OPPO["notify_level"]; ok {
t.Error("OPPO should NOT contain notify_level when category not configured")
}
if _, ok := payload.TPC.OPPO["category"]; ok {
t.Error("OPPO should NOT contain category when not configured")
}
// channel_id 仍正常下发
if payload.TPC.OPPO["channel_id"] != "oppo_chat" {
t.Errorf("OPPO channel_id = %v, want oppo_chat", payload.TPC.OPPO["channel_id"])
}
}
// TestHonorImportanceByScene 验证:荣耀 importance 按场景区分chat=NORMAL, system=LOW
func TestHonorImportanceByScene(t *testing.T) {
factory := NewFactory()
cfg := config.ChannelConfig{
Vendor: config.VendorChannel{
Honor: config.VendorChannelID{
Chat: "honor_chat",
System: "honor_sys",
HonorChatImportance: "NORMAL",
HonorSystemImportance: "LOW",
},
},
}
// chat 场景 -> NORMAL
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfg)
if payload.TPC == nil || payload.TPC.Honor == nil {
t.Fatal("Honor TPC should exist for chat scene")
}
if payload.TPC.Honor["importance"] != "NORMAL" {
t.Errorf("Honor chat importance = %v, want NORMAL", payload.TPC.Honor["importance"])
}
// system 场景 -> LOW
payload = factory.Build(MessageParams{Scene: SceneSystem, Title: "T", Body: "B"}, cfg)
if payload.TPC.Honor["importance"] != "LOW" {
t.Errorf("Honor system importance = %v, want LOW", payload.TPC.Honor["importance"])
}
}
// TestHonorImportanceOptional 验证:未配置 importance 时不下发,仅 channel_id
func TestHonorImportanceOptional(t *testing.T) {
factory := NewFactory()
cfg := config.ChannelConfig{
Vendor: config.VendorChannel{
Honor: config.VendorChannelID{Chat: "honor_chat"},
},
}
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfg)
if payload.TPC == nil || payload.TPC.Honor == nil {
t.Fatal("Honor TPC should exist (has channel_id)")
}
if _, ok := payload.TPC.Honor["importance"]; ok {
t.Error("Honor should NOT contain importance when not configured")
}
if payload.TPC.Honor["channel_id"] != "honor_chat" {
t.Errorf("Honor channel_id = %v, want honor_chat", payload.TPC.Honor["channel_id"])
}
}
// TestVivoCategoryByScene 验证vivo category 按场景区分
func TestVivoCategoryByScene(t *testing.T) {
factory := NewFactory()
cfg := config.ChannelConfig{
Vendor: config.VendorChannel{
VIVO: config.VendorChannelID{
Chat: "vivo_chat",
System: "vivo_sys",
VivoChatCategory: "IM",
VivoSystemCategory: "ACCOUNT",
},
},
}
// chat 场景 -> IM
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfg)
if payload.TPC == nil || payload.TPC.VIVO == nil {
t.Fatal("VIVO TPC should exist for chat scene")
}
if payload.TPC.VIVO["category"] != "IM" {
t.Errorf("VIVO chat category = %v, want IM", payload.TPC.VIVO["category"])
}
// system 场景 -> ACCOUNT
payload = factory.Build(MessageParams{Scene: SceneSystem, Title: "T", Body: "B"}, cfg)
if payload.TPC.VIVO["category"] != "ACCOUNT" {
t.Errorf("VIVO system category = %v, want ACCOUNT", payload.TPC.VIVO["category"])
}
}
// TestVivoCategoryOptional 验证:未配置 category 时不下发,仅 channel_id
func TestVivoCategoryOptional(t *testing.T) {
factory := NewFactory()
cfg := config.ChannelConfig{
Vendor: config.VendorChannel{
VIVO: config.VendorChannelID{Chat: "vivo_chat"},
},
}
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfg)
if payload.TPC == nil || payload.TPC.VIVO == nil {
t.Fatal("VIVO TPC should exist (has channel_id)")
}
if _, ok := payload.TPC.VIVO["category"]; ok {
t.Error("VIVO should NOT contain category when not configured")
}
if payload.TPC.VIVO["channel_id"] != "vivo_chat" {
t.Errorf("VIVO channel_id = %v, want vivo_chat", payload.TPC.VIVO["channel_id"])
}
}
func mustJSON(v any) string {
b, _ := json.Marshal(v)
return string(b)
}