forked from alterminal/alterminal
first commit
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
// 外部測試套件(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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user