first commit

This commit is contained in:
2026-10-03 10:44:29 +08:00
parent f373cb8d37
commit bcf3d3769c
58 changed files with 4313 additions and 487 deletions
+137
View File
@@ -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())
}
}