package echomeet import ( "encoding/base64" "encoding/json" "os" "strings" "testing" "time" ) // 票据是公开链接里的唯一凭据,下面每条都是「错了就等于任何人能读任意纪要」级别的性质, // 所以全部钉死在测试里。 func TestShareTicketRoundTrip(t *testing.T) { os.Setenv("FIELD_ENCRYPT_KEY", "test-field-key-0123456789") defer os.Unsetenv("FIELD_ENCRYPT_KEY") exp := time.Now().Add(time.Hour).Unix() ticket, err := signShareTicket(sharePayload{Id: 42, Uid: "1234567890", Expire: exp}) if err != nil { t.Fatalf("签发失败: %v", err) } got, err := parseShareTicket(ticket) if err != nil { t.Fatalf("校验失败: %v", err) } if got.Id != 42 || got.Uid != "1234567890" || got.Expire != exp { t.Fatalf("载荷不一致: %+v", got) } } func TestShareTicketRejects(t *testing.T) { os.Setenv("FIELD_ENCRYPT_KEY", "test-field-key-0123456789") defer os.Unsetenv("FIELD_ENCRYPT_KEY") good, _ := signShareTicket(sharePayload{Id: 42, Uid: "u1", Expire: time.Now().Add(time.Hour).Unix()}) body := good[:strings.LastIndex(good, ".")] // 改载荷不改签名:这是最直接的攻击——把 id 换成别人的记录。 tampered := base64.RawURLEncoding.EncodeToString([]byte(`{"i":43,"u":"u1","e":9999999999}`)) cases := map[string]string{ "空串": "", "没有分隔点": body, "签名被截断": body + ".", "签名乱填": body + ".AAAA", "载荷被篡改": tampered + good[strings.LastIndex(good, "."):], "整段不是base64": "!!!.@@@", } for name, tk := range cases { if _, err := parseShareTicket(tk); err == nil { t.Errorf("%s: 本该被拒,却通过了", name) } } // 过期票据必须拒绝:签名是对的,只有时间不对。 expired, _ := signShareTicket(sharePayload{Id: 42, Uid: "u1", Expire: time.Now().Add(-time.Second).Unix()}) if _, err := parseShareTicket(expired); err == nil { t.Error("过期票据本该被拒") } } // 换密钥必须让旧票据全部失效——否则「换密钥」这个应急手段是假的。 func TestShareTicketKeyIsolation(t *testing.T) { os.Setenv("FIELD_ENCRYPT_KEY", "key-A") ticket, err := signShareTicket(sharePayload{Id: 1, Uid: "u", Expire: time.Now().Add(time.Hour).Unix()}) if err != nil { t.Fatal(err) } os.Setenv("FIELD_ENCRYPT_KEY", "key-B") defer os.Unsetenv("FIELD_ENCRYPT_KEY") if _, err = parseShareTicket(ticket); err == nil { t.Fatal("换密钥后旧票据仍然有效") } } // 没有 FIELD_ENCRYPT_KEY 时必须报错,绝不能退化成用空密钥签名(那样谁都能自己造票据)。 func TestShareTicketRefusesEmptyKey(t *testing.T) { old := os.Getenv("FIELD_ENCRYPT_KEY") os.Unsetenv("FIELD_ENCRYPT_KEY") defer os.Setenv("FIELD_ENCRYPT_KEY", old) if _, err := signShareTicket(sharePayload{Id: 1, Uid: "u", Expire: 1}); err == nil { t.Fatal("缺密钥时本该拒绝签发") } } // 票据里不该出现任何登录凭据的痕迹:它进 URL、会被转发、会进浏览器历史。 func TestShareTicketCarriesOnlyScopedPayload(t *testing.T) { os.Setenv("FIELD_ENCRYPT_KEY", "test-field-key-0123456789") defer os.Unsetenv("FIELD_ENCRYPT_KEY") ticket, _ := signShareTicket(sharePayload{Id: 7, Uid: "u7", Expire: time.Now().Add(time.Hour).Unix()}) raw, err := base64.RawURLEncoding.DecodeString(ticket[:strings.LastIndex(ticket, ".")]) if err != nil { t.Fatal(err) } m := map[string]any{} if err = json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } if len(m) != 3 { t.Fatalf("载荷字段数变了(%d),新增字段前先确认它可以公开: %v", len(m), m) } for _, k := range []string{"i", "u", "e"} { if _, ok := m[k]; !ok { t.Fatalf("载荷缺字段 %s: %v", k, m) } } }