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) }