ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
95 lines
3.3 KiB
Go
95 lines
3.3 KiB
Go
package notify
|
|
|
|
import (
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// 签名基准值由 OpenSSL 独立算出,不是用本包自己的实现生成的——
|
|
// 否则只能证明「代码没变」,证明不了「算法对」。
|
|
//
|
|
// TS=1700000000000, SECRET=SECtest123
|
|
// 钉钉: printf '%s\n%s' "$TS" "$SECRET" | openssl dgst -sha256 -hmac "$SECRET" -binary | openssl base64 -A
|
|
// -> w3RMHXzixTMdzr8OHJUmVLS4IoPJVdu+Ut1LE48MePE=
|
|
// 飞书: printf '' | openssl dgst -sha256 -hmac "$(printf '%s\n%s' "$TS" "$SECRET")" -binary | openssl base64 -A
|
|
// -> Hd4xFWQU6R6ad4nzy4ETIznzlqebqH7xcTFVmONTudo=
|
|
const (
|
|
signTestTSMillis = int64(1700000000000)
|
|
signTestSecret = "SECtest123"
|
|
dingTalkExpected = "w3RMHXzixTMdzr8OHJUmVLS4IoPJVdu+Ut1LE48MePE="
|
|
feishuExpected = "Hd4xFWQU6R6ad4nzy4ETIznzlqebqH7xcTFVmONTudo="
|
|
)
|
|
|
|
func TestDingTalkSignMatchesReference(t *testing.T) {
|
|
got, err := dingTalkSignedURL("https://oapi.dingtalk.com/robot/send?access_token=tok", signTestSecret, time.UnixMilli(signTestTSMillis))
|
|
if err != nil {
|
|
t.Fatalf("签名失败: %v", err)
|
|
}
|
|
u, err := url.Parse(got)
|
|
if err != nil {
|
|
t.Fatalf("产出地址不可解析: %v", err)
|
|
}
|
|
q := u.Query()
|
|
if q.Get("sign") != dingTalkExpected {
|
|
t.Errorf("签名不符\n期望 %s\n得到 %s", dingTalkExpected, q.Get("sign"))
|
|
}
|
|
if q.Get("timestamp") != "1700000000000" {
|
|
t.Errorf("时间戳应为毫秒且原样带上,得到 %q", q.Get("timestamp"))
|
|
}
|
|
// 原有 query 参数(access_token)不能被签名覆盖掉。
|
|
if q.Get("access_token") != "tok" {
|
|
t.Errorf("原有 query 参数丢失,得到 %q", q.Get("access_token"))
|
|
}
|
|
}
|
|
|
|
func TestFeishuSignMatchesReference(t *testing.T) {
|
|
got := feishuSign("1700000000000", signTestSecret)
|
|
if got != feishuExpected {
|
|
t.Errorf("签名不符\n期望 %s\n得到 %s", feishuExpected, got)
|
|
}
|
|
}
|
|
|
|
// TestSignAlgorithmsDiffer 锁住两家的算法差异。它们刚好互为对方的参数顺序
|
|
// (钉钉 key=secret,飞书 key=待签串),照另一家抄必然校验失败,
|
|
// 这条用例确保将来重构不会把两者合并成同一个函数。
|
|
func TestSignAlgorithmsDiffer(t *testing.T) {
|
|
ts := "1700000000000"
|
|
dingURL, err := dingTalkSignedURL("https://example.com/hook", signTestSecret, time.UnixMilli(signTestTSMillis))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dq, _ := url.Parse(dingURL)
|
|
if dq.Query().Get("sign") == feishuSign(ts, signTestSecret) {
|
|
t.Fatal("钉钉与飞书签名相同,说明其中一家的算法实现错了")
|
|
}
|
|
}
|
|
|
|
func TestDingTalkNoSecretLeavesURLUntouched(t *testing.T) {
|
|
// 未开启加签的机器人:不得凭空添加 timestamp/sign 参数。
|
|
const hook = "https://oapi.dingtalk.com/robot/send?access_token=tok"
|
|
got, err := dingTalkSignedURL(hook, "", time.UnixMilli(signTestTSMillis))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != hook {
|
|
t.Fatalf("未配置 secret 时地址不应改动,得到 %q", got)
|
|
}
|
|
}
|
|
|
|
func TestValidateHTTPURL(t *testing.T) {
|
|
ok := []string{"https://example.com/hook", "http://10.0.0.1:8080/x?y=1"}
|
|
for _, s := range ok {
|
|
if err := validateHTTPURL(s); err != nil {
|
|
t.Errorf("%q 应被接受: %v", s, err)
|
|
}
|
|
}
|
|
// file:// 之类不应放行——http.Client 对它们的处理超出预期范围。
|
|
bad := []string{"", "file:///etc/passwd", "ftp://example.com", "https://", "gopher://x"}
|
|
for _, s := range bad {
|
|
if err := validateHTTPURL(s); err == nil {
|
|
t.Errorf("%q 应被拒绝", s)
|
|
}
|
|
}
|
|
}
|