feat(log): 前后端共用 request_id 与四时间点时钟校准
- X-Request-Id 请求头带合法 UUID 则沿用为 request_id(严格格式白名单防注入),否则自生成 12 位 hex - 响应头新增 X-Server-Received-Ms / X-Server-Sent-Ms 供前端时钟校准 - CORS Allow-Headers 增加 X-Request-Id,Expose-Headers 显式列出(credentials 下不认 *) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package hotime
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
)
|
||||
|
||||
func TestNormalizeRequestId_Valid(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"6f0a1b2c-3d4e-4f5a-8b6c-7d8e9f0a1b2c", "6f0a1b2c-3d4e-4f5a-8b6c-7d8e9f0a1b2c"},
|
||||
// 大写归一为小写
|
||||
{"6F0A1B2C-3D4E-4F5A-8B6C-7D8E9F0A1B2C", "6f0a1b2c-3d4e-4f5a-8b6c-7d8e9f0a1b2c"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := normalizeRequestId(c.in); got != c.want {
|
||||
t.Fatalf("normalizeRequestId(%q)=%q want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRequestId_Invalid(t *testing.T) {
|
||||
cases := []string{
|
||||
"",
|
||||
"abc",
|
||||
// 12 位短 hex(服务端自生成格式,不接受为前端传入 id)
|
||||
"a1b2c3d4e5f6",
|
||||
// 长度对但连字符位置错
|
||||
"6f0a1b2c3-d4e-4f5a-8b6c-7d8e9f0a1b2c",
|
||||
// 非 hex 字符
|
||||
"6f0a1b2c-3d4e-4f5a-8b6c-7d8e9f0a1bzz",
|
||||
// 注入尝试:换行/引号
|
||||
"6f0a1b2c-3d4e-4f5a-8b6c-7d8e9f0a1\n2c",
|
||||
"\"};alert(1);//-4f5a-8b6c-7d8e9f0a1b2c",
|
||||
// 超长
|
||||
"6f0a1b2c-3d4e-4f5a-8b6c-7d8e9f0a1b2c0",
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := normalizeRequestId(c); got != "" {
|
||||
t.Fatalf("normalizeRequestId(%q)=%q want empty", c, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 共用 request_id 行为:前端带合法 UUID 沿用,非法/缺失服务端自生成 12 位 hex
|
||||
func TestRequestIdSharing(t *testing.T) {
|
||||
// 合法 UUID → 沿用(归一小写)
|
||||
if got := normalizeRequestId("6F0A1B2C-3D4E-4F5A-8B6C-7D8E9F0A1B2C"); got != "6f0a1b2c-3d4e-4f5a-8b6c-7d8e9f0a1b2c" {
|
||||
t.Fatalf("合法 UUID 应沿用为 request_id,got %q", got)
|
||||
}
|
||||
// 非法 → 走服务端自生成分支
|
||||
if normalizeRequestId("evil\ninjection") != "" {
|
||||
t.Fatal("非法值应被丢弃并触发服务端自生成")
|
||||
}
|
||||
id := shortID(6)
|
||||
if len(id) != 12 {
|
||||
t.Fatalf("服务端自生成 request_id 应为 12 位 hex,got %q", id)
|
||||
}
|
||||
for i := 0; i < len(id); i++ {
|
||||
c := id[i]
|
||||
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
|
||||
t.Fatalf("服务端自生成 request_id 含非 hex 字符: %q", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewSetsServerSentMsHeader(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
ctx := &Context{Resp: rec, RespData: Map{"status": 0}}
|
||||
|
||||
before := time.Now().UnixMilli()
|
||||
ctx.View()
|
||||
after := time.Now().UnixMilli()
|
||||
|
||||
h := rec.Header().Get("X-Server-Sent-Ms")
|
||||
if h == "" {
|
||||
t.Fatal("X-Server-Sent-Ms 未设置")
|
||||
}
|
||||
ms, err := strconv.ParseInt(h, 10, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("X-Server-Sent-Ms=%q 非毫秒时间戳: %v", h, err)
|
||||
}
|
||||
if ms < before || ms > after {
|
||||
t.Fatalf("X-Server-Sent-Ms=%d 不在 [%d,%d] 区间", ms, before, after)
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "\"status\":0") {
|
||||
t.Fatalf("响应体异常: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user