package notify import ( "net/url" "testing" "time" ) // The signature reference values were computed independently with OpenSSL, not // generated by this package's own implementation — otherwise they could only // prove "the code has not changed", not "the algorithm is right". // // TS=1700000000000, SECRET=SECtest123 // DingTalk: printf '%s\n%s' "$TS" "$SECRET" | openssl dgst -sha256 -hmac "$SECRET" -binary | openssl base64 -A // -> w3RMHXzixTMdzr8OHJUmVLS4IoPJVdu+Ut1LE48MePE= // Feishu: 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("signing failed: %v", err) } u, err := url.Parse(got) if err != nil { t.Fatalf("the produced address cannot be parsed: %v", err) } q := u.Query() if q.Get("sign") != dingTalkExpected { t.Errorf("signature mismatch\nwant %s\ngot %s", dingTalkExpected, q.Get("sign")) } if q.Get("timestamp") != "1700000000000" { t.Errorf("the timestamp should be in milliseconds and carried as-is, got %q", q.Get("timestamp")) } // The original query parameter (access_token) must not be overwritten by the // signature. if q.Get("access_token") != "tok" { t.Errorf("the original query parameter was lost, got %q", q.Get("access_token")) } } func TestFeishuSignMatchesReference(t *testing.T) { got := feishuSign("1700000000000", signTestSecret) if got != feishuExpected { t.Errorf("signature mismatch\nwant %s\ngot %s", feishuExpected, got) } } // TestSignAlgorithmsDiffer locks the difference between the two platforms' // algorithms. They happen to be mirror images of each other's parameter order // (DingTalk key=secret, Feishu key=string-to-sign), and copying from the other // platform always fails verification; this case ensures a future refactor does // not merge the two into one function. 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("DingTalk and Feishu signatures are identical, which means one of the two algorithm implementations is wrong") } } func TestDingTalkNoSecretLeavesURLUntouched(t *testing.T) { // A bot without signing enabled: the timestamp/sign parameters must not be // added out of thin air. 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("the address should not change when no secret is configured, got %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 should be accepted: %v", s, err) } } // file:// and the like must not be let through — http.Client's handling of // them is beyond the expected scope. bad := []string{"", "file:///etc/passwd", "ftp://example.com", "https://", "gopher://x"} for _, s := range bad { if err := validateHTTPURL(s); err == nil { t.Errorf("%q should be rejected", s) } } }