1
0
forked from baron/baron-sso

Hydra 연동 관련 핸들러 테스트 케이스 추가

This commit is contained in:
2026-02-09 16:27:45 +09:00
parent 15a27a6620
commit dd9e6394ad
7 changed files with 884 additions and 25 deletions

View File

@@ -0,0 +1,106 @@
package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/service"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
)
func TestRevokeLinkedRp_Success(t *testing.T) {
// Mock Hydra transport for revocation
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
// 1. Kratos whoami
if r.URL.Path == "/sessions/whoami" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"identity": map[string]interface{}{"id": "user-123"},
}), nil
}
// 2. Hydra Revoke
if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
return httpResponse(r, http.StatusNoContent, ""), nil
}
return httpResponse(r, http.StatusNotFound, "not found"), nil
})
client := &http.Client{Transport: transport}
origDefault := http.DefaultClient
http.DefaultClient = client
defer func() { http.DefaultClient = origDefault }()
auditRepo := &mockAuditRepo{}
h := &AuthHandler{
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: client,
},
AuditRepo: auditRepo,
}
app := fiber.New()
app.Delete("/api/v1/user/rp/linked/:id", h.RevokeLinkedRp)
req := httptest.NewRequest(http.MethodDelete, "/api/v1/user/rp/linked/app-1", nil)
req.Header.Set("Cookie", "valid")
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, 1, len(auditRepo.logs))
}
func TestListRpHistory_Aggregation(t *testing.T) {
now := time.Now()
auditRepo := &mockAuditRepo{
logs: []domain.AuditLog{
{
UserID: "user-123",
EventType: "consent.revoked", // Newest
Timestamp: now,
Details: `{"client_id":"app-1"}`,
},
{
UserID: "user-123",
EventType: "consent.granted", // Oldest
Timestamp: now.Add(-1 * time.Hour),
Details: `{"client_id":"app-1", "client_name":"App One"}`,
},
},
}
h := &AuthHandler{
AuditRepo: auditRepo,
}
app := fiber.New()
app.Get("/api/v1/user/rp/history", h.ListRpHistory)
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"identity": map[string]interface{}{"id": "user-123"},
}), nil
})
http.DefaultClient = &http.Client{Transport: transport}
req := httptest.NewRequest(http.MethodGet, "/api/v1/user/rp/history", nil)
req.Header.Set("Cookie", "valid")
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var res struct {
Items []struct {
ClientID string `json:"client_id"`
Status string `json:"status"`
} `json:"items"`
}
json.NewDecoder(resp.Body).Decode(&res)
assert.Equal(t, 1, len(res.Items))
assert.Equal(t, "app-1", res.Items[0].ClientID)
// Newest event (revoked) should win
assert.Equal(t, "revoked", res.Items[0].Status)
}

View File

