template-monorepo/test/e2e/journey_owner_test.go

163 lines
5.9 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"
"testing"
"github.com/stretchr/testify/require"
)
// TestJourney_OwnerOnboarding 模擬 Tenant Owner 入職第一天會走的完整流程:
// 已 loginseed 提供 JWT→ 看自己 → 更新 profile → 驗證業務 email → 驗證 phone
// → 綁定 TOTP → step-up 驗碼 → 解除 TOTP → logout。
//
// 是「狀態流」測試每步都用上一步的結果challenge_id / display_name / OTP code
// 任一步 fail 後續自動 skip便於一眼定位斷點。
func TestJourney_OwnerOnboarding(t *testing.T) {
j := NewJourney(t, "J-1", "Tenant Owner 入職第一天(已登入後完整 onboarding")
defer j.Summary()
c := NewClient(t)
var (
emailTarget = "owner-journey@example.com"
phoneTarget = "+886900111222"
emailChallenge string
phoneChallenge string
otpauthURL string
totpDigits int
totpPeriod int
totpCode string
)
j.Step("1", "GET /me — 用 seed JWT 確認自己是 tenant_owner 且 status=active", func(t *testing.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.UID, me.UID)
require.Equal(t, "active", me.Status)
})
j.Step("2", "PATCH /me — 更新 display_name", func(t *testing.T) {
env := c.DoExpectOK(t, http.MethodPatch, "/api/v1/members/me", map[string]string{
"display_name": "Journey Owner",
}, true)
var me struct {
DisplayName string `json:"display_name"`
}
require.NoError(t, json.Unmarshal(env.Data, &me))
require.Equal(t, "Journey Owner", me.DisplayName)
})
j.Step("3", "POST /me/verifications/email/start — 申請業務 email OTP", func(t *testing.T) {
env := c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/verifications/email/start", map[string]string{
"target": emailTarget,
}, true)
var start struct {
ChallengeID string `json:"challenge_id"`
}
require.NoError(t, json.Unmarshal(env.Data, &start))
require.NotEmpty(t, start.ChallengeID)
emailChallenge = start.ChallengeID
})
j.Step("4", "POST /me/verifications/email/confirm — 從 Redis 取碼後驗證", func(t *testing.T) {
require.NotEmpty(t, emailChallenge, "missing email challenge from step 3")
code := FetchE2EOTP(t, emailChallenge)
c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/verifications/email/confirm", map[string]string{
"challenge_id": emailChallenge,
"code": code,
}, true)
})
j.Step("5", "GET /me — 確認 business_email_verified=true", func(t *testing.T) {
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, emailTarget, me.BusinessEmail)
require.True(t, me.BusinessEmailVerified, "expected email_verified=true after confirm")
})
j.Step("6", "POST /me/verifications/phone/start — 申請業務 phone OTP", func(t *testing.T) {
env := c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/verifications/phone/start", map[string]string{
"target": phoneTarget,
}, true)
var start struct {
ChallengeID string `json:"challenge_id"`
}
require.NoError(t, json.Unmarshal(env.Data, &start))
phoneChallenge = start.ChallengeID
})
j.Step("7", "POST /me/verifications/phone/confirm — 取碼後驗證", func(t *testing.T) {
require.NotEmpty(t, phoneChallenge)
code := FetchE2EOTP(t, phoneChallenge)
c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/verifications/phone/confirm", map[string]string{
"challenge_id": phoneChallenge,
"code": code,
}, true)
})
j.Step("8", "POST /me/totp/enroll-start — 開始綁定 TOTP", func(t *testing.T) {
env := 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(env.Data, &start))
require.NotEmpty(t, start.OtpauthURL)
otpauthURL = start.OtpauthURL
totpDigits = start.Digits
totpPeriod = start.PeriodSec
})
j.Step("9", "POST /me/totp/enroll-confirm — 用 otpauth_url 算當下 TOTP code 確認綁定", func(t *testing.T) {
require.NotEmpty(t, otpauthURL)
totpCode = codeFromOtpauthURL(t, otpauthURL, totpDigits, totpPeriod)
env := c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/totp/enroll-confirm", map[string]string{
"code": totpCode,
}, true)
var confirmed struct {
BackupCodes []string `json:"backup_codes"`
}
require.NoError(t, json.Unmarshal(env.Data, &confirmed))
require.NotEmpty(t, confirmed.BackupCodes, "enroll-confirm should return backup codes exactly once")
})
j.Step("10", "POST /me/totp/verify — step-up 驗碼成功", func(t *testing.T) {
require.NotEmpty(t, totpCode)
c.DoExpectOK(t, http.MethodPost, "/api/v1/members/me/totp/verify", map[string]string{
"code": totpCode,
}, true)
})
j.Step("11", "POST /me/totp/verify (replay) — 同 code 再打應該 403重放保護", func(t *testing.T) {
require.NotEmpty(t, totpCode)
env := c.DoExpectHTTP(t, http.MethodPost, "/api/v1/members/me/totp/verify", map[string]string{
"code": totpCode,
}, true, http.StatusForbidden)
require.NotEqual(t, int64(successCode), env.Code)
})
j.Step("12", "DELETE /me/totp — 解除綁定", func(t *testing.T) {
c.DoExpectOK(t, http.MethodDelete, "/api/v1/members/me/totp", nil, true)
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)
})
}