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,145 @@
|
||||
package weixin_ilink
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeriveInboundKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
key := bytes.Repeat([]byte{0x7F}, aes.BlockSize)
|
||||
token := encodeToken(key)
|
||||
|
||||
got, err := deriveInboundKey(token)
|
||||
if err != nil {
|
||||
t.Fatalf("deriveInboundKey: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, key) {
|
||||
t.Fatalf("key = %x, want %x", got, key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveInboundKey_Errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("empty token", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if _, err := deriveInboundKey(""); !errors.Is(err, ErrInvalidConfig) {
|
||||
t.Fatalf("err = %v, want ErrInvalidConfig", err)
|
||||
}
|
||||
})
|
||||
t.Run("not base64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if _, err := deriveInboundKey("@not-base64@"); err == nil {
|
||||
t.Fatal("err = nil, want decode error")
|
||||
}
|
||||
})
|
||||
t.Run("token too short", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if _, err := deriveInboundKey(encodeToken([]byte("short"))); !errors.Is(err, ErrKeyTooShort) {
|
||||
t.Fatalf("err = %v, want ErrKeyTooShort", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloadMedia_DecryptsFromTokenDerivedKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
key := bytes.Repeat([]byte{0x11}, aes.BlockSize)
|
||||
plain := []byte("the quick brown fox jumps over the lazy dog")
|
||||
enc, err := aesECBEncrypt(key, plain)
|
||||
if err != nil {
|
||||
t.Fatalf("aesECBEncrypt: %v", err)
|
||||
}
|
||||
token := encodeToken(key)
|
||||
|
||||
srv := newJSONServer(t, nil, func(req recordedReq) handlerResult {
|
||||
if req.path == "/media/1" {
|
||||
return handlerResult{ctype: "application/octet-stream", body: enc}
|
||||
}
|
||||
return handlerResult{status: 404}
|
||||
})
|
||||
|
||||
ad := newAdapter(t, Config{BaseURL: srv.URL, CDNBaseURL: srv.URL, BotToken: token}, srv)
|
||||
got, err := ad.DownloadMedia(context.Background(), srv.URL+"/media/1")
|
||||
if err != nil {
|
||||
t.Fatalf("DownloadMedia: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, plain) {
|
||||
t.Fatalf("decrypted = %q, want %q", got, plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadMediaWithKey_ExplicitKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
key := bytes.Repeat([]byte{0x22}, aes.BlockSize)
|
||||
plain := []byte("explicit key path")
|
||||
enc, err := aesECBEncrypt(key, plain)
|
||||
if err != nil {
|
||||
t.Fatalf("aesECBEncrypt: %v", err)
|
||||
}
|
||||
srv := newJSONServer(t, nil, func(req recordedReq) handlerResult {
|
||||
if req.path == "/media/2" {
|
||||
return handlerResult{ctype: "application/octet-stream", body: enc}
|
||||
}
|
||||
return handlerResult{status: 404}
|
||||
})
|
||||
ad := newAdapter(t, Config{BaseURL: srv.URL, CDNBaseURL: srv.URL, BotToken: "unused"}, srv)
|
||||
got, err := ad.DownloadMediaWithKey(context.Background(), srv.URL+"/media/2", key)
|
||||
if err != nil {
|
||||
t.Fatalf("DownloadMediaWithKey: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, plain) {
|
||||
t.Fatalf("decrypted = %q, want %q", got, plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadMedia_RejectsOversize(t *testing.T) {
|
||||
t.Parallel()
|
||||
big := bytes.Repeat([]byte{0x01}, 256)
|
||||
srv := newJSONServer(t, nil, func(req recordedReq) handlerResult {
|
||||
if req.path == "/media/big" {
|
||||
return handlerResult{ctype: "application/octet-stream", body: big}
|
||||
}
|
||||
return handlerResult{status: 404}
|
||||
})
|
||||
ad := newAdapter(t, Config{
|
||||
BaseURL: srv.URL, CDNBaseURL: srv.URL,
|
||||
BotToken: encodeToken(bytes.Repeat([]byte{1}, aes.BlockSize)),
|
||||
MaxMediaSize: 64,
|
||||
}, srv)
|
||||
_, err := ad.DownloadMedia(context.Background(), srv.URL+"/media/big")
|
||||
if !errors.Is(err, ErrMediaTooLarge) {
|
||||
t.Fatalf("err = %v, want ErrMediaTooLarge", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadMedia_RejectsDecryptFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Serve bytes that are not a valid AES block sequence.
|
||||
srv := newJSONServer(t, nil, func(req recordedReq) handlerResult {
|
||||
if req.path == "/media/bad" {
|
||||
return handlerResult{ctype: "application/octet-stream", body: []byte("not-a-block")}
|
||||
}
|
||||
return handlerResult{status: 404}
|
||||
})
|
||||
ad := newAdapter(t, Config{
|
||||
BaseURL: srv.URL, CDNBaseURL: srv.URL,
|
||||
BotToken: encodeToken(bytes.Repeat([]byte{1}, aes.BlockSize)),
|
||||
}, srv)
|
||||
if _, err := ad.DownloadMedia(context.Background(), srv.URL+"/media/bad"); err == nil {
|
||||
t.Fatal("err = nil, want decrypt failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCDNURL_JoinsRelativeAndPassesAbsolute(t *testing.T) {
|
||||
t.Parallel()
|
||||
ad, _ := New(Config{BaseURL: "https://api.example.com", CDNBaseURL: "https://cdn.example.com/c2c"}, nil, nil)
|
||||
if got := ad.cdnURL("media/1.jpg"); got != "https://cdn.example.com/c2c/media/1.jpg" {
|
||||
t.Fatalf("cdnURL(relative) = %q", got)
|
||||
}
|
||||
if got := ad.cdnURL("https://other.example.com/x"); got != "https://other.example.com/x" {
|
||||
t.Fatalf("cdnURL(absolute) = %q", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user