diff --git a/backend/internal/service/hydra_admin_service_test.go b/backend/internal/service/hydra_admin_service_test.go new file mode 100644 index 00000000..0004e590 --- /dev/null +++ b/backend/internal/service/hydra_admin_service_test.go @@ -0,0 +1,401 @@ +/* +이 테스트 파일은 HydraAdminService의 기능을 검증하기 위한 유닛 테스트입니다. +Hydra Admin API와의 통신을 시뮬레이션하기 위해 httptest 패키지를 사용하여 +실제 네트워크 호출 없이 HTTP 핸들러를 모킹(Mocking)하는 방식으로 작성되었습니다. + +주요 테스트 항목: +1. 클라이언트 관리: List, Get, Create, Update, Patch, Delete (성공 및 NotFound 케이스) +2. OIDC 인증/동의 흐름: GetConsentRequest, AcceptConsentRequest, RejectConsentRequest, GetLoginRequest, AcceptLoginRequest, RejectLoginRequest +3. 세션 관리: ListConsentSessions, RevokeConsentSessions +4. 존재하지 않는 리소스 접근 시 에러 처리 +*/ + +package service + +import ( + "baron-sso-backend/internal/domain" + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// clientForHandler는 주어진 핸들러로 요청을 보내는 http.Client를 반환합니다. +// ory_service_test.go에 정의된 것과 동일한 로직입니다. +func mockHydraClient(h http.Handler) *http.Client { + return &http.Client{ + Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) { + var bodyBytes []byte + if req.Body != nil { + bodyBytes, _ = io.ReadAll(req.Body) + } + r := httptest.NewRequest(req.Method, req.URL.String(), bytes.NewReader(bodyBytes)) + r.Header = req.Header.Clone() + + w := httptest.NewRecorder() + h.ServeHTTP(w, r) + return w.Result(), nil + }), + } +} + +func TestHydraAdminService_ListClients(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || !strings.Contains(r.URL.Path, "/clients") { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.String()) + return + } + + clients := []domain.HydraClient{ + {ClientID: "client-1", ClientName: "Client One"}, + {ClientID: "client-2", ClientName: "Client Two"}, + } + json.NewEncoder(w).Encode(clients) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + clients, err := svc.ListClients(context.Background(), 10, 0) + if err != nil { + t.Fatalf("ListClients failed: %v", err) + } + + if len(clients) != 2 { + t.Errorf("expected 2 clients, got %d", len(clients)) + } +} + +func TestHydraAdminService_GetClient_Success(t *testing.T) { + targetID := "test-client" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + client := domain.HydraClient{ + ClientID: targetID, + ClientName: "Test Client", + } + json.NewEncoder(w).Encode(client) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + client, err := svc.GetClient(context.Background(), targetID) + if err != nil { + t.Fatalf("GetClient failed: %v", err) + } + + if client.ClientID != targetID { + t.Errorf("expected %s, got %s", targetID, client.ClientID) + } +} + +func TestHydraAdminService_GetClient_NotFound(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + _, err := svc.GetClient(context.Background(), "non-existent") + if err != ErrHydraNotFound { + t.Errorf("expected ErrHydraNotFound, got %v", err) + } +} + +func TestHydraAdminService_PatchClientStatus(t *testing.T) { + targetID := "test-client" + status := "inactive" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPatch { + t.Errorf("expected PATCH, got %s", r.Method) + } + var patch []map[string]interface{} + json.NewDecoder(r.Body).Decode(&patch) + if patch[0]["value"] != status { + t.Errorf("expected status %s, got %v", status, patch[0]["value"]) + } + + client := domain.HydraClient{ClientID: targetID} + json.NewEncoder(w).Encode(client) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + _, err := svc.PatchClientStatus(context.Background(), targetID, status) + if err != nil { + t.Fatalf("PatchClientStatus failed: %v", err) + } +} + +func TestHydraAdminService_CreateClient(t *testing.T) { + newClient := domain.HydraClient{ClientID: "new-client"} + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(newClient) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + created, err := svc.CreateClient(context.Background(), newClient) + if err != nil { + t.Fatalf("CreateClient failed: %v", err) + } + if created.ClientID != newClient.ClientID { + t.Errorf("expected %s, got %s", newClient.ClientID, created.ClientID) + } +} + +func TestHydraAdminService_UpdateClient(t *testing.T) { + targetID := "test-client" + updatedClient := domain.HydraClient{ClientID: targetID, ClientName: "Updated"} + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("expected PUT, got %s", r.Method) + } + json.NewEncoder(w).Encode(updatedClient) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + res, err := svc.UpdateClient(context.Background(), targetID, updatedClient) + if err != nil { + t.Fatalf("UpdateClient failed: %v", err) + } + if res.ClientName != "Updated" { + t.Errorf("expected Updated, got %s", res.ClientName) + } +} + +func TestHydraAdminService_DeleteClient(t *testing.T) { + targetID := "test-client" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + t.Errorf("expected DELETE, got %s", r.Method) + } + w.WriteHeader(http.StatusNoContent) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + err := svc.DeleteClient(context.Background(), targetID) + if err != nil { + t.Fatalf("DeleteClient failed: %v", err) + } +} + +func TestHydraAdminService_ListConsentSessions(t *testing.T) { + subject := "user-123" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("subject") != subject { + t.Errorf("expected subject %s, got %s", subject, r.URL.Query().Get("subject")) + } + sessions := []domain.HydraConsentSession{{Subject: subject}} + json.NewEncoder(w).Encode(sessions) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + res, err := svc.ListConsentSessions(context.Background(), subject, "") + if err != nil { + t.Fatalf("ListConsentSessions failed: %v", err) + } + if len(res) != 1 { + t.Errorf("expected 1 session, got %d", len(res)) + } +} + +func TestHydraAdminService_RevokeConsentSessions(t *testing.T) { + subject := "user-123" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + t.Errorf("expected DELETE, got %s", r.Method) + } + w.WriteHeader(http.StatusNoContent) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + err := svc.RevokeConsentSessions(context.Background(), subject, "client-1") + if err != nil { + t.Fatalf("RevokeConsentSessions failed: %v", err) + } +} + +func TestHydraAdminService_GetConsentRequest(t *testing.T) { + challenge := "consent-challenge" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("consent_challenge") != challenge { + t.Errorf("expected challenge %s, got %s", challenge, r.URL.Query().Get("consent_challenge")) + } + resp := domain.HydraConsentRequest{Challenge: challenge} + json.NewEncoder(w).Encode(resp) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + req, err := svc.GetConsentRequest(context.Background(), challenge) + if err != nil { + t.Fatalf("GetConsentRequest failed: %v", err) + } + if req.Challenge != challenge { + t.Errorf("expected %s, got %s", challenge, req.Challenge) + } +} + +func TestHydraAdminService_RejectConsentRequest(t *testing.T) { + challenge := "consent-challenge" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("expected PUT, got %s", r.Method) + } + resp := struct { + RedirectTo string `json:"redirect_to"` + }{ + RedirectTo: "http://redirect", + } + json.NewEncoder(w).Encode(resp) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + resp, err := svc.RejectConsentRequest(context.Background(), challenge) + if err != nil { + t.Fatalf("RejectConsentRequest failed: %v", err) + } + if resp.RedirectTo != "http://redirect" { + t.Errorf("expected http://redirect, got %s", resp.RedirectTo) + } +} + +func TestHydraAdminService_RejectLoginRequest(t *testing.T) { + challenge := "login-challenge" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := struct { + RedirectTo string `json:"redirect_to"` + }{ + RedirectTo: "http://redirect", + } + json.NewEncoder(w).Encode(resp) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + resp, err := svc.RejectLoginRequest(context.Background(), challenge, "error", "desc") + if err != nil { + t.Fatalf("RejectLoginRequest failed: %v", err) + } + if resp.RedirectTo != "http://redirect" { + t.Errorf("expected http://redirect, got %s", resp.RedirectTo) + } +} + +func TestHydraAdminService_GetLoginRequest(t *testing.T) { + challenge := "login-challenge" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := domain.HydraLoginRequest{Challenge: challenge} + json.NewEncoder(w).Encode(resp) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + req, err := svc.GetLoginRequest(context.Background(), challenge) + if err != nil { + t.Fatalf("GetLoginRequest failed: %v", err) + } + if req.Challenge != challenge { + t.Errorf("expected %s, got %s", challenge, req.Challenge) + } +} + +func TestHydraAdminService_AcceptConsentRequest(t *testing.T) { + challenge := "consent-challenge" + grantInfo := &domain.HydraConsentRequest{RequestedScope: []string{"openid"}} + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := struct { + RedirectTo string `json:"redirect_to"` + }{ + RedirectTo: "http://callback", + } + json.NewEncoder(w).Encode(resp) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + resp, err := svc.AcceptConsentRequest(context.Background(), challenge, grantInfo, nil) + if err != nil { + t.Fatalf("AcceptConsentRequest failed: %v", err) + } + if resp.RedirectTo != "http://callback" { + t.Errorf("expected http://callback, got %s", resp.RedirectTo) + } +} + +func TestHydraAdminService_AcceptLoginRequest(t *testing.T) { + challenge := "login-challenge" + subject := "user@example.com" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := struct { + RedirectTo string `json:"redirect_to"` + }{ + RedirectTo: "http://callback", + } + json.NewEncoder(w).Encode(resp) + }) + + svc := &HydraAdminService{ + AdminURL: "http://hydra:4445", + HTTPClient: mockHydraClient(handler), + } + + resp, err := svc.AcceptLoginRequest(context.Background(), challenge, subject) + if err != nil { + t.Fatalf("AcceptLoginRequest failed: %v", err) + } + if resp.RedirectTo != "http://callback" { + t.Errorf("expected http://callback, got %s", resp.RedirectTo) + } +} \ No newline at end of file