Files
affiliate_dash/backend/internal/pkg/signutil/signutil_test.go
T
yml2213 a47b61a98d refactor(sign): 抽取公共签名原语 signutil,三套签名串格式保持不变
商户侧 /api/client/v1、源头侧 /api/open/v1 与乐玩开放平台各有一套签名实现,
其中语义完全一致的部分重复了多份,任一处修复都容易漏改另外两处。

- 新增 internal/pkg/signutil,只收敛四类一致性原语:HMAC-SHA256→hex、
  body SHA256 摘要、时间戳偏差校验、常数时间比较
- 中间件与 lewan 改为调用该包,删除 middleware 里重复的 hmacSHA256Hex 与 abs64
- 刻意不统一「签名串构造」:三者拼接方式是对外契约且互不相同
  (商户:字典序 & + body 摘要;源头:字典序 & + body 原文;乐玩:换行 6 行),
  强行合并会直接改变握手结果
- 保留各处原有细节语义:商户侧十六进制签名大小写不敏感、乐玩签名原样比较、
  VerifyWebhook 在 allowedSkew<=0 时仍要求时间戳可解析

验证:三套方案既有端到端测试全部通过(含乐玩官方测试向量、大写签名、重放拒绝);
新增 signutil 已知向量与偏差/比较边界测试,以及 VerifyWebhook 边界语义测试。
2026-10-06 19:53:55 +08:00

110 lines
3.7 KiB
Go

package signutil
import (
"strconv"
"testing"
"time"
)
// TestHMACSHA256HexKnownVector 用公开测试向量锁定算法,避免以后「优化」时改坏。
func TestHMACSHA256HexKnownVector(t *testing.T) {
got := HMACSHA256Hex("key", "The quick brown fox jumps over the lazy dog")
const want = "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"
if got != want {
t.Fatalf("HMAC-SHA256 向量不符:期望 %s,实际 %s", want, got)
}
}
// TestHMACSHA256HexBytesMatchesStringVersion 分段写入必须与整体写入等价。
func TestHMACSHA256HexBytesMatchesStringVersion(t *testing.T) {
whole := HMACSHA256Hex("secret", "prefix-body-suffix")
parts := HMACSHA256HexBytes("secret", []byte("prefix-"), []byte("body"), []byte("-suffix"))
if whole != parts {
t.Fatalf("分段写入结果不一致:%s vs %s", whole, parts)
}
}
func TestHMACSHA256HexBytesNoParts(t *testing.T) {
// 空内容等价于对空串做 HMAC。
if got, want := HMACSHA256HexBytes("secret"), HMACSHA256Hex("secret", ""); got != want {
t.Fatalf("无分段应等价于空内容:%s vs %s", got, want)
}
}
func TestSHA256Hex(t *testing.T) {
if got := SHA256Hex(nil); got != EmptyBodySHA256 {
t.Fatalf("空内容应返回空串摘要常量,实际 %s", got)
}
if got := SHA256Hex([]byte{}); got != EmptyBodySHA256 {
t.Fatalf("空切片应返回空串摘要常量,实际 %s", got)
}
const wantAbc = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
if got := SHA256Hex([]byte("abc")); got != wantAbc {
t.Fatalf("SHA256(abc) 不符:期望 %s,实际 %s", wantAbc, got)
}
}
func TestTimestampWithinSkew(t *testing.T) {
now := time.Now().Unix()
cases := []struct {
name string
timestamp string
skew time.Duration
want bool
}{
{"当前时间在窗口内", itoa(now), 300 * time.Second, true},
{"边界上刚好等于 skew", itoa(now - 300), 300 * time.Second, true},
{"超出 skew 一秒", itoa(now - 301), 300 * time.Second, false},
{"未来时间同样按绝对值判断", itoa(now + 301), 300 * time.Second, false},
{"未来时间在窗口内", itoa(now + 60), 300 * time.Second, true},
{"非法时间戳", "not-a-number", 300 * time.Second, false},
{"空时间戳", "", 300 * time.Second, false},
{"不裁剪空白(保持既有行为)", " " + itoa(now) + " ", 300 * time.Second, false},
{"skew 为 0 时只有同一秒通过", itoa(now), 0, true},
{"skew 为 0 时上一秒不通过", itoa(now - 1), 0, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := TimestampWithinSkew(tc.timestamp, tc.skew); got != tc.want {
t.Fatalf("期望 %v,实际 %v", tc.want, got)
}
})
}
}
func TestTimestampWithinSkewSecondsMatchesDuration(t *testing.T) {
now := itoa(time.Now().Unix())
if TimestampWithinSkewSeconds(now, 300) != TimestampWithinSkew(now, 300*time.Second) {
t.Fatal("秒版本与 Duration 版本应一致")
}
}
func TestEqualSignatures(t *testing.T) {
if !EqualSignatures("abc123", "abc123") {
t.Fatal("相同签名应通过")
}
if EqualSignatures("abc123", "abc124") {
t.Fatal("不同签名不应通过")
}
if EqualSignatures("abc", "abcd") {
t.Fatal("长度不同的签名不应通过")
}
// 该函数不做大小写归一化,这是刻意为之(乐玩签名带 v1= 前缀,需原样比较)。
if EqualSignatures("ABC123", "abc123") {
t.Fatal("未归一化的版本应区分大小写")
}
}
func TestEqualHexSignatures(t *testing.T) {
if !EqualHexSignatures("ABC123DEF", "abc123def") {
t.Fatal("十六进制签名应大小写不敏感")
}
if EqualHexSignatures("abc123", "abc124") {
t.Fatal("不同签名不应通过")
}
}
func itoa(v int64) string {
return strconv.FormatInt(v, 10)
}