Files
alterminal/internal/oidc/jwks_test.go
T
2026-10-03 10:44:29 +08:00

138 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 外部測試套件(package oidc_test):store 為遷移 OIDC 模型而匯入
// oidc 套件,內部測試套件匯入 testdb(→ store → oidc)會形成循環。
package oidc_test
import (
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"alterminal/internal/jwk"
"alterminal/internal/oidc"
"alterminal/internal/testdb"
)
// jwksGet 對 handler 發出 GET /.well-known/jwks.json 並回傳回應記錄器。
func jwksGet(h http.HandlerFunc) *httptest.ResponseRecorder {
rec := httptest.NewRecorder()
h(rec, httptest.NewRequest(http.MethodGet, "/.well-known/jwks.json", nil))
return rec
}
// mustNewKey 產生一把簽章金鑰;retired 為 true 時標記為已退休。
func mustNewKey(t *testing.T, retired bool) *jwk.SigningKey {
t.Helper()
k, err := jwk.NewSigningKey()
if err != nil {
t.Fatal("NewSigningKey: ", err)
}
if retired {
now := time.Now()
k.RetiredAt = &now
}
return k
}
func TestJWKSHandlerIntegration(t *testing.T) {
db := testdb.New(t)
active1 := mustNewKey(t, false)
active2 := mustNewKey(t, false)
retired := mustNewKey(t, true)
for _, k := range []*jwk.SigningKey{active1, active2, retired} {
if err := db.Create(k).Error; err != nil {
t.Fatal("建立測試金鑰: ", err)
}
}
h := oidc.JWKSHandler(db)
t.Run("僅發佈使用中的金鑰", func(t *testing.T) {
rec := jwksGet(h)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200, body = %s", rec.Code, rec.Body.String())
}
if ct := rec.Header().Get("Content-Type"); ct != "application/json; charset=utf-8" {
t.Errorf("Content-Type = %q, want application/json; charset=utf-8", ct)
}
if cc := rec.Header().Get("Cache-Control"); cc != "public, max-age=3600" {
t.Errorf("Cache-Control = %q, want public, max-age=3600", cc)
}
var set jwk.JWKS
if err := json.Unmarshal(rec.Body.Bytes(), &set); err != nil {
t.Fatalf("解析 JWKS: %v", err)
}
if len(set.Keys) != 2 {
t.Fatalf("keys = %d 把, want 2(退休金鑰不應發佈): %s", len(set.Keys), rec.Body.String())
}
kids := map[string]bool{}
for _, k := range set.Keys {
kids[k.Kid] = true
if k.Kty != jwk.KeyTypeRSA || k.Use != jwk.KeyUseSig || k.Alg != jwk.AlgRS256 {
t.Errorf("kid %s 參數 = kty:%q use:%q alg:%q", k.Kid, k.Kty, k.Use, k.Alg)
}
if k.E != "AQAB" {
t.Errorf("kid %s e = %q, want AQAB", k.Kid, k.E)
}
n, err := base64.RawURLEncoding.DecodeString(k.N)
if err != nil || len(n) == 0 {
t.Errorf("kid %s 的 n 應為可解碼的非空 base64url: %q (err=%v)", k.Kid, k.N, err)
}
}
if !kids[active1.Kid] || !kids[active2.Kid] {
t.Errorf("應發佈兩把使用中金鑰 %q、%q,實際 %v", active1.Kid, active2.Kid, kids)
}
if kids[retired.Kid] {
t.Error("退休金鑰不應出現於 JWKS")
}
})
t.Run("GET 為冪等", func(t *testing.T) {
first, second := jwksGet(h), jwksGet(h)
if first.Body.String() != second.Body.String() {
t.Error("兩次 GET 的 JWKS 應相同")
}
})
}
// 空資料庫時 keys 為空陣列而非 null(RFC 7517 §5.1:keys 必要)。
func TestJWKSHandlerEmpty(t *testing.T) {
db := testdb.New(t)
rec := jwksGet(oidc.JWKSHandler(db))
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200, body = %s", rec.Code, rec.Body.String())
}
if s := rec.Body.String(); s != "{\"keys\":[]}\n" && s != `{"keys":[]}` {
t.Fatalf("空 JWKS 應為 {\"keys\":[]},得到 %s", s)
}
}
// 私鑰儲存毀損的金鑰被跳過,其餘金鑰照常發佈。
func TestJWKSHandlerSkipsCorruptKey(t *testing.T) {
db := testdb.New(t)
good := mustNewKey(t, false)
if err := db.Create(good).Error; err != nil {
t.Fatal("建立測試金鑰: ", err)
}
bad := &jwk.SigningKey{Kid: "corrupt-kid", Algorithm: jwk.AlgRS256, PrivateKeyPEM: "not a pem"}
if err := db.Create(bad).Error; err != nil {
t.Fatal("建立毀損金鑰: ", err)
}
rec := jwksGet(oidc.JWKSHandler(db))
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200, body = %s", rec.Code, rec.Body.String())
}
var set jwk.JWKS
if err := json.Unmarshal(rec.Body.Bytes(), &set); err != nil {
t.Fatalf("解析 JWKS: %v", err)
}
if len(set.Keys) != 1 || set.Keys[0].Kid != good.Kid {
t.Fatalf("應僅發佈完好的 %q,得到 %s", good.Kid, rec.Body.String())
}
}