// 外部測試套件(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()) } }