package lewan import ( "strconv" "testing" "time" ) // 这些用例锁定 VerifyWebhook 在「共享原语重构」时被刻意保留的边界语义。 func TestVerifyWebhookTimestampContract(t *testing.T) { const ( secret = "whsec_test" eventID = "evt_1" ) rawBody := []byte(`{"hello":"world"}`) now := time.Now().Unix() cases := []struct { name string timestamp string allowedSkew time.Duration want bool }{ { name: "allowedSkew 为 0 时仍要求时间戳可解析", timestamp: "not-a-number", allowedSkew: 0, want: false, }, { name: "allowedSkew 为 0 时同一秒通过", timestamp: strconv.FormatInt(now, 10), allowedSkew: 0, want: true, }, { name: "allowedSkew 为 0 时不校验时间窗口", timestamp: strconv.FormatInt(now-86400, 10), allowedSkew: 0, want: true, }, { name: "指定窗口内通过", timestamp: strconv.FormatInt(now-60, 10), allowedSkew: 5 * time.Minute, want: true, }, { name: "超出窗口被拒", timestamp: strconv.FormatInt(now-600, 10), allowedSkew: 5 * time.Minute, want: false, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { sig := SignWebhook(secret, tc.timestamp, eventID, rawBody) got := VerifyWebhook(secret, sig, tc.timestamp, eventID, rawBody, tc.allowedSkew) if got != tc.want { t.Fatalf("期望 %v,实际 %v", tc.want, got) } }) } } // 带不带 v1= 前缀都应被接受。 func TestVerifyWebhookAcceptsSignatureWithoutPrefix(t *testing.T) { const ( secret = "whsec_test" eventID = "evt_2" ) rawBody := []byte(`{"a":1}`) timestamp := strconv.FormatInt(time.Now().Unix(), 10) full := SignWebhook(secret, timestamp, eventID, rawBody) if !VerifyWebhook(secret, full, timestamp, eventID, rawBody, 5*time.Minute) { t.Fatal("带 v1= 前缀的签名应通过") } if !VerifyWebhook(secret, full[len("v1="):], timestamp, eventID, rawBody, 5*time.Minute) { t.Fatal("不带 v1= 前缀的签名也应通过") } } // 任一必填字段为空都必须直接拒绝。 func TestVerifyWebhookRejectsEmptyInputs(t *testing.T) { rawBody := []byte(`{}`) timestamp := strconv.FormatInt(time.Now().Unix(), 10) const ( secret = "whsec_test" eventID = "evt_3" ) sig := SignWebhook(secret, timestamp, eventID, rawBody) if VerifyWebhook("", sig, timestamp, eventID, rawBody, 5*time.Minute) { t.Fatal("空 secret 必须被拒") } if VerifyWebhook(secret, "", timestamp, eventID, rawBody, 5*time.Minute) { t.Fatal("空签名必须被拒") } if VerifyWebhook(secret, sig, "", eventID, rawBody, 5*time.Minute) { t.Fatal("空时间戳必须被拒") } if VerifyWebhook(secret, sig, timestamp, "", rawBody, 5*time.Minute) { t.Fatal("空 eventID 必须被拒") } }