forked from baron/baron-sso
216 lines
6.8 KiB
Go
216 lines
6.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"baron-sso-backend/internal/domain"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type recordingUpdateMeUserRepo struct {
|
|
MockUserRepoForHandler
|
|
updated *domain.User
|
|
loginIDs []domain.UserLoginID
|
|
}
|
|
|
|
func (r *recordingUpdateMeUserRepo) Update(ctx context.Context, user *domain.User) error {
|
|
copied := *user
|
|
r.updated = &copied
|
|
return nil
|
|
}
|
|
|
|
func (r *recordingUpdateMeUserRepo) UpdateUserLoginIDs(ctx context.Context, userID string, loginIDs []domain.UserLoginID) error {
|
|
r.loginIDs = append([]domain.UserLoginID(nil), loginIDs...)
|
|
return nil
|
|
}
|
|
|
|
func TestUpdateMe_InvalidatesProfileCacheForTokenSession(t *testing.T) {
|
|
token := "token-abc"
|
|
identityID := "user-1"
|
|
traits := map[string]interface{}{
|
|
"email": "qa@example.com",
|
|
"name": "QA User",
|
|
"phone_number": "+821012345678",
|
|
"department": "Old Dept",
|
|
"affiliationType": "employee",
|
|
"companyCode": "",
|
|
"role": domain.RoleUser,
|
|
}
|
|
|
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
|
switch {
|
|
case r.URL.Host == "kratos.test" &&
|
|
r.URL.Path == "/sessions/whoami" &&
|
|
r.Method == http.MethodGet:
|
|
if r.Header.Get("X-Session-Token") != token {
|
|
return httpResponse(r, http.StatusUnauthorized, `{"error":"invalid token"}`), nil
|
|
}
|
|
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
|
|
"identity": map[string]interface{}{
|
|
"id": identityID,
|
|
"traits": traits,
|
|
},
|
|
}), nil
|
|
|
|
case r.URL.Host == "kratos.test" &&
|
|
r.URL.Path == "/admin/identities/"+identityID &&
|
|
r.Method == http.MethodPut:
|
|
var payload struct {
|
|
Traits map[string]interface{} `json:"traits"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
return httpResponse(r, http.StatusBadRequest, `{"error":"invalid body"}`), nil
|
|
}
|
|
for k, v := range payload.Traits {
|
|
traits[k] = v
|
|
}
|
|
return httpResponse(r, http.StatusOK, `{"ok":true}`), nil
|
|
}
|
|
|
|
return httpResponse(r, http.StatusNotFound, "not found"), nil
|
|
})
|
|
setDefaultHTTPClientForTest(t, transport)
|
|
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
|
|
t.Setenv("KRATOS_ADMIN_URL", "http://kratos.test")
|
|
|
|
redis := &mockRedisRepo{data: make(map[string]string)}
|
|
h := &AuthHandler{
|
|
RedisService: redis,
|
|
}
|
|
app := fiber.New()
|
|
app.Get("/api/v1/user/me", h.GetMe)
|
|
app.Put("/api/v1/user/me", h.UpdateMe)
|
|
|
|
// 1) 첫 조회로 Old Dept가 캐시에 저장됨
|
|
getReq1 := httptest.NewRequest(http.MethodGet, "/api/v1/user/me", nil)
|
|
getReq1.Header.Set("Authorization", "Bearer "+token)
|
|
getResp1, err := app.Test(getReq1, -1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, getResp1.StatusCode)
|
|
var profile1 map[string]interface{}
|
|
require.NoError(t, json.NewDecoder(getResp1.Body).Decode(&profile1))
|
|
require.Equal(t, "Old Dept", profile1["department"])
|
|
|
|
// 2) 소속을 New Dept로 변경
|
|
updateBody, _ := json.Marshal(map[string]string{
|
|
"name": "QA User",
|
|
"phone": "01012345678",
|
|
"department": "New Dept",
|
|
})
|
|
updateReq := httptest.NewRequest(
|
|
http.MethodPut,
|
|
"/api/v1/user/me",
|
|
bytes.NewReader(updateBody),
|
|
)
|
|
updateReq.Header.Set("Content-Type", "application/json")
|
|
updateReq.Header.Set("Authorization", "Bearer "+token)
|
|
updateResp, err := app.Test(updateReq, -1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, updateResp.StatusCode)
|
|
require.Equal(t, "New Dept", traits["department"])
|
|
|
|
// 3) 새로고침 재조회 시 New Dept가 보여야 함(캐시 무효화 회귀 방지)
|
|
getReq2 := httptest.NewRequest(http.MethodGet, "/api/v1/user/me", nil)
|
|
getReq2.Header.Set("Authorization", "Bearer "+token)
|
|
getResp2, err := app.Test(getReq2, -1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, getResp2.StatusCode)
|
|
var profile2 map[string]interface{}
|
|
require.NoError(t, json.NewDecoder(getResp2.Body).Decode(&profile2))
|
|
require.Equal(t, "New Dept", profile2["department"])
|
|
}
|
|
|
|
func TestUpdateMe_SyncsLocalReadModelFields(t *testing.T) {
|
|
token := "token-sync"
|
|
identityID := "user-sync"
|
|
traits := map[string]interface{}{
|
|
"email": "sync@example.com",
|
|
"name": "Old Name",
|
|
"phone_number": "+821012345678",
|
|
"department": "Old Dept",
|
|
"affiliationType": "employee",
|
|
"companyCode": "saman",
|
|
"tenant_id": "11111111-1111-1111-1111-111111111111",
|
|
"role": domain.RoleUser,
|
|
}
|
|
|
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
|
switch {
|
|
case r.URL.Host == "kratos.test" &&
|
|
r.URL.Path == "/sessions/whoami" &&
|
|
r.Method == http.MethodGet:
|
|
if r.Header.Get("X-Session-Token") != token {
|
|
return httpResponse(r, http.StatusUnauthorized, `{"error":"invalid token"}`), nil
|
|
}
|
|
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
|
|
"identity": map[string]interface{}{
|
|
"id": identityID,
|
|
"traits": traits,
|
|
},
|
|
}), nil
|
|
|
|
case r.URL.Host == "kratos.test" &&
|
|
r.URL.Path == "/admin/identities/"+identityID &&
|
|
r.Method == http.MethodPut:
|
|
var payload struct {
|
|
Traits map[string]interface{} `json:"traits"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
return httpResponse(r, http.StatusBadRequest, `{"error":"invalid body"}`), nil
|
|
}
|
|
for k, v := range payload.Traits {
|
|
traits[k] = v
|
|
}
|
|
return httpResponse(r, http.StatusOK, `{"ok":true}`), nil
|
|
}
|
|
|
|
return httpResponse(r, http.StatusNotFound, "not found"), nil
|
|
})
|
|
setDefaultHTTPClientForTest(t, transport)
|
|
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
|
|
t.Setenv("KRATOS_ADMIN_URL", "http://kratos.test")
|
|
|
|
redis := &mockRedisRepo{data: map[string]string{
|
|
"verify_update_phone:" + identityID + ":+821087654321": "verified",
|
|
}}
|
|
userRepo := &recordingUpdateMeUserRepo{}
|
|
h := &AuthHandler{
|
|
RedisService: redis,
|
|
UserRepo: userRepo,
|
|
}
|
|
app := fiber.New()
|
|
app.Put("/api/v1/user/me", h.UpdateMe)
|
|
|
|
updateBody, _ := json.Marshal(map[string]interface{}{
|
|
"name": "New Name",
|
|
"phone": "01087654321",
|
|
"department": "New Dept",
|
|
})
|
|
updateReq := httptest.NewRequest(
|
|
http.MethodPut,
|
|
"/api/v1/user/me",
|
|
bytes.NewReader(updateBody),
|
|
)
|
|
updateReq.Header.Set("Content-Type", "application/json")
|
|
updateReq.Header.Set("Authorization", "Bearer "+token)
|
|
updateResp, err := app.Test(updateReq, -1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, updateResp.StatusCode)
|
|
|
|
require.NotNil(t, userRepo.updated)
|
|
require.Equal(t, identityID, userRepo.updated.ID)
|
|
require.Equal(t, "sync@example.com", userRepo.updated.Email)
|
|
require.Equal(t, "New Name", userRepo.updated.Name)
|
|
require.Equal(t, "+821087654321", userRepo.updated.Phone)
|
|
require.Equal(t, "New Dept", userRepo.updated.Department)
|
|
require.Empty(t, userRepo.updated.CompanyCode)
|
|
require.NotNil(t, userRepo.updated.TenantID)
|
|
require.Equal(t, "11111111-1111-1111-1111-111111111111", *userRepo.updated.TenantID)
|
|
}
|