Refactor OTP tests to remove deprecated /otp/create endpoint; update tests to reflect server-side OTP generation and enhance login flow validation.

This commit is contained in:
Max 2026-02-21 21:53:54 +08:00
parent 886c91372b
commit 406750c665

View file

@ -182,85 +182,10 @@ func TestOTPLoginInvalidJSON(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}
// ---------- POST /otp/create (protected) ----------
// ---------- POST /otp/create (disabled — HTTP endpoint removed for security) ----------
// Validation tests now covered by TestOTPServiceCreateValidation.
func TestOTPCreateUnauthorized(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
baseURL := ""
if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = openapi.Server.Config.BaseURL
}
body, _ := json.Marshal(map[string]interface{}{
"user_id": "someone",
"redirect": "/dashboard",
})
resp, err := http.Post(serverURL+baseURL+"/otp/create", "application/json", bytes.NewBuffer(body))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
func TestOTPCreateMissingRedirect(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
baseURL := ""
if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = openapi.Server.Config.BaseURL
}
tc, cleanup := setupTestData(t, serverURL)
defer cleanup()
body, _ := json.Marshal(map[string]interface{}{
"user_id": tc.UserID,
})
req, _ := http.NewRequest("POST", serverURL+baseURL+"/otp/create", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tc.Token)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
rawBody, _ := io.ReadAll(resp.Body)
t.Logf("Response status: %d, body: %s", resp.StatusCode, string(rawBody))
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}
func TestOTPCreateMissingUserAndMember(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
baseURL := ""
if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = openapi.Server.Config.BaseURL
}
tc, cleanup := setupTestData(t, serverURL)
defer cleanup()
body, _ := json.Marshal(map[string]interface{}{
"redirect": "/dashboard",
})
req, _ := http.NewRequest("POST", serverURL+baseURL+"/otp/create", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tc.Token)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
rawBody, _ := io.ReadAll(resp.Body)
t.Logf("Response status: %d, body: %s", resp.StatusCode, string(rawBody))
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}
// ---------- Full flow: create -> login ----------
// ---------- Full flow: create (service) -> login (HTTP) ----------
func TestOTPCreateAndLogin(t *testing.T) {
serverURL := testutils.Prepare(t)
@ -274,59 +199,36 @@ func TestOTPCreateAndLogin(t *testing.T) {
tc, cleanup := setupTestData(t, serverURL)
defer cleanup()
// Step 1: Create OTP code via API
createBody, _ := json.Marshal(map[string]interface{}{
"user_id": tc.UserID,
"redirect": "/test/dashboard",
code, err := otp.OTP.Create(&otp.GenerateParams{
UserID: tc.UserID,
TeamID: tc.TeamID,
Redirect: "/test/dashboard",
Consume: true,
})
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/otp/create", bytes.NewBuffer(createBody))
createReq.Header.Set("Content-Type", "application/json")
createReq.Header.Set("Authorization", "Bearer "+tc.Token)
createResp, err := http.DefaultClient.Do(createReq)
require.NoError(t, err)
defer createResp.Body.Close()
assert.Len(t, code, 12)
createBody2, _ := io.ReadAll(createResp.Body)
t.Logf("Create response status: %d, body: %s", createResp.StatusCode, string(createBody2))
require.Equal(t, http.StatusOK, createResp.StatusCode, "OTP create should succeed, body: %s", string(createBody2))
var createResult map[string]interface{}
err = json.Unmarshal(createBody2, &createResult)
require.NoError(t, err)
code, ok := createResult["code"].(string)
require.True(t, ok, "Response should contain a code string")
assert.Len(t, code, 12, "OTP code should be 12 characters long")
t.Logf("Created OTP code: %s", code)
// Step 2: Login with the OTP code (public endpoint)
loginBody, _ := json.Marshal(map[string]string{
"code": code,
"locale": "en-US",
})
// Login with the OTP code (public endpoint)
loginBody, _ := json.Marshal(map[string]string{"code": code, "locale": "en-US"})
loginResp, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody))
require.NoError(t, err)
defer loginResp.Body.Close()
loginBody2, _ := io.ReadAll(loginResp.Body)
t.Logf("Login response status: %d, body: %s", loginResp.StatusCode, string(loginBody2))
require.Equal(t, http.StatusOK, loginResp.StatusCode, "OTP login should succeed, body: %s", string(loginBody2))
loginRaw, _ := io.ReadAll(loginResp.Body)
t.Logf("Login response: %d, body: %s", loginResp.StatusCode, string(loginRaw))
require.Equal(t, http.StatusOK, loginResp.StatusCode, "body: %s", string(loginRaw))
var loginResult map[string]interface{}
err = json.Unmarshal(loginBody2, &loginResult)
require.NoError(t, err)
json.Unmarshal(loginRaw, &loginResult)
assert.Equal(t, "success", loginResult["status"])
assert.Equal(t, "/test/dashboard", loginResult["redirect"])
// Step 3: Verify the code is consumed (default Consume=true)
loginBody3, _ := json.Marshal(map[string]string{"code": code})
loginResp3, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody3))
// Verify the code is consumed (default Consume=true)
loginBody2, _ := json.Marshal(map[string]string{"code": code})
loginResp2, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody2))
require.NoError(t, err)
defer loginResp3.Body.Close()
assert.Equal(t, http.StatusUnauthorized, loginResp3.StatusCode, "Code should be consumed after first login")
defer loginResp2.Body.Close()
assert.Equal(t, http.StatusUnauthorized, loginResp2.StatusCode, "Code should be consumed after first login")
}
func TestOTPCreateAndLoginWithMemberID(t *testing.T) {
@ -341,36 +243,20 @@ func TestOTPCreateAndLoginWithMemberID(t *testing.T) {
tc, cleanup := setupTestData(t, serverURL)
defer cleanup()
// Create OTP with member_id instead of user_id
createBody, _ := json.Marshal(map[string]interface{}{
"team_id": tc.TeamID,
"member_id": tc.MemberID,
"redirect": "/member-login-test",
code, err := otp.OTP.Create(&otp.GenerateParams{
TeamID: tc.TeamID,
MemberID: tc.MemberID,
Redirect: "/member-login-test",
Consume: true,
})
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/otp/create", bytes.NewBuffer(createBody))
createReq.Header.Set("Content-Type", "application/json")
createReq.Header.Set("Authorization", "Bearer "+tc.Token)
createResp, err := http.DefaultClient.Do(createReq)
require.NoError(t, err)
defer createResp.Body.Close()
createRaw, _ := io.ReadAll(createResp.Body)
t.Logf("Create response: %d, body: %s", createResp.StatusCode, string(createRaw))
require.Equal(t, http.StatusOK, createResp.StatusCode, "body: %s", string(createRaw))
var createResult map[string]interface{}
json.Unmarshal(createRaw, &createResult)
code := createResult["code"].(string)
// Verify the stored payload has member_id
payload, err := otp.OTP.Verify(code)
require.NoError(t, err)
assert.Equal(t, tc.MemberID, payload.MemberID)
assert.Equal(t, tc.TeamID, payload.TeamID)
assert.Equal(t, "", payload.UserID, "user_id should be empty when member_id is used")
assert.Equal(t, "", payload.UserID)
// Login with the OTP code — should resolve member_id to user_id
loginBody, _ := json.Marshal(map[string]string{"code": code, "locale": "en-US"})
loginResp, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody))
require.NoError(t, err)
@ -378,7 +264,7 @@ func TestOTPCreateAndLoginWithMemberID(t *testing.T) {
loginRaw, _ := io.ReadAll(loginResp.Body)
t.Logf("Login response: %d, body: %s", loginResp.StatusCode, string(loginRaw))
require.Equal(t, http.StatusOK, loginResp.StatusCode, "OTP login with member_id should succeed, body: %s", string(loginRaw))
require.Equal(t, http.StatusOK, loginResp.StatusCode, "body: %s", string(loginRaw))
var loginResult map[string]interface{}
json.Unmarshal(loginRaw, &loginResult)
@ -398,28 +284,13 @@ func TestOTPCreateWithConsumeDisabled(t *testing.T) {
tc, cleanup := setupTestData(t, serverURL)
defer cleanup()
consumeFalse := false
createBody, _ := json.Marshal(map[string]interface{}{
"user_id": tc.UserID,
"redirect": "/reusable",
"consume": &consumeFalse,
code, err := otp.OTP.Create(&otp.GenerateParams{
UserID: tc.UserID,
TeamID: tc.TeamID,
Redirect: "/reusable",
Consume: false,
})
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/otp/create", bytes.NewBuffer(createBody))
createReq.Header.Set("Content-Type", "application/json")
createReq.Header.Set("Authorization", "Bearer "+tc.Token)
createResp, err := http.DefaultClient.Do(createReq)
require.NoError(t, err)
defer createResp.Body.Close()
createRaw, _ := io.ReadAll(createResp.Body)
t.Logf("Create response: %d, body: %s", createResp.StatusCode, string(createRaw))
require.Equal(t, http.StatusOK, createResp.StatusCode, "body: %s", string(createRaw))
var createResult map[string]interface{}
json.Unmarshal(createRaw, &createResult)
code := createResult["code"].(string)
t.Logf("Created reusable OTP code: %s", code)
// First login
loginBody, _ := json.Marshal(map[string]string{"code": code, "locale": "en-US"})
@ -433,42 +304,21 @@ func TestOTPCreateWithConsumeDisabled(t *testing.T) {
loginResp2, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody2))
require.NoError(t, err)
defer loginResp2.Body.Close()
assert.Equal(t, http.StatusOK, loginResp2.StatusCode, "Reusable OTP code should allow multiple logins")
}
func TestOTPCreateWithTokenExpiresIn(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
_ = serverURL
baseURL := ""
if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = openapi.Server.Config.BaseURL
}
tc, cleanup := setupTestData(t, serverURL)
defer cleanup()
createBody, _ := json.Marshal(map[string]interface{}{
"user_id": tc.UserID,
"redirect": "/custom-ttl",
"token_expires_in": 600,
code, err := otp.OTP.Create(&otp.GenerateParams{
UserID: "token_ttl_user",
Redirect: "/custom-ttl",
TokenExpiresIn: 600,
Consume: true,
})
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/otp/create", bytes.NewBuffer(createBody))
createReq.Header.Set("Content-Type", "application/json")
createReq.Header.Set("Authorization", "Bearer "+tc.Token)
createResp, err := http.DefaultClient.Do(createReq)
require.NoError(t, err)
defer createResp.Body.Close()
createRaw, _ := io.ReadAll(createResp.Body)
t.Logf("Create response: %d, body: %s", createResp.StatusCode, string(createRaw))
require.Equal(t, http.StatusOK, createResp.StatusCode, "body: %s", string(createRaw))
var createResult map[string]interface{}
json.Unmarshal(createRaw, &createResult)
code := createResult["code"].(string)
payload, err := otp.OTP.Verify(code)
require.NoError(t, err)
@ -671,33 +521,8 @@ func TestOTPServiceCreateWithCustomExpiry(t *testing.T) {
}
// ---------- Cross-team validation ----------
func TestOTPCreateCrossTeamRejected(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
baseURL := ""
if openapi.Server != nil && openapi.Server.Config != nil {
baseURL = openapi.Server.Config.BaseURL
}
tc, cleanup := setupTestData(t, serverURL)
defer cleanup()
body, _ := json.Marshal(map[string]interface{}{
"user_id": "user_not_in_team_" + fmt.Sprintf("%d", time.Now().UnixNano()),
"redirect": "/cross-team",
})
req, _ := http.NewRequest("POST", serverURL+baseURL+"/otp/create", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tc.Token)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusForbidden, resp.StatusCode, "Should reject OTP creation for user not in team")
}
// NOTE: Cross-team HTTP endpoint test removed — /otp/create is disabled.
// Server-side Process callers are trusted and should validate team membership themselves.
// ---------- OTP Login sets cookies (no refresh token) ----------
@ -713,25 +538,13 @@ func TestOTPLoginSetsAccessTokenCookieOnly(t *testing.T) {
tc, cleanup := setupTestData(t, serverURL)
defer cleanup()
createBody, _ := json.Marshal(map[string]interface{}{
"user_id": tc.UserID,
"redirect": "/cookie-test",
code, err := otp.OTP.Create(&otp.GenerateParams{
UserID: tc.UserID,
TeamID: tc.TeamID,
Redirect: "/cookie-test",
Consume: true,
})
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/otp/create", bytes.NewBuffer(createBody))
createReq.Header.Set("Content-Type", "application/json")
createReq.Header.Set("Authorization", "Bearer "+tc.Token)
createResp, err := http.DefaultClient.Do(createReq)
require.NoError(t, err)
defer createResp.Body.Close()
createRaw, _ := io.ReadAll(createResp.Body)
t.Logf("Create response: %d, body: %s", createResp.StatusCode, string(createRaw))
require.Equal(t, http.StatusOK, createResp.StatusCode, "body: %s", string(createRaw))
var createResult map[string]interface{}
json.Unmarshal(createRaw, &createResult)
code := createResult["code"].(string)
loginBody, _ := json.Marshal(map[string]string{"code": code})
loginResp, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody))