template-monorepo/test/e2e/member_test.go

201 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build e2e
package e2e
import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"testing"
"time"
membertotp "gateway/internal/model/member/totp"
"github.com/stretchr/testify/require"
)
func TestMember_GetMe(t *testing.T) {
e2eStep(t, "M-01", "GET", "/api/v1/members/me", "讀 profiletenant/uid/status")
c := NewClient(t)
env := c.DoExpectOK(t, http.MethodGet, "/api/v1/members/me", nil, true)
var me struct {
TenantID string `json:"tenant_id"`
UID string `json:"uid"`
Status string `json:"status"`
}
require.NoError(t, json.Unmarshal(env.Data, &me))
require.Equal(t, c.Fixture.TenantID, me.TenantID)
require.Equal(t, c.Fixture.UID, me.UID)
require.Equal(t, "active", me.Status)
}
func TestMember_UpdateMe(t *testing.T) {
e2eStep(t, "M-02", "PATCH", "/api/v1/members/me", "更新 display_name")
c := NewClient(t)
name := "E2E Updated Name"
env := c.DoExpectOK(t, http.MethodPatch, "/api/v1/members/me", map[string]string{
"display_name": name,
}, true)
var me struct {
DisplayName string `json:"display_name"`
}
require.NoError(t, json.Unmarshal(env.Data, &me))
require.Equal(t, name, me.DisplayName)
}
func TestMember_EmailVerification_FullFlow(t *testing.T) {
e2eStep(t, "M-03/M-04", "POST", "/me/verifications/email/{start,confirm}", "業務 email OTP 申請 → 從 Redis 取碼 → 驗證 → email_verified=true")
c := NewClient(t)
target := "verified-e2e@example.com"
startEnv := c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/verifications/email/start", map[string]string{
"target": target,
}, true)
var start struct {
ChallengeID string `json:"challenge_id"`
ExpiresIn int `json:"expires_in"`
}
require.NoError(t, json.Unmarshal(startEnv.Data, &start))
require.NotEmpty(t, start.ChallengeID)
require.Positive(t, start.ExpiresIn)
code := FetchE2EOTP(t, start.ChallengeID)
c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/verifications/email/confirm", map[string]string{
"challenge_id": start.ChallengeID,
"code": code,
}, true)
env := c.DoExpectOK(t, http.MethodGet, "/api/v1/members/me", nil, true)
var me struct {
BusinessEmail string `json:"business_email"`
BusinessEmailVerified bool `json:"business_email_verified"`
}
require.NoError(t, json.Unmarshal(env.Data, &me))
require.Equal(t, target, me.BusinessEmail)
require.True(t, me.BusinessEmailVerified)
}
func TestMember_PhoneVerification_FullFlow(t *testing.T) {
e2eStep(t, "M-05/M-06", "POST", "/me/verifications/phone/{start,confirm}", "業務 phone OTP 申請 → 從 Redis 取碼 → 驗證 → phone_verified=true")
c := NewClient(t)
target := "+886912345678"
startEnv := c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/verifications/phone/start", map[string]string{
"target": target,
}, true)
var start struct {
ChallengeID string `json:"challenge_id"`
ExpiresIn int `json:"expires_in"`
}
require.NoError(t, json.Unmarshal(startEnv.Data, &start))
require.NotEmpty(t, start.ChallengeID)
require.Positive(t, start.ExpiresIn)
code := FetchE2EOTP(t, start.ChallengeID)
c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/verifications/phone/confirm", map[string]string{
"challenge_id": start.ChallengeID,
"code": code,
}, true)
env := c.DoExpectOK(t, http.MethodGet, "/api/v1/members/me", nil, true)
var me struct {
BusinessPhone string `json:"business_phone"`
BusinessPhoneVerified bool `json:"business_phone_verified"`
}
require.NoError(t, json.Unmarshal(env.Data, &me))
require.Equal(t, target, me.BusinessPhone)
require.True(t, me.BusinessPhoneVerified)
}
func TestMember_TOTP_Status(t *testing.T) {
e2eStep(t, "M-07", "GET", "/api/v1/members/me/totp", "查 TOTP 狀態(初始 enrolled=false")
c := NewClient(t)
env := c.DoExpectOK(t, http.MethodGet, "/api/v1/members/me/totp", nil, true)
var st struct {
Enrolled bool `json:"enrolled"`
}
require.NoError(t, json.Unmarshal(env.Data, &st))
require.False(t, st.Enrolled)
}
func TestMember_TOTP_FullFlow(t *testing.T) {
e2eStep(t, "M-08~M-12", "POST", "/me/totp/*", "TOTP 全鏈路enroll-start → confirm → verify → replay 403 → backup-codes → DELETE")
c := NewClient(t)
startEnv := c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/totp/enroll-start", nil, true)
var start struct {
OtpauthURL string `json:"otpauth_url"`
Digits int `json:"digits"`
PeriodSec int `json:"period_seconds"`
}
require.NoError(t, json.Unmarshal(startEnv.Data, &start))
require.NotEmpty(t, start.OtpauthURL)
require.Positive(t, start.Digits)
require.Positive(t, start.PeriodSec)
code := codeFromOtpauthURL(t, start.OtpauthURL, start.Digits, start.PeriodSec)
confirmEnv := c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/totp/enroll-confirm", map[string]string{
"code": code,
}, true)
var confirmed struct {
BackupCodes []string `json:"backup_codes"`
}
require.NoError(t, json.Unmarshal(confirmEnv.Data, &confirmed))
require.NotEmpty(t, confirmed.BackupCodes)
statusEnv := c.DoExpectOK(t, http.MethodGet, "/api/v1/members/me/totp", nil, true)
var status struct {
Enrolled bool `json:"enrolled"`
BackupCodesRemaining int `json:"backup_codes_remaining"`
}
require.NoError(t, json.Unmarshal(statusEnv.Data, &status))
require.True(t, status.Enrolled)
require.Equal(t, len(confirmed.BackupCodes), status.BackupCodesRemaining)
c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/totp/verify", map[string]string{
"code": code,
}, true)
replayEnv := c.DoExpectHTTP(t, http.MethodPost, "/api/v1/members/me/totp/verify", map[string]string{
"code": code,
}, true, http.StatusForbidden)
require.NotEqual(t, int64(successCode), replayEnv.Code)
backupEnv := c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/totp/backup-codes", nil, true)
var backup struct {
BackupCodes []string `json:"backup_codes"`
}
require.NoError(t, json.Unmarshal(backupEnv.Data, &backup))
require.NotEmpty(t, backup.BackupCodes)
c.DoExpectOK(t, http.MethodDelete, "/api/v1/members/me/totp", nil, true)
finalEnv := c.DoExpectOK(t, http.MethodGet, "/api/v1/members/me/totp", nil, true)
var finalStatus struct {
Enrolled bool `json:"enrolled"`
}
require.NoError(t, json.Unmarshal(finalEnv.Data, &finalStatus))
require.False(t, finalStatus.Enrolled)
}
func codeFromOtpauthURL(t *testing.T, rawURL string, digits, periodSec int) string {
t.Helper()
u, err := url.Parse(rawURL)
require.NoError(t, err)
require.Equal(t, "otpauth", u.Scheme)
require.Equal(t, "totp", u.Host)
q := u.Query()
secret, err := membertotp.DecodeSecret(q.Get("secret"))
require.NoError(t, err)
if digits <= 0 {
digits, _ = strconv.Atoi(q.Get("digits"))
}
if periodSec <= 0 {
periodSec, _ = strconv.Atoi(q.Get("period"))
}
code, err := membertotp.Generate(secret, time.Now(), time.Duration(periodSec)*time.Second, digits)
require.NoError(t, err)
return code
}