@@ -0,0 +1,197 @@
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"
)
// --- Test Helpers ---
func newConsentTestApp(h *AuthHandler) *fiber.App {
app := fiber.New()
app.Get("/api/v1/auth/consent", h.GetConsentRequest)
app.Post("/api/v1/auth/consent/accept", h.AcceptConsentRequest)
return app
}
// --- Tests ---
func TestGetConsentRequest_Normal(t *testing.T) {
// Mock Hydra transport
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-123" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"challenge": "challenge-123",
"requested_scope": []string{"openid", "profile"},
"skip": false,
"subject": "user-123",
"client": map[string]interface{}{
"client_id": "client-app",
"client_name": "Test App",
},
}), nil
}
return httpResponse(r, http.StatusNotFound, "not found"), nil
})
client := &http.Client{Transport: transport}
origDefault := http.DefaultClient
http.DefaultClient = client
defer func() { http.DefaultClient = origDefault }()
h := &AuthHandler{
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: client,
},
}
app := newConsentTestApp(h)
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/consent?consent_challenge=challenge-123", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
assert.Equal(t, "challenge-123", body["challenge"])
assert.Equal(t, false, body["skip"])
}
func TestGetConsentRequest_Skip_AutoAccept(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
// Hydra: Get Consent Request
if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-skip" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"challenge": "challenge-skip",
"requested_scope": []string{"openid"},
"skip": true,
"subject": "user-123",
"client": map[string]interface{}{
"client_id": "client-app",
},
}), nil
}
// Kratos: Get Identity
if r.URL.Path == "/admin/identities/user-123" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"id": "user-123",
"traits": map[string]interface{}{
"email": "user@test.com",
},
}), nil
}
// Hydra: Accept Consent Request
if r.URL.Path == "/oauth2/auth/requests/consent/accept" && r.URL.Query().Get("consent_challenge") == "challenge-skip" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"redirect_to": "http://rp/cb",
}), nil
}
return httpResponse(r, http.StatusNotFound, "not found"), nil
})
client := &http.Client{Transport: transport}
origDefault := http.DefaultClient
http.DefaultClient = client
defer func() { http.DefaultClient = origDefault }()
consentRepo := &mockConsentRepo{}
h := &AuthHandler{
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: client,
},
KratosAdmin: &service.KratosAdminService{
AdminURL: "http://kratos.test",
HTTPClient: client,
},
ConsentRepo: consentRepo,
}
app := newConsentTestApp(h)
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/consent?consent_challenge=challenge-skip", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
assert.Equal(t, "http://rp/cb", body["redirectTo"])
}
func TestAcceptConsentRequest_Normal(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-accept" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"challenge": "challenge-accept",
"requested_scope": []string{"openid", "profile"},
"subject": "user-123",
"client": map[string]interface{}{
"client_id": "client-app",
},
}), nil
}
if r.URL.Path == "/admin/identities/user-123" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"id": "user-123",
"traits": map[string]interface{}{
"email": "user@test.com",
},
}), nil
}
if r.URL.Path == "/oauth2/auth/requests/consent/accept" && r.URL.Query().Get("consent_challenge") == "challenge-accept" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"redirect_to": "http://rp/cb",
}), nil
}
return httpResponse(r, http.StatusNotFound, "not found"), nil
})
client := &http.Client{Transport: transport}
origDefault := http.DefaultClient
http.DefaultClient = client
defer func() { http.DefaultClient = origDefault }()
auditRepo := &mockAuditRepo{}
consentRepo := &mockConsentRepo{}
h := &AuthHandler{
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: client,
},
KratosAdmin: &service.KratosAdminService{
AdminURL: "http://kratos.test",
HTTPClient: client,
},
AuditRepo: auditRepo,
ConsentRepo: consentRepo,
}
app := newConsentTestApp(h)
body, _ := json.Marshal(map[string]interface{}{
"consent_challenge": "challenge-accept",
"grant_scope": []string{"openid"},
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/consent/accept", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, 1, len(auditRepo.logs))
}

View File

@@ -0,0 +1,121 @@
package handler
import (
"baron-sso-backend/internal/domain"
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
)
// Mock services
type mockEmailService struct{}
func (m *mockEmailService) SendEmail(to, subject, body string) error { return nil }
type mockSmsService struct{}
func (m *mockSmsService) SendSms(to, content string) error { return nil }
func TestEnchantedLinkFlow_Email_Success(t *testing.T) {
redis := &mockRedisRepo{data: make(map[string]string)}
// Force "Not Supported" for InitiateLinkLogin only to trigger custom Enchanted Link logic
idp := &mockIdpProvider{
userExists: true,
initiateLinkErr: domain.ErrNotSupported,
}
h := &AuthHandler{
RedisService: redis,
IdpProvider: idp,
EmailService: &mockEmailService{},
SmsService: &mockSmsService{},
}
app := fiber.New()
app.Post("/api/v1/auth/enchanted-link/init", h.InitEnchantedLink)
app.Post("/api/v1/auth/enchanted-link/poll", h.PollEnchantedLink)
app.Post("/api/v1/auth/magic-link/verify", h.VerifyMagicLink)
t.Setenv("USERFRONT_URL", "http://userfront.test")
// 1. Init Enchanted Link (Email)
body, _ := json.Marshal(map[string]string{
"loginId": "user@example.com",
"method": "email",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/enchanted-link/init", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var initResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&initResp)
pendingRef := initResp["pendingRef"].(string)
assert.NotEmpty(t, pendingRef)
// Find the token key "enchanted_token:..." in mock redis
var token string
for k := range redis.data {
if len(k) > 16 && k[:16] == "enchanted_token:" {
token = k[16:]
break
}
}
assert.NotEmpty(t, token)
// 2. Verify Magic Link
verifyBody, _ := json.Marshal(map[string]interface{}{
"token": token,
"verifyOnly": true,
})
req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/magic-link/verify", bytes.NewReader(verifyBody))
req.Header.Set("Content-Type", "application/json")
resp, _ = app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
// 3. Poll (Success)
pollBody, _ := json.Marshal(map[string]string{"pendingRef": pendingRef})
req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/enchanted-link/poll", bytes.NewReader(pollBody))
req.Header.Set("Content-Type", "application/json")
resp, _ = app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var pollResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&pollResp)
assert.Equal(t, "ok", pollResp["status"])
assert.Equal(t, "valid-jwt", pollResp["sessionJwt"])
}
func TestEnchantedLinkFlow_Sms_Success(t *testing.T) {
redis := &mockRedisRepo{data: make(map[string]string)}
idp := &mockIdpProvider{
userExists: true,
initiateLinkErr: domain.ErrNotSupported,
}
h := &AuthHandler{
RedisService: redis,
IdpProvider: idp,
SmsService: &mockSmsService{},
}
app := fiber.New()
app.Post("/api/v1/auth/enchanted-link/init", h.InitEnchantedLink)
// 1. Init Enchanted Link (SMS)
body, _ := json.Marshal(map[string]string{
"loginId": "010-1234-5678",
"method": "sms",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/enchanted-link/init", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var initResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&initResp)
assert.NotEmpty(t, initResp["userCode"])
}

View File

@@ -0,0 +1,144 @@
package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/service"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
)
// --- Helper ---
func newLinkedRpTestApp(h *AuthHandler) *fiber.App {
app := fiber.New()
app.Get("/api/v1/user/rp/linked", h.ListLinkedRps)
return app
}
// --- Tests ---
func TestListLinkedRps_PriorityAndAggregation(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
switch r.URL.Host {
case "kratos.test":
if r.URL.Path == "/sessions/whoami" {
if r.Header.Get("X-Session-Token") == "" && r.Header.Get("Cookie") == "" {
return httpResponse(r, http.StatusUnauthorized, "unauthorized"), nil
}
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"identity": map[string]interface{}{
"id": "user-123",
"traits": map[string]interface{}{
"email": "user@test.com",
},
},
}), nil
}
case "hydra.test":
if r.URL.Path == "/oauth2/auth/sessions/consent" {
return httpJSONAny(r, http.StatusOK, []map[string]interface{}{
{
"client": map[string]interface{}{
"client_id": "client-active",
"client_name": "Active App",
},
"granted_scope": []string{"openid"},
"handled_at": time.Now().Format(time.RFC3339),
},
}), nil
}
if r.URL.Path == "/admin/clients/client-audit" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"client_id": "client-audit",
"client_name": "Audit App",
}), nil
}
if r.URL.Path == "/admin/clients/client-consent" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"client_id": "client-consent",
"client_name": "Consent App",
}), nil
}
}
return httpResponse(r, http.StatusNotFound, "not found"), nil
})
client := &http.Client{Transport: transport}
origDefault := http.DefaultClient
http.DefaultClient = client
defer func() {
http.DefaultClient = origDefault
}()
auditRepo := &mockAuditRepo{
logs: []domain.AuditLog{
{
UserID: "user-123",
EventType: "consent.granted",
Timestamp: time.Now().Add(-10 * time.Hour),
Details: `{"client_id":"client-audit", "scopes":["audit_scope"]}`,
},
},
}
consentRepo := &mockConsentRepo{
consents: []domain.ClientConsent{
{
Subject: "user-123",
ClientID: "client-consent",
GrantedScopes: []string{"consent_scope"},
UpdatedAt: time.Now().Add(-2 * time.Hour),
},
},
}
h := &AuthHandler{
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: client,
},
AuditRepo: auditRepo,
ConsentRepo: consentRepo,
KratosAdmin: &service.KratosAdminService{},
}
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
t.Setenv("KRATOS_ADMIN_URL", "http://kratos.test")
app := newLinkedRpTestApp(h)
req := httptest.NewRequest(http.MethodGet, "/api/v1/user/rp/linked", nil)
req.Header.Set("Cookie", "ory_kratos_session=valid")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var res struct {
Items []struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Scopes []string `json:"scopes"`
} `json:"items"`
}
json.NewDecoder(resp.Body).Decode(&res)
assert.Equal(t, 3, len(res.Items))
statusMap := make(map[string]string)
for _, item := range res.Items {
statusMap[item.ID] = item.Status
}
assert.Equal(t, "active", statusMap["client-active"])
assert.Equal(t, "inactive", statusMap["client-consent"])
assert.Equal(t, "inactive", statusMap["client-audit"])
}

