1
0
forked from baron/baron-sso

비밀번호 재설정 중복 완료 요청 문제 수정

This commit is contained in:
2026-03-31 11:17:55 +09:00
parent df145b2957
commit 68114eea66
8 changed files with 309 additions and 31 deletions

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
@@ -254,6 +255,65 @@ func TestCompletePasswordReset_InvalidTokenRejectedEvenWhenLoginIDExists(t *test
}
}
func TestCompletePasswordReset_DuplicateTokenSubmitIsIdempotent(t *testing.T) {
const resetToken = "dup-token"
const loginID = "user@example.com"
const newPassword = "StrongPass1!"
redis := &testRedisRepo{
values: map[string]string{
prefixPwdResetToken + resetToken: loginID,
},
}
idp := &mockIdpProvider{
userExists: true,
err: nil,
}
h := &AuthHandler{
RedisService: redis,
IdpProvider: idp,
}
app := newResetFlowTestApp(h)
body, _ := json.Marshal(map[string]string{
"newPassword": newPassword,
})
url := fmt.Sprintf(
"/api/v1/auth/password/reset/complete?token=%s",
resetToken,
)
firstReq := httptest.NewRequest(http.MethodPost, url, bytes.NewReader(body))
firstReq.Header.Set("Content-Type", "application/json")
firstResp, err := app.Test(firstReq)
if err != nil {
t.Fatalf("first request failed: %v", err)
}
defer firstResp.Body.Close()
if firstResp.StatusCode != http.StatusOK {
t.Fatalf("expected first response to be 200, got %d", firstResp.StatusCode)
}
if idp.updateCallCount != 1 {
t.Fatalf("expected first request to update password once, got %d", idp.updateCallCount)
}
secondReq := httptest.NewRequest(http.MethodPost, url, bytes.NewReader(body))
secondReq.Header.Set("Content-Type", "application/json")
secondResp, err := app.Test(secondReq)
if err != nil {
t.Fatalf("second request failed: %v", err)
}
defer secondResp.Body.Close()
if secondResp.StatusCode != http.StatusOK {
t.Fatalf("expected duplicate response to be 200, got %d", secondResp.StatusCode)
}
if idp.updateCallCount != 1 {
t.Fatalf("expected duplicate request not to update password again, got %d", idp.updateCallCount)
}
}
func TestProcessPasswordResetToken_EncodesLoginIDInRedirect(t *testing.T) {
const token = "tok-enc"
const loginID = "user+alias@example.com"
@@ -295,6 +355,102 @@ func TestProcessPasswordResetToken_EncodesLoginIDInRedirect(t *testing.T) {
}
}
func TestPasswordResetVerifyAlias_AcceptsShortVePath(t *testing.T) {
const token = "tok-ve"
const loginID = "user@example.com"
redis := &testRedisRepo{
values: map[string]string{
prefixPwdResetToken + token: loginID,
},
}
h := &AuthHandler{
RedisService: redis,
}
app := fiber.New()
app.Get("/api/v1/auth/password/reset/ve", h.VerifyPasswordResetPage)
app.Post("/api/v1/auth/password/reset/ve", h.ProcessPasswordResetToken)
getReq := httptest.NewRequest(
http.MethodGet,
"/api/v1/auth/password/reset/ve?token="+token,
nil,
)
getResp, err := app.Test(getReq)
if err != nil {
t.Fatalf("get request failed: %v", err)
}
defer getResp.Body.Close()
if getResp.StatusCode != http.StatusOK {
t.Fatalf("expected alias GET to return 200, got %d", getResp.StatusCode)
}
postReq := httptest.NewRequest(
http.MethodPost,
"/api/v1/auth/password/reset/ve?token="+token,
nil,
)
postResp, err := app.Test(postReq)
if err != nil {
t.Fatalf("post request failed: %v", err)
}
defer postResp.Body.Close()
if postResp.StatusCode != http.StatusFound {
t.Fatalf("expected alias POST to return 302, got %d", postResp.StatusCode)
}
}
func TestPasswordResetVerifyPathToken_AcceptsShortVPath(t *testing.T) {
const token = "tok-path"
const loginID = "user@example.com"
redis := &testRedisRepo{
values: map[string]string{
prefixPwdResetToken + token: loginID,
},
}
h := &AuthHandler{
RedisService: redis,
}
app := fiber.New()
app.Get("/api/v1/auth/password/reset/v/:token", h.VerifyPasswordResetPage)
app.Post("/api/v1/auth/password/reset/v/:token", h.ProcessPasswordResetToken)
getReq := httptest.NewRequest(
http.MethodGet,
"/api/v1/auth/password/reset/v/"+token,
nil,
)
getResp, err := app.Test(getReq)
if err != nil {
t.Fatalf("get request failed: %v", err)
}
defer getResp.Body.Close()
if getResp.StatusCode != http.StatusOK {
t.Fatalf("expected path-token GET to return 200, got %d", getResp.StatusCode)
}
postReq := httptest.NewRequest(
http.MethodPost,
"/api/v1/auth/password/reset/v/"+token,
nil,
)
postResp, err := app.Test(postReq)
if err != nil {
t.Fatalf("post request failed: %v", err)
}
defer postResp.Body.Close()
if postResp.StatusCode != http.StatusFound {
t.Fatalf("expected path-token POST to return 302, got %d", postResp.StatusCode)
}
}
func TestPasswordResetInit_LegacyErrorResponseHasCodeViaMiddleware(t *testing.T) {
h := &AuthHandler{}
app := newResetInitAppWithErrorCodeEnricher(h)
@@ -326,3 +482,40 @@ func TestPasswordResetInit_LegacyErrorResponseHasCodeViaMiddleware(t *testing.T)
t.Fatalf("expected code=bad_request, got %v", got["code"])
}
}
func TestInitiatePasswordReset_SmsContainsVerifyLink(t *testing.T) {
t.Setenv("USERFRONT_URL", "https://sss.hmac.kr")
redis := &testRedisRepo{values: map[string]string{}}
smsSvc := &mockSmsService{}
h := &AuthHandler{
RedisService: redis,
IdpProvider: &mockIdpProvider{},
SmsService: smsSvc,
}
app := fiber.New()
app.Post("/api/v1/auth/password/reset/init", h.InitiatePasswordReset)
body, _ := json.Marshal(map[string]string{
"loginId": "01012345678",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/reset/init", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if !strings.Contains(smsSvc.lastContent, "/api/v1/auth/password/reset/v/") {
t.Fatalf("expected SMS to contain short path verify link, got %q", smsSvc.lastContent)
}
if strings.Contains(smsSvc.lastContent, "/reset-password?token=") {
t.Fatalf("expected direct reset-password link to be removed, got %q", smsSvc.lastContent)
}
}