243 lines
7.9 KiB
Go
243 lines
7.9 KiB
Go
package jpush
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tinode/chat/server/push"
|
|
"github.com/tinode/chat/server/store"
|
|
"github.com/tinode/chat/server/store/types"
|
|
)
|
|
|
|
type testThirdPartyConfigStore map[string]string
|
|
|
|
func (s testThirdPartyConfigStore) Get(_ string, keys ...string) (map[string]string, error) {
|
|
result := make(map[string]string, len(keys))
|
|
for _, key := range keys {
|
|
if value, ok := s[key]; ok {
|
|
result[key] = value
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
type testDevicePushTokenStore struct {
|
|
devices map[types.Uid][]types.DeviceDef
|
|
}
|
|
|
|
func (s *testDevicePushTokenStore) Upsert(types.TenantID, *types.DevicePushToken) (*types.DevicePushToken, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *testDevicePushTokenStore) GetAllByProvider(types.TenantID, string, ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error) {
|
|
count := 0
|
|
for _, devices := range s.devices {
|
|
count += len(devices)
|
|
}
|
|
return s.devices, count, nil
|
|
}
|
|
|
|
func TestNormalizeConfigLoadsThirdPartyConfig(t *testing.T) {
|
|
oldConfig := store.ThirdPartyConfig
|
|
store.ThirdPartyConfig = testThirdPartyConfigStore{
|
|
"jpush_enabled": "1",
|
|
"jpush_app_key": "app-key",
|
|
"jpush_master_secret": "master",
|
|
"jpush_api_domain": "https://push.example.com/",
|
|
"jpush_push_path": "v3/push",
|
|
"jpush_default_ttl_seconds": "123",
|
|
"jpush_call_ttl_seconds": "45",
|
|
"jpush_use_alias": "false",
|
|
"jpush_classification": "1",
|
|
}
|
|
defer func() { store.ThirdPartyConfig = oldConfig }()
|
|
|
|
var config configType
|
|
loadThirdPartyConfig(&config)
|
|
config, err := normalizeConfig(config)
|
|
if err != nil {
|
|
t.Fatalf("normalize config: %v", err)
|
|
}
|
|
if !config.Enabled || config.AppKey != "app-key" || config.MasterSecret != "master" {
|
|
t.Fatalf("unexpected credentials: %#v", config)
|
|
}
|
|
if config.APIDomain != "https://push.example.com" || config.PushPath != "/v3/push" {
|
|
t.Fatalf("unexpected endpoint: %#v", config)
|
|
}
|
|
if config.DefaultTTLSeconds != 123 || config.CallTTLSeconds != 45 || config.UseAlias == nil || *config.UseAlias {
|
|
t.Fatalf("unexpected ttl/use_alias: %#v", config)
|
|
}
|
|
}
|
|
|
|
func TestPreparePayloadsUsesAliasAndNormalizesP2PTopic(t *testing.T) {
|
|
caller := types.Uid(42)
|
|
callee := types.Uid(43)
|
|
topic := caller.P2PName(callee)
|
|
config := &configType{Title: "MeetingIM", DefaultTTLSeconds: 86400, CallTTLSeconds: 60}
|
|
payloads, uids := preparePayloads(&push.Receipt{
|
|
TenantID: 7,
|
|
To: map[types.Uid]push.Recipient{
|
|
callee: {Unread: 3},
|
|
},
|
|
Payload: push.Payload{
|
|
What: push.ActMsg,
|
|
Topic: topic,
|
|
From: caller.UserId(),
|
|
SeqId: 12,
|
|
ContentType: "text/plain",
|
|
Content: map[string]any{"text": "hello"},
|
|
Timestamp: time.Date(2026, 9, 4, 10, 0, 0, 0, time.UTC),
|
|
},
|
|
}, config)
|
|
if len(payloads) != 1 || len(uids) != 1 || uids[0] != callee {
|
|
t.Fatalf("unexpected payload count: %d %#v", len(payloads), uids)
|
|
}
|
|
payload := payloads[0]
|
|
if got := payload.Audience["alias"][0]; got != aliasForUid(callee) {
|
|
t.Fatalf("unexpected alias: %s", got)
|
|
}
|
|
if got := payload.Notification.Android.Extras["topic"]; got != topic {
|
|
t.Fatalf("topic was not kept canonical for callee: got %s want %s", got, topic)
|
|
}
|
|
if payload.Notification.IOS.Badge != "3" {
|
|
t.Fatalf("unexpected badge: %s", payload.Notification.IOS.Badge)
|
|
}
|
|
}
|
|
|
|
func TestAliasForUidUsesJPushSafeCharacters(t *testing.T) {
|
|
uid := types.ParseUserId("usrd6Ag5opa-ww")
|
|
if uid.IsZero() {
|
|
t.Fatal("test user id did not parse")
|
|
}
|
|
if got, want := aliasForUid(uid), "usrd6Ag5opa.ww"; got != want {
|
|
t.Fatalf("unexpected alias: got %s want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestPreparePayloadsCanUseRegistrationID(t *testing.T) {
|
|
oldTokens := store.DevicePushTokens
|
|
uid := types.Uid(42)
|
|
store.DevicePushTokens = &testDevicePushTokenStore{devices: map[types.Uid][]types.DeviceDef{
|
|
uid: {
|
|
{DeviceId: "online-token", Platform: "ios"},
|
|
{DeviceId: "offline-token", Platform: "android"},
|
|
},
|
|
}}
|
|
defer func() { store.DevicePushTokens = oldTokens }()
|
|
useAlias := false
|
|
config := &configType{Title: "MeetingIM", DefaultTTLSeconds: 86400, CallTTLSeconds: 60, UseAlias: &useAlias}
|
|
payloads, _ := preparePayloads(&push.Receipt{
|
|
TenantID: 7,
|
|
To: map[types.Uid]push.Recipient{
|
|
uid: {Devices: []string{"online-token"}},
|
|
},
|
|
Payload: push.Payload{What: push.ActMsg, Topic: "grpTopic", SeqId: 9, Timestamp: time.Now()},
|
|
}, config)
|
|
if len(payloads) != 1 {
|
|
t.Fatalf("unexpected payload count: %d", len(payloads))
|
|
}
|
|
got := payloads[0].Audience["registration_id"]
|
|
if len(got) != 1 || got[0] != "offline-token" {
|
|
t.Fatalf("unexpected registration ids: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestPreparePayloadsDoesNotDropRecipientWithDeliveredSession(t *testing.T) {
|
|
oldTokens := store.DevicePushTokens
|
|
uid := types.Uid(42)
|
|
store.DevicePushTokens = &testDevicePushTokenStore{devices: map[types.Uid][]types.DeviceDef{
|
|
uid: {{DeviceId: "push-token", Platform: "ios"}},
|
|
}}
|
|
defer func() { store.DevicePushTokens = oldTokens }()
|
|
useAlias := false
|
|
config := &configType{Title: "MeetingIM", DefaultTTLSeconds: 86400, CallTTLSeconds: 60, UseAlias: &useAlias}
|
|
payloads, _ := preparePayloads(&push.Receipt{
|
|
TenantID: 7,
|
|
To: map[types.Uid]push.Recipient{
|
|
uid: {Delivered: 1},
|
|
},
|
|
Payload: push.Payload{What: push.ActMsg, Topic: "grpTopic", SeqId: 9, Timestamp: time.Now()},
|
|
}, config)
|
|
if len(payloads) != 1 {
|
|
t.Fatalf("unexpected payload count: %d", len(payloads))
|
|
}
|
|
if payloads[0].Message == nil || payloads[0].Message.Extras["silent"] != "true" {
|
|
t.Fatalf("expected silent payload for delivered recipient: %#v", payloads[0])
|
|
}
|
|
}
|
|
|
|
func TestBuildPayloadUsesShortTTLForCalls(t *testing.T) {
|
|
config := &configType{Title: "MeetingIM", DefaultTTLSeconds: 86400, CallTTLSeconds: 60}
|
|
payload := buildPayload(map[string][]string{"alias": {"usr1"}}, "p2pTopic", map[string]string{
|
|
"type": "call_invite",
|
|
"topic": "p2pTopic",
|
|
"seq": "30",
|
|
"call_type": "audio",
|
|
}, 0, config)
|
|
if payload.Options.TimeToLive != 60 {
|
|
t.Fatalf("unexpected ttl: %d", payload.Options.TimeToLive)
|
|
}
|
|
if payload.Notification.Android.Title != "语音通话" {
|
|
t.Fatalf("unexpected title: %s", payload.Notification.Android.Title)
|
|
}
|
|
}
|
|
|
|
func TestPushDataExtractsCallFields(t *testing.T) {
|
|
data, err := pushData(push.Payload{
|
|
What: push.ActMsg,
|
|
Topic: "p2pTopic",
|
|
From: "usrCaller",
|
|
SeqId: 30,
|
|
Webrtc: "started",
|
|
Content: map[string]any{
|
|
"callId": "call_1",
|
|
"callType": "audio",
|
|
},
|
|
Timestamp: time.Date(2026, 9, 4, 10, 0, 0, 0, time.UTC),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("push data: %v", err)
|
|
}
|
|
if data["type"] != "call_invite" || data["call_id"] != "call_1" || data["call_type"] != "audio" || data["caller_id"] != "usrCaller" {
|
|
t.Fatalf("unexpected call data: %#v", data)
|
|
}
|
|
}
|
|
|
|
func TestSendPayloadUsesBasicAuth(t *testing.T) {
|
|
var gotAuth string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotAuth = r.Header.Get("Authorization")
|
|
if r.URL.Path != "/v3/push" {
|
|
t.Fatalf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
var payload jpushPayload
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode payload: %v", err)
|
|
}
|
|
if payload.Audience["alias"][0] != "usr1" {
|
|
t.Fatalf("unexpected audience: %#v", payload.Audience)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"sendno":"0","msg_id":"1"}`))
|
|
}))
|
|
defer srv.Close()
|
|
config := &configType{AppKey: "key", MasterSecret: "secret", APIDomain: srv.URL, PushPath: "/v3/push"}
|
|
err := sendPayload(&jpushPayload{
|
|
Platform: "all",
|
|
Audience: map[string][]string{"alias": {"usr1"}},
|
|
Message: &jpushMessage{MsgContent: "hello"},
|
|
Options: jpushOptions{TimeToLive: 60},
|
|
}, config, srv.Client())
|
|
if err != nil {
|
|
t.Fatalf("send payload: %v", err)
|
|
}
|
|
wantAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("key:secret"))
|
|
if gotAuth != wantAuth {
|
|
t.Fatalf("unexpected auth: got %s want %s", gotAuth, wantAuth)
|
|
}
|
|
}
|