feat(channels/weixin_ilink): 微信 iLink 渠道插件(扫码登录+长轮询+游标+context_token+AES-128-ECB 媒体+24h 主动推送窗口)
This commit is contained in:
1 parent
7831b0ea1b
commit
88dc12466d
13 files changed
+2627
No files matched your search
@@ -0,0 +1,184 @@
|
||||
package weixin_ilink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
)
|
||||
|
||||
func TestAdapter_Name_And_DefaultCapabilities(t *testing.T) {
|
||||
t.Parallel()
|
||||
ad, err := New(Config{BaseURL: "https://ilinkai.weixin.qq.com"}, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
if got := ad.Name(); got != "weixin-ilink" {
|
||||
t.Fatalf("Name() = %q, want weixin-ilink", got)
|
||||
}
|
||||
caps := ad.Capabilities()
|
||||
if !caps.Proactive {
|
||||
t.Error("Capabilities().Proactive = false, want true (iLink has a 24h window)")
|
||||
}
|
||||
if !caps.Image {
|
||||
t.Error("Capabilities().Image = false, want true")
|
||||
}
|
||||
if got := caps.Normalize().MaxMessageLength; got <= 0 {
|
||||
t.Fatalf("MaxMessageLength = %d, want > 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_New_RejectsBadBaseURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := New(Config{BaseURL: "not-a-url"}, nil, nil)
|
||||
if !errors.Is(err, ErrInvalidConfig) {
|
||||
t.Fatalf("err = %v, want ErrInvalidConfig", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_Config_ReturnsSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
ad, err := New(Config{BaseURL: "https://ilinkai.weixin.qq.com", BotID: "bot-1"}, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
cfg := ad.Config()
|
||||
if cfg["bot_id"] != "bot-1" {
|
||||
t.Fatalf("Config()[bot_id] = %v, want bot-1", cfg["bot_id"])
|
||||
}
|
||||
if cfg["base_url"] != "https://ilinkai.weixin.qq.com" {
|
||||
t.Fatalf("Config()[base_url] = %v", cfg["base_url"])
|
||||
}
|
||||
// Round-tripping through DecodeConfig must preserve the typed config.
|
||||
var back Config
|
||||
if err := adapter.DecodeConfig(cfg, &back); err != nil {
|
||||
t.Fatalf("DecodeConfig: %v", err)
|
||||
}
|
||||
if back.BaseURL != "https://ilinkai.weixin.qq.com" || back.BotID != "bot-1" {
|
||||
t.Fatalf("decoded config = %+v, want base_url/bot_id preserved", back)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_Connect_WithToken_IsIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
ad, err := New(Config{BaseURL: "https://ilinkai.weixin.qq.com", BotToken: "tok-1"}, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
if ad.IsConnected() {
|
||||
t.Fatal("IsConnected() = true before Connect")
|
||||
}
|
||||
if err := ad.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("Connect: %v", err)
|
||||
}
|
||||
if !ad.IsConnected() {
|
||||
t.Fatal("IsConnected() = false after Connect")
|
||||
}
|
||||
// Second call is a no-op success.
|
||||
if err := ad.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("second Connect: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_HealthCheck(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("disconnected", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ad, _ := New(Config{BaseURL: "https://ilinkai.weixin.qq.com"}, nil, nil)
|
||||
if err := ad.HealthCheck(context.Background()); !errors.Is(err, adapter.ErrNotConnected) {
|
||||
t.Fatalf("HealthCheck = %v, want ErrNotConnected", err)
|
||||
}
|
||||
})
|
||||
t.Run("connected_with_token", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ad, _ := New(Config{BaseURL: "https://ilinkai.weixin.qq.com", BotToken: "tok-1"}, nil, nil)
|
||||
if err := ad.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("Connect: %v", err)
|
||||
}
|
||||
if err := ad.HealthCheck(context.Background()); err != nil {
|
||||
t.Fatalf("HealthCheck = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdapter_Disconnect_IsIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
ad, _ := New(Config{BaseURL: "https://ilinkai.weixin.qq.com", BotToken: "tok-1"}, nil, nil)
|
||||
_ = ad.Connect(context.Background())
|
||||
if err := ad.Disconnect(context.Background()); err != nil {
|
||||
t.Fatalf("Disconnect: %v", err)
|
||||
}
|
||||
if ad.IsConnected() {
|
||||
t.Fatal("IsConnected() = true after Disconnect")
|
||||
}
|
||||
if err := ad.Disconnect(context.Background()); err != nil {
|
||||
t.Fatalf("second Disconnect: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_ListChannels(t *testing.T) {
|
||||
t.Parallel()
|
||||
ad, _ := New(Config{BaseURL: "https://ilinkai.weixin.qq.com", BotID: "bot-9"}, nil, nil)
|
||||
chans, err := ad.ListChannels(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("ListChannels: %v", err)
|
||||
}
|
||||
if len(chans) != 1 {
|
||||
t.Fatalf("ListChannels() = %d channels, want 1", len(chans))
|
||||
}
|
||||
if chans[0].ChannelID != "bot-9" || chans[0].ChannelType != adapter.ChannelDirect {
|
||||
t.Fatalf("channel = %+v, want {bot-9 direct}", chans[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_SetMessageHandler_And_RecordInbound(t *testing.T) {
|
||||
t.Parallel()
|
||||
ad, _ := New(Config{BaseURL: "https://ilinkai.weixin.qq.com"}, nil, nil)
|
||||
|
||||
called := false
|
||||
ad.SetMessageHandler(func(ctx context.Context, m adapter.UnifiedMessage) { called = true })
|
||||
if ad.handlerLocked() == nil {
|
||||
t.Fatal("handlerLocked() = nil after SetMessageHandler")
|
||||
}
|
||||
ad.SetMessageHandler(nil)
|
||||
if ad.handlerLocked() != nil {
|
||||
t.Fatal("handlerLocked() != nil after clearing")
|
||||
}
|
||||
if called {
|
||||
t.Fatal("handler called before any message")
|
||||
}
|
||||
|
||||
// recordInbound must refresh the per-peer token and last-message time.
|
||||
ad.recordInbound("tok-abc", adapter.UnifiedMessage{
|
||||
SenderID: "user-1",
|
||||
Timestamp: ad.clock(),
|
||||
})
|
||||
if got := ad.contextFor("user-1"); got != "tok-abc" {
|
||||
t.Fatalf("contextFor(user-1) = %q, want tok-abc", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapter_Recipient_ResolvesGroupAndSender(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
msg adapter.UnifiedMessage
|
||||
wantPeer string
|
||||
wantGroup bool
|
||||
}{
|
||||
{name: "direct sender", msg: adapter.UnifiedMessage{SenderID: "u1"}, wantPeer: "u1"},
|
||||
{name: "group id", msg: adapter.UnifiedMessage{ChannelType: adapter.ChannelGroup, GroupID: "g1"}, wantPeer: "g1", wantGroup: true},
|
||||
{name: "default channel id", msg: adapter.UnifiedMessage{ChannelID: "c1"}, wantPeer: "c1"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
peer, group := recipient(tc.msg)
|
||||
if peer != tc.wantPeer || group != tc.wantGroup {
|
||||
t.Fatalf("recipient() = (%q,%v), want (%q,%v)", peer, group, tc.wantPeer, tc.wantGroup)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user