This commit is contained in:
@@ -1,170 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/middleware"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type legacyCheckCodeResponse struct {
|
||||
Code uint32 `json:"code"`
|
||||
Data struct {
|
||||
Status bool `json:"status"`
|
||||
Exist bool `json:"exist"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func newLegacyCheckCodeTestRouter(svcCtx *svc.ServiceContext) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(middleware.ApiVersionMiddleware(svcCtx))
|
||||
router.POST("/v1/auth/check-code", middleware.ApiVersionSwitchHandler(
|
||||
CheckCodeLegacyV1Handler(svcCtx),
|
||||
CheckCodeLegacyV2Handler(svcCtx),
|
||||
))
|
||||
return router
|
||||
}
|
||||
|
||||
func newLegacyCheckCodeTestSvcCtx(t *testing.T) (*svc.ServiceContext, *redis.Client) {
|
||||
t.Helper()
|
||||
|
||||
miniRedis := miniredis.RunT(t)
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: miniRedis.Addr()})
|
||||
t.Cleanup(func() {
|
||||
redisClient.Close()
|
||||
miniRedis.Close()
|
||||
})
|
||||
|
||||
svcCtx := &svc.ServiceContext{
|
||||
Redis: redisClient,
|
||||
Config: config.Config{
|
||||
VerifyCode: config.VerifyCode{
|
||||
VerifyCodeExpireTime: 900,
|
||||
},
|
||||
},
|
||||
}
|
||||
return svcCtx, redisClient
|
||||
}
|
||||
|
||||
func seedLegacyVerifyCode(t *testing.T, redisClient *redis.Client, scene string, email string, code string) string {
|
||||
t.Helper()
|
||||
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, email)
|
||||
payload := map[string]interface{}{
|
||||
"code": code,
|
||||
"lastAt": time.Now().Unix(),
|
||||
}
|
||||
payloadRaw, err := json.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
err = redisClient.Set(context.Background(), cacheKey, payloadRaw, time.Minute*15).Err()
|
||||
require.NoError(t, err)
|
||||
return cacheKey
|
||||
}
|
||||
|
||||
func callLegacyCheckCode(t *testing.T, router *gin.Engine, apiHeader string, body string) legacyCheckCodeResponse {
|
||||
t.Helper()
|
||||
|
||||
reqBody := bytes.NewBufferString(body)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/auth/check-code", reqBody)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if apiHeader != "" {
|
||||
req.Header.Set("api-header", apiHeader)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, req)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var resp legacyCheckCodeResponse
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
return resp
|
||||
}
|
||||
|
||||
func TestCheckCodeLegacyHandler_NoHeaderNotConsumed(t *testing.T) {
|
||||
svcCtx, redisClient := newLegacyCheckCodeTestSvcCtx(t)
|
||||
router := newLegacyCheckCodeTestRouter(svcCtx)
|
||||
|
||||
email := "legacy@example.com"
|
||||
code := "123456"
|
||||
cacheKey := seedLegacyVerifyCode(t, redisClient, constant.Security.String(), email, code)
|
||||
|
||||
resp := callLegacyCheckCode(t, router, "", `{"email":"legacy@example.com","code":"123456","type":3}`)
|
||||
assert.Equal(t, uint32(200), resp.Code)
|
||||
assert.True(t, resp.Data.Status)
|
||||
assert.True(t, resp.Data.Exist)
|
||||
|
||||
exists, err := redisClient.Exists(context.Background(), cacheKey).Result()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), exists)
|
||||
}
|
||||
|
||||
func TestCheckCodeLegacyHandler_GreaterVersionConsumed(t *testing.T) {
|
||||
svcCtx, redisClient := newLegacyCheckCodeTestSvcCtx(t)
|
||||
router := newLegacyCheckCodeTestRouter(svcCtx)
|
||||
|
||||
email := "latest@example.com"
|
||||
code := "999888"
|
||||
cacheKey := seedLegacyVerifyCode(t, redisClient, constant.Security.String(), email, code)
|
||||
|
||||
resp := callLegacyCheckCode(t, router, "1.0.1", `{"email":"latest@example.com","code":"999888","type":3}`)
|
||||
assert.Equal(t, uint32(200), resp.Code)
|
||||
assert.True(t, resp.Data.Status)
|
||||
|
||||
exists, err := redisClient.Exists(context.Background(), cacheKey).Result()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(0), exists)
|
||||
|
||||
resp = callLegacyCheckCode(t, router, "1.0.1", `{"email":"latest@example.com","code":"999888","type":3}`)
|
||||
assert.Equal(t, uint32(200), resp.Code)
|
||||
assert.False(t, resp.Data.Status)
|
||||
assert.False(t, resp.Data.Exist)
|
||||
}
|
||||
|
||||
func TestCheckCodeLegacyHandler_EqualThresholdNotConsumed(t *testing.T) {
|
||||
svcCtx, redisClient := newLegacyCheckCodeTestSvcCtx(t)
|
||||
router := newLegacyCheckCodeTestRouter(svcCtx)
|
||||
|
||||
email := "equal@example.com"
|
||||
code := "112233"
|
||||
cacheKey := seedLegacyVerifyCode(t, redisClient, constant.Security.String(), email, code)
|
||||
|
||||
resp := callLegacyCheckCode(t, router, "1.0.0", `{"email":"equal@example.com","code":"112233","type":3}`)
|
||||
assert.Equal(t, uint32(200), resp.Code)
|
||||
assert.True(t, resp.Data.Status)
|
||||
|
||||
exists, err := redisClient.Exists(context.Background(), cacheKey).Result()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), exists)
|
||||
}
|
||||
|
||||
func TestCheckCodeLegacyHandler_InvalidVersionNotConsumed(t *testing.T) {
|
||||
svcCtx, redisClient := newLegacyCheckCodeTestSvcCtx(t)
|
||||
router := newLegacyCheckCodeTestRouter(svcCtx)
|
||||
|
||||
email := "invalid@example.com"
|
||||
code := "445566"
|
||||
cacheKey := seedLegacyVerifyCode(t, redisClient, constant.Security.String(), email, code)
|
||||
|
||||
resp := callLegacyCheckCode(t, router, "abc", `{"email":"invalid@example.com","code":"445566","type":3}`)
|
||||
assert.Equal(t, uint32(200), resp.Code)
|
||||
assert.True(t, resp.Data.Status)
|
||||
|
||||
exists, err := redisClient.Exists(context.Background(), cacheKey).Result()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), exists)
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/middleware"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/authmethod"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type canonicalCheckCodeResponse struct {
|
||||
Code uint32 `json:"code"`
|
||||
Data struct {
|
||||
Status bool `json:"status"`
|
||||
Exist bool `json:"exist"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func newCanonicalCheckCodeTestSvcCtx(t *testing.T) (*svc.ServiceContext, *redis.Client) {
|
||||
t.Helper()
|
||||
|
||||
miniRedis := miniredis.RunT(t)
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: miniRedis.Addr()})
|
||||
t.Cleanup(func() {
|
||||
redisClient.Close()
|
||||
miniRedis.Close()
|
||||
})
|
||||
|
||||
svcCtx := &svc.ServiceContext{
|
||||
Redis: redisClient,
|
||||
Config: config.Config{
|
||||
VerifyCode: config.VerifyCode{
|
||||
VerifyCodeExpireTime: 900,
|
||||
},
|
||||
},
|
||||
}
|
||||
return svcCtx, redisClient
|
||||
}
|
||||
|
||||
func newCanonicalCheckCodeTestRouter(svcCtx *svc.ServiceContext) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.Use(middleware.ApiVersionMiddleware(svcCtx))
|
||||
router.POST("/v1/common/check_verification_code", middleware.ApiVersionSwitchHandler(
|
||||
CheckVerificationCodeV1Handler(svcCtx),
|
||||
CheckVerificationCodeV2Handler(svcCtx),
|
||||
))
|
||||
return router
|
||||
}
|
||||
|
||||
func seedCanonicalVerifyCode(t *testing.T, redisClient *redis.Client, scene string, account string, code string) string {
|
||||
t.Helper()
|
||||
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, account)
|
||||
payload := map[string]interface{}{
|
||||
"code": code,
|
||||
"lastAt": time.Now().Unix(),
|
||||
}
|
||||
payloadRaw, err := json.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
err = redisClient.Set(context.Background(), cacheKey, payloadRaw, time.Minute*15).Err()
|
||||
require.NoError(t, err)
|
||||
return cacheKey
|
||||
}
|
||||
|
||||
func callCanonicalCheckCode(t *testing.T, router *gin.Engine, apiHeader string, body string) canonicalCheckCodeResponse {
|
||||
t.Helper()
|
||||
|
||||
reqBody := bytes.NewBufferString(body)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/common/check_verification_code", reqBody)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if apiHeader != "" {
|
||||
req.Header.Set("api-header", apiHeader)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, req)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var resp canonicalCheckCodeResponse
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
return resp
|
||||
}
|
||||
|
||||
func TestCheckVerificationCodeHandler_ApiHeaderGate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
apiHeader string
|
||||
expectConsume bool
|
||||
}{
|
||||
{name: "no header", apiHeader: "", expectConsume: false},
|
||||
{name: "invalid header", apiHeader: "invalid", expectConsume: false},
|
||||
{name: "equal threshold", apiHeader: "1.0.0", expectConsume: false},
|
||||
{name: "greater threshold", apiHeader: "1.0.1", expectConsume: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
svcCtx, redisClient := newCanonicalCheckCodeTestSvcCtx(t)
|
||||
router := newCanonicalCheckCodeTestRouter(svcCtx)
|
||||
|
||||
account := "header-gate@example.com"
|
||||
code := "123123"
|
||||
cacheKey := seedCanonicalVerifyCode(t, redisClient, constant.Register.String(), account, code)
|
||||
body := fmt.Sprintf(`{"method":"%s","account":"%s","code":"%s","type":%d}`,
|
||||
authmethod.Email,
|
||||
account,
|
||||
code,
|
||||
constant.Register,
|
||||
)
|
||||
|
||||
resp := callCanonicalCheckCode(t, router, tt.apiHeader, body)
|
||||
assert.Equal(t, uint32(200), resp.Code)
|
||||
assert.True(t, resp.Data.Status)
|
||||
|
||||
exists, err := redisClient.Exists(context.Background(), cacheKey).Result()
|
||||
require.NoError(t, err)
|
||||
if tt.expectConsume {
|
||||
assert.Equal(t, int64(0), exists)
|
||||
} else {
|
||||
assert.Equal(t, int64(1), exists)
|
||||
}
|
||||
|
||||
resp = callCanonicalCheckCode(t, router, tt.apiHeader, body)
|
||||
if tt.expectConsume {
|
||||
assert.False(t, resp.Data.Status)
|
||||
} else {
|
||||
assert.True(t, resp.Data.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type handlerResponse struct {
|
||||
Code uint32 `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
func newDeleteAccountTestRouter(serverCtx *svc.ServiceContext) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.POST("/v1/public/user/delete_account", DeleteAccountHandler(serverCtx))
|
||||
return router
|
||||
}
|
||||
|
||||
func TestDeleteAccountHandlerInvalidParamsUsesUnifiedResponse(t *testing.T) {
|
||||
router := newDeleteAccountTestRouter(&svc.ServiceContext{})
|
||||
|
||||
reqBody := bytes.NewBufferString(`{"email":"invalid-email"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/public/user/delete_account", reqBody)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected HTTP 200, got %d", recorder.Code)
|
||||
}
|
||||
|
||||
var resp handlerResponse
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if resp.Code != 400 {
|
||||
t.Fatalf("expected business code 400, got %d, body=%s", resp.Code, recorder.Body.String())
|
||||
}
|
||||
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &raw); err != nil {
|
||||
t.Fatalf("failed to decode raw response: %v", err)
|
||||
}
|
||||
if _, exists := raw["error"]; exists {
|
||||
t.Fatalf("unexpected raw error field in response: %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAccountHandlerVerifyCodeErrorUsesUnifiedResponse(t *testing.T) {
|
||||
redisClient := redis.NewClient(&redis.Options{
|
||||
Addr: "invalid:6379",
|
||||
Dialer: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return nil, errors.New("dial disabled in test")
|
||||
},
|
||||
})
|
||||
defer redisClient.Close()
|
||||
|
||||
serverCtx := &svc.ServiceContext{
|
||||
Redis: redisClient,
|
||||
Config: config.Config{
|
||||
VerifyCode: config.VerifyCode{
|
||||
VerifyCodeExpireTime: 900,
|
||||
},
|
||||
},
|
||||
}
|
||||
router := newDeleteAccountTestRouter(serverCtx)
|
||||
|
||||
reqBody := bytes.NewBufferString(`{"email":"user@example.com","code":"123456"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/public/user/delete_account", reqBody)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected HTTP 200, got %d", recorder.Code)
|
||||
}
|
||||
|
||||
var resp handlerResponse
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if resp.Code != 70001 {
|
||||
t.Fatalf("expected business code 70001, got %d, body=%s", resp.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmailCode_DeleteAccountSceneConsume(t *testing.T) {
|
||||
miniRedis := miniredis.RunT(t)
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: miniRedis.Addr()})
|
||||
t.Cleanup(func() {
|
||||
redisClient.Close()
|
||||
miniRedis.Close()
|
||||
})
|
||||
|
||||
serverCtx := &svc.ServiceContext{
|
||||
Redis: redisClient,
|
||||
Config: config.Config{
|
||||
VerifyCode: config.VerifyCode{VerifyCodeExpireTime: 900},
|
||||
},
|
||||
}
|
||||
|
||||
email := "delete-account@example.com"
|
||||
code := "112233"
|
||||
cacheKey := seedDeleteSceneCode(t, redisClient, constant.DeleteAccount.String(), email, code)
|
||||
|
||||
err := verifyEmailCode(context.Background(), serverCtx, email, code)
|
||||
if err != nil {
|
||||
t.Fatalf("verifyEmailCode returned unexpected error: %v", err)
|
||||
}
|
||||
|
||||
exists, err := redisClient.Exists(context.Background(), cacheKey).Result()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to check redis key: %v", err)
|
||||
}
|
||||
if exists != 0 {
|
||||
t.Fatalf("expected verification code to be consumed, key still exists")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmailCode_SecurityFallbackConsume(t *testing.T) {
|
||||
miniRedis := miniredis.RunT(t)
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: miniRedis.Addr()})
|
||||
t.Cleanup(func() {
|
||||
redisClient.Close()
|
||||
miniRedis.Close()
|
||||
})
|
||||
|
||||
serverCtx := &svc.ServiceContext{
|
||||
Redis: redisClient,
|
||||
Config: config.Config{
|
||||
VerifyCode: config.VerifyCode{VerifyCodeExpireTime: 900},
|
||||
},
|
||||
}
|
||||
|
||||
email := "security-fallback@example.com"
|
||||
code := "445566"
|
||||
cacheKey := seedDeleteSceneCode(t, redisClient, constant.Security.String(), email, code)
|
||||
|
||||
err := verifyEmailCode(context.Background(), serverCtx, email, code)
|
||||
if err != nil {
|
||||
t.Fatalf("verifyEmailCode fallback returned unexpected error: %v", err)
|
||||
}
|
||||
|
||||
exists, err := redisClient.Exists(context.Background(), cacheKey).Result()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to check redis key: %v", err)
|
||||
}
|
||||
if exists != 0 {
|
||||
t.Fatalf("expected fallback verification code to be consumed, key still exists")
|
||||
}
|
||||
}
|
||||
|
||||
func seedDeleteSceneCode(t *testing.T, redisClient *redis.Client, scene string, email string, code string) string {
|
||||
t.Helper()
|
||||
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, email)
|
||||
payload := map[string]interface{}{
|
||||
"code": code,
|
||||
"lastAt": time.Now().Unix(),
|
||||
}
|
||||
payloadRaw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal payload: %v", err)
|
||||
}
|
||||
err = redisClient.Set(context.Background(), cacheKey, payloadRaw, time.Minute*15).Err()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to seed redis payload: %v", err)
|
||||
}
|
||||
|
||||
return cacheKey
|
||||
}
|
||||
Reference in New Issue
Block a user