feat: 接入 CoinGlass 清算热力图隔离链路
Some checks failed
ci / build + vet + guard + race (push) Has been cancelled

This commit is contained in:
dela
2026-05-25 16:57:22 +08:00
parent e5fb54cb72
commit 1b61541ff5
32 changed files with 2550 additions and 70 deletions

View File

@@ -0,0 +1,128 @@
package coinglass
import (
"bytes"
"compress/gzip"
"crypto/aes"
"encoding/base64"
"errors"
"testing"
"github.com/stretchr/testify/require"
)
// helper把 plaintext 用 V2 通用管线反向加密,产出可喂给 decryptBlob 的 base64 ciphertext。
// 形态plaintext → '"plaintext"' → gzip → pkcs7 pad → AES-ECB → base64
func encryptForTest(t *testing.T, key []byte, plaintext string) string {
t.Helper()
// CoinGlass 服务器端会在 plaintext 外加引号decryptBlob 反向 strip
// 测试也照样加引号 → 验证 strip 逻辑。
quoted := []byte(`"` + plaintext + `"`)
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
_, err := gz.Write(quoted)
require.NoError(t, err)
require.NoError(t, gz.Close())
padded := pkcs7Pad(buf.Bytes(), aes.BlockSize)
enc, err := aesECBEncrypt(key, padded)
require.NoError(t, err)
return base64.StdEncoding.EncodeToString(enc)
}
func TestDecryptBlob_RoundTrip(t *testing.T) {
cases := []struct {
name string
key []byte
text string
}{
{"16-byte key (AES-128)", []byte("0123456789abcdef"), `{"hello":"world"}`},
{"32-byte key (AES-256)", []byte("1f68efd73f8d4921acc0dead41dd39bc"), `{"foo":42,"bar":"baz"}`},
{"empty json object", []byte("0123456789abcdef"), `{}`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ct := encryptForTest(t, tc.key, tc.text)
got, err := decryptBlob(ct, tc.key)
require.NoError(t, err)
require.Equal(t, tc.text, got)
})
}
}
func TestDecryptBlob_BadCiphertext(t *testing.T) {
key := []byte("0123456789abcdef")
_, err := decryptBlob("!!!not_base64!!!", key)
require.Error(t, err)
require.ErrorIs(t, err, ErrDecryptFail)
short := base64.StdEncoding.EncodeToString([]byte("toosmall"))
_, err = decryptBlob(short, key)
require.ErrorIs(t, err, ErrDecryptFail)
}
func TestPKCS7_PadUnpadRoundTrip(t *testing.T) {
cases := [][]byte{
[]byte(""),
[]byte("a"),
[]byte("0123456789abcde"), // 15 → pad 1
[]byte("0123456789abcdef"), // 16 → pad 16 (full block)
[]byte("0123456789abcdef0"),
}
for _, in := range cases {
padded := pkcs7Pad(in, 16)
require.Zero(t, len(padded)%16)
out, err := pkcs7Unpad(padded, 16)
require.NoError(t, err)
require.Equal(t, in, out)
}
}
func TestPKCS7_UnpadRejectsBadInput(t *testing.T) {
bad := []byte("0123456789abcdef") // pad byte = 'f' (0x66 = 102 > 16)
_, err := pkcs7Unpad(bad, 16)
require.Error(t, err)
_, err = pkcs7Unpad([]byte{}, 16)
require.Error(t, err)
}
func TestUserKeyFromSeed_TruncatesTo16(t *testing.T) {
seed := "863f08689c97435b" // v=77 constant from bundle
got := userKeyFromSeed(seed)
require.Len(t, got, 16)
// base64("863f08689c97435b") = "ODYzZjA4Njg5Yzk3NDM1Yg==" → first 16 = "ODYzZjA4Njg5Yzk3"
require.Equal(t, "ODYzZjA4Njg5Yzk3", string(got))
}
func TestBodyKeyFromToken_RawASCIIFirst16(t *testing.T) {
token := "dfa89d5d00cf4000extra_chars"
got := bodyKeyFromToken(token)
require.Len(t, got, 16)
require.Equal(t, "dfa89d5d00cf4000", string(got))
}
func TestDecryptUniversal_TwoStage(t *testing.T) {
seed := "863f08689c97435b" // v=77 bundle constant
userKey := userKeyFromSeed(seed)
token := "dfa89d5d00cf4000" // exactly 16 chars (matches cg.md L79 example)
bodyKey := []byte(token)
plaintext := `{"success":true,"code":"0","msg":"","data":{"priceMin":110000.0}}`
userHeaderB64 := encryptForTest(t, userKey, token)
bodyDataB64 := encryptForTest(t, bodyKey, plaintext)
got, err := DecryptUniversal(seed, userHeaderB64, bodyDataB64)
require.NoError(t, err)
require.JSONEq(t, plaintext, string(got))
}
func TestDecryptUniversal_PropagatesDecryptFail(t *testing.T) {
_, err := DecryptUniversal("seed", "!!!", "!!!")
require.Error(t, err)
require.True(t, errors.Is(err, ErrDecryptFail))
}