View File

@@ -33,7 +33,7 @@ func TestAcceptOidcLoginRequest_CookieOnly(t *testing.T) {
if r.Header.Get("Cookie") == "" {
return httpResponse(r, http.StatusUnauthorized, "missing cookie"), nil
}
return httpJSON(r, http.StatusOK, map[string]interface{}{
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"identity": map[string]interface{}{
"id": "kratos-123",
"traits": map[string]interface{}{},
@@ -117,7 +117,7 @@ func TestAcceptOidcLoginRequest_TokenFallbackToCookie(t *testing.T) {
if r.Header.Get("Cookie") == "" {
return httpResponse(r, http.StatusUnauthorized, "missing cookie"), nil
}
return httpJSON(r, http.StatusOK, map[string]interface{}{
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"identity": map[string]interface{}{
"id": "kratos-456",
"traits": map[string]interface{}{},
@@ -175,26 +175,4 @@ func TestAcceptOidcLoginRequest_TokenFallbackToCookie(t *testing.T) {
if gotSubject != "kratos-456" {
t.Fatalf("unexpected subject: %v", gotSubject)
}
}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}
func httpResponse(req *http.Request, status int, body string) *http.Response {
return &http.Response{
StatusCode: status,
Header: make(http.Header),
Body: io.NopCloser(bytes.NewBufferString(body)),
Request: req,
}
}
func httpJSON(req *http.Request, status int, payload map[string]interface{}) *http.Response {
data, _ := json.Marshal(payload)
resp := httpResponse(req, status, string(data))
resp.Header.Set("Content-Type", "application/json")
return resp
}
}

View File

@@ -0,0 +1,110 @@
package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
)
func TestHandleKratosCourierRelay_Email(t *testing.T) {
redis := &mockRedisRepo{data: make(map[string]string)}
emailSvc := &mockEmailService{}
h := &AuthHandler{
RedisService: redis,
EmailService: emailSvc,
}
app := fiber.New()
app.Post("/api/v1/auth/kratos/courier", h.HandleKratosCourierRelay)
// Simulate Kratos Courier Request for Email
reqBody := map[string]interface{}{
"recipient": "user@example.com",
"template_type": "verification_code",
"template_data": map[string]interface{}{
"verification_code": "123456",
},
"subject": "Verify your email",
"body": "Your code is 123456",
}
body, _ := json.Marshal(reqBody)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/kratos/courier", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestVerifySignupCode_Success(t *testing.T) {
redis := &mockRedisRepo{data: make(map[string]string)}
h := &AuthHandler{
RedisService: redis,
}
app := fiber.New()
app.Post("/api/v1/auth/signup/verify", h.VerifySignupCode)
// Mock stored code in redis
// signup:email:user@test.com -> {"code":"654321", "verified":false, "expires_at":...}
state := map[string]interface{}{
"code": "654321",
"verified": false,
"expires_at": 9999999999, // far future
}
stateJSON, _ := json.Marshal(state)
redis.data["signup:email:user@test.com"] = string(stateJSON)
// Verify Code
verifyBody := map[string]string{
"type": "email",
"target": "user@test.com",
"code": "654321",
}
body, _ := json.Marshal(verifyBody)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/signup/verify", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var res map[string]interface{}
json.NewDecoder(resp.Body).Decode(&res)
assert.True(t, res["success"].(bool))
// Check redis state updated to verified
val, _ := redis.Get("signup:email:user@test.com")
var updatedState map[string]interface{}
json.Unmarshal([]byte(val), &updatedState)
assert.True(t, updatedState["verified"].(bool))
}
func TestVerifySignupCode_Invalid(t *testing.T) {
redis := &mockRedisRepo{data: make(map[string]string)}
h := &AuthHandler{
RedisService: redis,
}
app := fiber.New()
app.Post("/api/v1/auth/signup/verify", h.VerifySignupCode)
stateJSON, _ := json.Marshal(map[string]interface{}{
"code": "111111",
"expires_at": 9999999999,
})
redis.data["signup:email:user@test.com"] = string(stateJSON)
verifyBody := map[string]string{
"type": "email",
"target": "user@test.com",
"code": "000000", // wrong code
}
body, _ := json.Marshal(verifyBody)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/signup/verify", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}

View File

@@ -0,0 +1,203 @@
package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
)
// --- Mock Redis ---
type mockRedisRepo struct {
data map[string]string
}
func (m *mockRedisRepo) Set(key, value string, ttl time.Duration) error {
if m.data == nil { m.data = make(map[string]string) }
m.data[key] = value
return nil
}
func (m *mockRedisRepo) Get(key string) (string, error) {
// Bypass rate limiting for tests
if strings.HasPrefix(key, "poll_meta:") {
return "", nil
}
return m.data[key], nil
}
func (m *mockRedisRepo) Delete(key string) error {
delete(m.data, key)
return nil
}
func (m *mockRedisRepo) StoreVerificationCode(phone, code string) error {
return m.Set("sms:"+phone, code, time.Minute)
}
func (m *mockRedisRepo) GetVerificationCode(phone string) (string, error) {
return m.Get("sms:"+phone)
}
func (m *mockRedisRepo) DeleteVerificationCode(phone string) error {
return m.Delete("sms:"+phone)
}
// --- Tests ---
func TestQRLoginFlow_Success(t *testing.T) {
redis := &mockRedisRepo{data: make(map[string]string)}
h := &AuthHandler{
RedisService: redis,
}
app := fiber.New()
app.Post("/api/v1/auth/qr/init", h.InitQRLogin)
app.Post("/api/v1/auth/qr/poll", h.PollQRLogin)
// 1. Init QR Login
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/qr/init", nil)
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var initResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&initResp)
pendingRef := initResp["pendingRef"].(string)
// 2. Poll (Pending)
body, _ := json.Marshal(map[string]string{"pendingRef": pendingRef})
req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/qr/poll", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ = app.Test(req, -1)
// Expect authorization_pending (400)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var pollResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&pollResp)
assert.Equal(t, "authorization_pending", pollResp["error"])
// 3. Mock Approval
sessionData, _ := json.Marshal(map[string]string{
"status": "success",
"jwt": "mock-session-jwt",
})
redis.data["enchanted_session:"+pendingRef] = string(sessionData)
// 4. Poll (Success)
req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/qr/poll", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ = app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var successResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&successResp)
assert.Equal(t, "ok", successResp["status"])
assert.Equal(t, "mock-session-jwt", successResp["sessionJwt"])
}
func TestScanQRLogin_Success(t *testing.T) {
redis := &mockRedisRepo{data: make(map[string]string)}
idp := &mockIdpProvider{userExists: true}
h := &AuthHandler{
RedisService: redis,
IdpProvider: idp,
}
app := fiber.New()
app.Post("/api/v1/auth/qr/approve", h.ScanQRLogin)
pendingRef := "test-ref"
redis.data["enchanted_session:"+pendingRef] = `{"status":"pending"}`
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.URL.Path == "/sessions/whoami" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"identity": map[string]interface{}{
"id": "user-123",
"traits": map[string]interface{}{
"email": "user@example.com",
},
},
}), nil
}
return httpResponse(r, http.StatusNotFound, "not found"), nil
})
origDefault := http.DefaultClient
http.DefaultClient = &http.Client{Transport: transport}
defer func() { http.DefaultClient = origDefault }()
body, _ := json.Marshal(map[string]string{
"pendingRef": pendingRef,
"token": "valid-token",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/qr/approve", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestResolveConsentSubjects_TokenAndCookie(t *testing.T) {
h := &AuthHandler{}
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.Header.Get("X-Session-Token") == "token-123" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"identity": map[string]interface{}{
"id": "user-token",
"traits": map[string]interface{}{
"email": "token@test.com",
},
},
}), nil
}
if r.Header.Get("Cookie") == "ory_kratos_session=cookie-123" {
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"identity": map[string]interface{}{
"id": "user-cookie",
"traits": map[string]interface{}{
"email": "cookie@test.com",
"phone": "010-1234-5678",
},
},
}), nil
}
return httpResponse(r, http.StatusUnauthorized, "unauthorized"), nil
})
origDefault := http.DefaultClient
http.DefaultClient = &http.Client{Transport: transport}
defer func() { http.DefaultClient = origDefault }()
app := fiber.New()
// Token case
app.Get("/test-token", func(c *fiber.Ctx) error {
subjects, err := h.resolveConsentSubjects(c)
assert.NoError(t, err)
assert.Contains(t, subjects, "user-token")
return c.SendStatus(200)
})
req := httptest.NewRequest("GET", "/test-token", nil)
req.Header.Set("Authorization", "Bearer token-123")
app.Test(req, -1)
// Cookie case
app.Get("/test-cookie", func(c *fiber.Ctx) error {
subjects, err := h.resolveConsentSubjects(c)
assert.NoError(t, err)
assert.Contains(t, subjects, "user-cookie")
return c.SendStatus(200)
})
req = httptest.NewRequest("GET", "/test-cookie", nil)
req.Header.Set("Cookie", "ory_kratos_session=cookie-123")
app.Test(req, -1)
}