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) 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) { // ---------- Full flow: create (service) -> login (HTTP) ----------
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 ----------
func TestOTPCreateAndLogin(t *testing.T) { func TestOTPCreateAndLogin(t *testing.T) {
serverURL := testutils.Prepare(t) serverURL := testutils.Prepare(t)
@ -274,59 +199,36 @@ func TestOTPCreateAndLogin(t *testing.T) {
tc, cleanup := setupTestData(t, serverURL) tc, cleanup := setupTestData(t, serverURL)
defer cleanup() defer cleanup()
// Step 1: Create OTP code via API code, err := otp.OTP.Create(&otp.GenerateParams{
createBody, _ := json.Marshal(map[string]interface{}{ UserID: tc.UserID,
"user_id": tc.UserID, TeamID: tc.TeamID,
"redirect": "/test/dashboard", 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) require.NoError(t, err)
defer createResp.Body.Close() assert.Len(t, code, 12)
createBody2, _ := io.ReadAll(createResp.Body) // Login with the OTP code (public endpoint)
t.Logf("Create response status: %d, body: %s", createResp.StatusCode, string(createBody2)) loginBody, _ := json.Marshal(map[string]string{"code": code, "locale": "en-US"})
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",
})
loginResp, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody)) loginResp, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody))
require.NoError(t, err) require.NoError(t, err)
defer loginResp.Body.Close() defer loginResp.Body.Close()
loginBody2, _ := io.ReadAll(loginResp.Body) loginRaw, _ := io.ReadAll(loginResp.Body)
t.Logf("Login response status: %d, body: %s", loginResp.StatusCode, string(loginBody2)) t.Logf("Login response: %d, body: %s", loginResp.StatusCode, string(loginRaw))
require.Equal(t, http.StatusOK, loginResp.StatusCode, "OTP login should succeed, body: %s", string(loginBody2)) require.Equal(t, http.StatusOK, loginResp.StatusCode, "body: %s", string(loginRaw))
var loginResult map[string]interface{} var loginResult map[string]interface{}
err = json.Unmarshal(loginBody2, &loginResult) json.Unmarshal(loginRaw, &loginResult)
require.NoError(t, err)
assert.Equal(t, "success", loginResult["status"]) assert.Equal(t, "success", loginResult["status"])
assert.Equal(t, "/test/dashboard", loginResult["redirect"]) assert.Equal(t, "/test/dashboard", loginResult["redirect"])
// Step 3: Verify the code is consumed (default Consume=true) // Verify the code is consumed (default Consume=true)
loginBody3, _ := json.Marshal(map[string]string{"code": code}) loginBody2, _ := json.Marshal(map[string]string{"code": code})
loginResp3, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody3)) loginResp2, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody2))
require.NoError(t, err) require.NoError(t, err)
defer loginResp3.Body.Close() defer loginResp2.Body.Close()
assert.Equal(t, http.StatusUnauthorized, loginResp2.StatusCode, "Code should be consumed after first login")
assert.Equal(t, http.StatusUnauthorized, loginResp3.StatusCode, "Code should be consumed after first login")
} }
func TestOTPCreateAndLoginWithMemberID(t *testing.T) { func TestOTPCreateAndLoginWithMemberID(t *testing.T) {
@ -341,36 +243,20 @@ func TestOTPCreateAndLoginWithMemberID(t *testing.T) {
tc, cleanup := setupTestData(t, serverURL) tc, cleanup := setupTestData(t, serverURL)
defer cleanup() defer cleanup()
// Create OTP with member_id instead of user_id code, err := otp.OTP.Create(&otp.GenerateParams{
createBody, _ := json.Marshal(map[string]interface{}{ TeamID: tc.TeamID,
"team_id": tc.TeamID, MemberID: tc.MemberID,
"member_id": tc.MemberID, Redirect: "/member-login-test",
"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) 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) payload, err := otp.OTP.Verify(code)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tc.MemberID, payload.MemberID) assert.Equal(t, tc.MemberID, payload.MemberID)
assert.Equal(t, tc.TeamID, payload.TeamID) 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"}) loginBody, _ := json.Marshal(map[string]string{"code": code, "locale": "en-US"})
loginResp, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody)) loginResp, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody))
require.NoError(t, err) require.NoError(t, err)
@ -378,7 +264,7 @@ func TestOTPCreateAndLoginWithMemberID(t *testing.T) {
loginRaw, _ := io.ReadAll(loginResp.Body) loginRaw, _ := io.ReadAll(loginResp.Body)
t.Logf("Login response: %d, body: %s", loginResp.StatusCode, string(loginRaw)) 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{} var loginResult map[string]interface{}
json.Unmarshal(loginRaw, &loginResult) json.Unmarshal(loginRaw, &loginResult)
@ -398,28 +284,13 @@ func TestOTPCreateWithConsumeDisabled(t *testing.T) {
tc, cleanup := setupTestData(t, serverURL) tc, cleanup := setupTestData(t, serverURL)
defer cleanup() defer cleanup()
consumeFalse := false code, err := otp.OTP.Create(&otp.GenerateParams{
createBody, _ := json.Marshal(map[string]interface{}{ UserID: tc.UserID,
"user_id": tc.UserID, TeamID: tc.TeamID,
"redirect": "/reusable", Redirect: "/reusable",
"consume": &consumeFalse, 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) 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 // First login
loginBody, _ := json.Marshal(map[string]string{"code": code, "locale": "en-US"}) 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)) loginResp2, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody2))
require.NoError(t, err) require.NoError(t, err)
defer loginResp2.Body.Close() defer loginResp2.Body.Close()
assert.Equal(t, http.StatusOK, loginResp2.StatusCode, "Reusable OTP code should allow multiple logins") assert.Equal(t, http.StatusOK, loginResp2.StatusCode, "Reusable OTP code should allow multiple logins")
} }
func TestOTPCreateWithTokenExpiresIn(t *testing.T) { func TestOTPCreateWithTokenExpiresIn(t *testing.T) {
serverURL := testutils.Prepare(t) serverURL := testutils.Prepare(t)
defer testutils.Clean() defer testutils.Clean()
_ = serverURL
baseURL := "" code, err := otp.OTP.Create(&otp.GenerateParams{
if openapi.Server != nil && openapi.Server.Config != nil { UserID: "token_ttl_user",
baseURL = openapi.Server.Config.BaseURL Redirect: "/custom-ttl",
} TokenExpiresIn: 600,
Consume: true,
tc, cleanup := setupTestData(t, serverURL)
defer cleanup()
createBody, _ := json.Marshal(map[string]interface{}{
"user_id": tc.UserID,
"redirect": "/custom-ttl",
"token_expires_in": 600,
}) })
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) 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) payload, err := otp.OTP.Verify(code)
require.NoError(t, err) require.NoError(t, err)
@ -671,33 +521,8 @@ func TestOTPServiceCreateWithCustomExpiry(t *testing.T) {
} }
// ---------- Cross-team validation ---------- // ---------- Cross-team validation ----------
// NOTE: Cross-team HTTP endpoint test removed — /otp/create is disabled.
func TestOTPCreateCrossTeamRejected(t *testing.T) { // Server-side Process callers are trusted and should validate team membership themselves.
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")
}
// ---------- OTP Login sets cookies (no refresh token) ---------- // ---------- OTP Login sets cookies (no refresh token) ----------
@ -713,25 +538,13 @@ func TestOTPLoginSetsAccessTokenCookieOnly(t *testing.T) {
tc, cleanup := setupTestData(t, serverURL) tc, cleanup := setupTestData(t, serverURL)
defer cleanup() defer cleanup()
createBody, _ := json.Marshal(map[string]interface{}{ code, err := otp.OTP.Create(&otp.GenerateParams{
"user_id": tc.UserID, UserID: tc.UserID,
"redirect": "/cookie-test", 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) 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}) loginBody, _ := json.Marshal(map[string]string{"code": code})
loginResp, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody)) loginResp, err := http.Post(serverURL+baseURL+"/otp/login", "application/json", bytes.NewBuffer(loginBody))