1
0
forked from baron/baron-sso

df 클라이언트 상세 조회 테스트 추가

This commit is contained in:
2026-02-10 09:03:34 +09:00
parent 68459151c3
commit 0741baf60b
3 changed files with 206 additions and 1 deletions

View File

@@ -118,6 +118,24 @@ func (m *mockConsentRepo) ListByTenant(ctx context.Context, clientID, tenantID s
return nil, 0, nil
}
// --- Mock Secret Repository ---
type mockSecretRepo struct {
secrets map[string]string
}
func (m *mockSecretRepo) Upsert(ctx context.Context, clientID, secret string) error {
if m.secrets == nil { m.secrets = make(map[string]string) }
m.secrets[clientID] = secret
return nil
}
func (m *mockSecretRepo) GetByID(ctx context.Context, clientID string) (string, error) {
return m.secrets[clientID], nil
}
func (m *mockSecretRepo) Delete(ctx context.Context, clientID string) error {
delete(m.secrets, clientID)
return nil
}
// --- HTTP Mock Helpers ---

View File

@@ -0,0 +1,142 @@
package handler
import (
"baron-sso-backend/internal/service"
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
)
func TestListClients_Success(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.URL.Path == "/clients" {
return httpJSONAny(r, http.StatusOK, []map[string]interface{}{
{"client_id": "client-1", "client_name": "App One", "metadata": map[string]interface{}{"status": "active"}},
{"client_id": "client-2", "client_name": "App Two", "metadata": map[string]interface{}{"status": "inactive"}},
}), nil
}
return httpJSONAny(r, http.StatusNotFound, map[string]string{"error":"not found"}), nil
})
h := &DevHandler{
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: &http.Client{Transport: transport},
},
}
app := fiber.New()
app.Get("/api/v1/dev/clients", h.ListClients)
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var res struct {
Items []clientSummary `json:"items"`
}
json.NewDecoder(resp.Body).Decode(&res)
assert.Equal(t, 2, len(res.Items))
assert.Equal(t, "client-1", res.Items[0].ID)
assert.Equal(t, "App One", res.Items[0].Name)
}
func TestGetClient_Success(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.URL.Path == "/clients/client-123" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"client_id": "client-123",
"client_name": "Test App",
"metadata": map[string]interface{}{"status": "active"},
}), nil
}
return httpJSONAny(r, http.StatusNotFound, map[string]string{"error":"not found"}), nil
})
h := &DevHandler{
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
PublicURL: "http://hydra-public.test", // PublicURL 추가
HTTPClient: &http.Client{Transport: transport},
},
}
app := fiber.New()
app.Get("/api/v1/dev/clients/:id", h.GetClient)
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/client-123", nil)
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var res clientDetailResponse
json.NewDecoder(resp.Body).Decode(&res)
assert.Equal(t, "client-123", res.Client.ID)
assert.Equal(t, "Test App", res.Client.Name)
assert.Equal(t, "http://hydra-public.test/oauth2/auth", res.Endpoints.Authorization)
}
func TestGetClient_NotFound(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
return httpJSONAny(r, http.StatusNotFound, map[string]string{"error":"not found"}), nil
})
h := &DevHandler{
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: &http.Client{Transport: transport},
},
}
app := fiber.New()
app.Get("/api/v1/dev/clients/:id", h.GetClient)
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/non-existent", nil)
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}
func TestCreateClient_Success(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.Method == http.MethodPost && r.URL.Path == "/clients" {
return httpJSONAny(r, http.StatusCreated, map[string]interface{}{
"client_id": "new-client-123",
"client_name": "New App",
"client_secret": "secret-123",
}), nil
}
return httpJSONAny(r, http.StatusInternalServerError, map[string]string{"error":"hydra error"}), nil
})
secretRepo := &mockSecretRepo{secrets: make(map[string]string)}
redisRepo := &mockRedisRepo{data: make(map[string]string)}
h := &DevHandler{
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: &http.Client{Transport: transport},
},
SecretRepo: secretRepo,
Redis: redisRepo,
}
app := fiber.New()
app.Post("/api/v1/dev/clients", h.CreateClient)
body, _ := json.Marshal(map[string]interface{}{
"client_name": "New App",
"type": "confidential",
"redirectUris": []string{"http://localhost/cb"},
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
secret, _ := secretRepo.GetByID(nil, "new-client-123")
assert.Equal(t, "secret-123", secret)
}

View File

@@ -0,0 +1,45 @@
# Frontend 기능과 백엔드 테스트 매핑 가이드
이 문서는 `devfront``userfront`의 Hydra 관련 기능이 백엔드의 어떤 API를 호출하고, 해당 API가 어떤 테스트 코드로 검증되는지 설명합니다. 모든 기능은 백엔드에 이미 구현되어 있으며, '테스트' 열은 해당 기능을 검증하는 자동화 테스트의 존재 여부를 나타냅니다.
## 1. `devfront` (개발자/관리자 포털)
`devfront`는 OAuth2 클라이언트(RP)를 생성하고 관리하는 데 사용됩니다.
| `devfront` 기능 | 백엔드 API | 검증 테스트 파일 | 테스트 상태 |
| :--- | :--- | :--- | :--- |
| **클라이언트 목록 조회** | `GET /api/v1/dev/clients` | `dev_handler_test.go` | `TestListClients_Success` |
| **클라이언트 생성** | `POST /api/v1/dev/clients` | `dev_handler_test.go` | `TestCreateClient_Success` |
| **클라이언트 상세 조회** | `GET /api/v1/dev/clients/:id` | `dev_handler_test.go` | `TestGetClient_Success`, `TestGetClient_NotFound` |
| **클라이언트 정보 수정** | `PUT /api/v1/dev/clients/:id` | - | (테스트 미작성) |
| **클라이언트 상태 변경** | `PATCH /api/v1/dev/clients/:id/status`| - | (테스트 미작성) |
| **클라이언트 삭제** | `DELETE /api/v1/dev/clients/:id` | - | (테스트 미작성) |
| **시크릿 재발급** | `POST /api/v1/dev/clients/:id/rotate-secret`| - | (테스트 미작성) |
| **동의한 사용자 목록 조회**| `GET /api/v1/dev/consents` | - | (테스트 미작성) |
| **사용자 동의 철회** | `DELETE /api/v1/dev/consents` | - | (테스트 미작성) |
*참고: `dev_handler.go` 내의 기능들은 백엔드에 구현되어 있으나, 이번 커버리지 90% 달성 목표(핵심 인증 로직 중심)에서 관리자 기능으로 분류되어 우선순위가 조정되었습니다.*
---
## 2. `userfront` (사용자 포털)
`userfront`는 최종 사용자가 애플리케이션(RP)의 정보 접근 요청을 승인하거나 거부하는 OIDC 동의 화면 및 연동 관리를 처리합니다.
### 2.1. OIDC 동의 (Consent) 및 연동 관리
| `userfront` 기능 | 백엔드 API | 검증 테스트 파일 | 테스트 상태 |
| :--- | :--- | :--- | :--- |
| **동의 정보 조회** | `GET /api/v1/auth/consent` | `auth_handler_consent_test.go` | `TestGetConsentRequest_Normal` |
| **동의 승인** | `POST /api/v1/auth/consent/accept` | `auth_handler_consent_test.go` | `TestAcceptConsentRequest_Normal` |
| **동의 거부** | `POST /api/v1/auth/consent/reject` | - | (테스트 미작성) |
| **연동된 앱 목록 조회** | `GET /api/v1/user/rp/linked` | `auth_handler_linked_test.go` | `TestListLinkedRps_PriorityAndAggregation` |
| **연동 해제 (Revoke)** | `DELETE /api/v1/user/rp/linked/:id`| `auth_handler_client_test.go` | `TestRevokeLinkedRp_Success` |
| **연동 이력 조회** | `GET /api/v1/user/rp/history` | `auth_handler_client_test.go` | `TestListRpHistory_Aggregation` |
### 2.2. 인증 플로우 (Login Flows)
| `userfront` 기능 | 백엔드 API | 검증 테스트 파일 | 테스트 상태 |
| :--- | :--- | :--- | :--- |
| **QR 로그인 초기화** | `POST /api/v1/auth/qr/init` | `auth_handler_qr_test.go` | `TestQRLoginFlow_Success` |
| **QR 로그인 승인 (Scan)** | `POST /api/v1/auth/qr/approve` | `auth_handler_qr_test.go` | `TestScanQRLogin_Success` |
| **매직 링크 초기화** | `POST /api/v1/auth/enchanted-link/init`| `auth_handler_link_test.go` | `TestEnchantedLinkFlow_Email_Success` |
| **매직 링크 검증** | `POST /api/v1/auth/magic-link/verify` | `auth_handler_link_test.go` | `TestEnchantedLinkFlow_Email_Success` |