348 lines
12 KiB
Go
348 lines
12 KiB
Go
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"dubbo.apache.org/dubbo-go/v3/client"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/topfans/backend/pkg/logger"
|
|
pbAsset "github.com/topfans/backend/pkg/proto/asset"
|
|
pbCommon "github.com/topfans/backend/pkg/proto/common"
|
|
"go.uber.org/zap"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
)
|
|
|
|
// TestMain 初始化全局 logger 和 gin 测试模式
|
|
// 使用 zap.NewNop() 不输出任何日志,测试结果只靠断言判定
|
|
// 注意:本包内 laser_generate_controller_test.go 也有 TestMain,
|
|
// 为避免冲突,这里统一在此处声明(它将被 go test 收集为唯一的 TestMain)
|
|
func TestMain(m *testing.M) {
|
|
gin.SetMode(gin.TestMode)
|
|
logger.Logger = zap.NewNop()
|
|
logger.Sugar = logger.Logger.Sugar()
|
|
m.Run()
|
|
}
|
|
|
|
// ============================================================
|
|
// fakeAssetService —— 实现 pbAsset.AssetService 接口
|
|
// 只 stub 关注的 GetAssetQrcode / TrackShare 两个方法,
|
|
// 其余方法通过嵌入 nil 接口在调用时触发 panic(测试不会触发)
|
|
// ============================================================
|
|
type fakeAssetService struct {
|
|
pbAsset.AssetService // 嵌入 nil 接口,未 stub 的方法调用时 panic(预期不会发生)
|
|
|
|
// GetAssetQrcode 配置
|
|
qrcodeResp *pbAsset.GetAssetQrcodeResponse
|
|
qrcodeErr error
|
|
lastQrcodeReq *pbAsset.GetAssetQrcodeRequest
|
|
qrcodeCallCnt int
|
|
|
|
// TrackShare 配置
|
|
trackResp *pbAsset.TrackShareResponse
|
|
trackErr error
|
|
lastTrackReq *pbAsset.TrackShareRequest
|
|
trackCallCnt int
|
|
}
|
|
|
|
func newFakeAssetService() *fakeAssetService {
|
|
return &fakeAssetService{}
|
|
}
|
|
|
|
func (f *fakeAssetService) GetAssetQrcode(ctx context.Context, req *pbAsset.GetAssetQrcodeRequest, opts ...client.CallOption) (*pbAsset.GetAssetQrcodeResponse, error) {
|
|
f.qrcodeCallCnt++
|
|
f.lastQrcodeReq = req
|
|
if f.qrcodeErr != nil {
|
|
return nil, f.qrcodeErr
|
|
}
|
|
return f.qrcodeResp, nil
|
|
}
|
|
|
|
func (f *fakeAssetService) TrackShare(ctx context.Context, req *pbAsset.TrackShareRequest, opts ...client.CallOption) (*pbAsset.TrackShareResponse, error) {
|
|
f.trackCallCnt++
|
|
f.lastTrackReq = req
|
|
if f.trackErr != nil {
|
|
return nil, f.trackErr
|
|
}
|
|
return f.trackResp, nil
|
|
}
|
|
|
|
// ============================================================
|
|
// helper:构造一个带 user_id 的 gin.Context,模拟 JWT 中间件已写入
|
|
// ============================================================
|
|
func newAuthedContext(userID int64) (*gin.Context, *httptest.ResponseRecorder) {
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Set("user_id", userID)
|
|
return c, rec
|
|
}
|
|
|
|
// ============================================================
|
|
// TestGetAssetQrcode_Success
|
|
// mock AssetService,GET /share/asset-qrcode/{assetId}?sharer_user_id=42&system_type=ios
|
|
// user_id=42,期望 200 + JSON 含 qrcode_url + expires_at
|
|
// ============================================================
|
|
func TestGetAssetQrcode_Success(t *testing.T) {
|
|
fake := newFakeAssetService()
|
|
fake.qrcodeResp = &pbAsset.GetAssetQrcodeResponse{
|
|
Base: &pbCommon.BaseResponse{Code: 0, Message: "ok"},
|
|
QrcodeUrl: "https://cdn.example.com/qr/abc.png",
|
|
ExpiresAt: 1700000000,
|
|
}
|
|
ctrl := &AssetController{assetService: fake}
|
|
|
|
c, rec := newAuthedContext(42)
|
|
c.Request = httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/share/asset-qrcode/1001?sharer_user_id=42&system_type=ios&share_target=wechat", nil)
|
|
c.Params = gin.Params{{Key: "assetId", Value: "1001"}}
|
|
|
|
ctrl.GetAssetQrcode(c)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected HTTP 200, got %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var body struct {
|
|
Code uint32 `json:"code"`
|
|
Message string `json:"message"`
|
|
Data interface{} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("invalid JSON: %v, body=%s", err, rec.Body.String())
|
|
}
|
|
if body.Code != 0 {
|
|
t.Errorf("expected code=0, got %d (msg=%s)", body.Code, body.Message)
|
|
}
|
|
dataMap, ok := body.Data.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected data to be object, got %T", body.Data)
|
|
}
|
|
if dataMap["qrcode_url"] != "https://cdn.example.com/qr/abc.png" {
|
|
t.Errorf("qrcode_url mismatch: %v", dataMap["qrcode_url"])
|
|
}
|
|
// expires_at 序列化为 float64
|
|
if exp, ok := dataMap["expires_at"].(float64); !ok || int64(exp) != 1700000000 {
|
|
t.Errorf("expires_at mismatch: %v", dataMap["expires_at"])
|
|
}
|
|
// 断言 fake 收到正确参数
|
|
if fake.qrcodeCallCnt != 1 {
|
|
t.Errorf("expected AssetService.GetAssetQrcode to be called once, got %d", fake.qrcodeCallCnt)
|
|
}
|
|
if fake.lastQrcodeReq.GetAssetId() != 1001 {
|
|
t.Errorf("AssetId mismatch: got %d", fake.lastQrcodeReq.GetAssetId())
|
|
}
|
|
if fake.lastQrcodeReq.GetSharerUserId() != 42 {
|
|
t.Errorf("SharerUserId mismatch: got %d", fake.lastQrcodeReq.GetSharerUserId())
|
|
}
|
|
if fake.lastQrcodeReq.GetSystemType() != "ios" {
|
|
t.Errorf("SystemType mismatch: got %q", fake.lastQrcodeReq.GetSystemType())
|
|
}
|
|
if fake.lastQrcodeReq.GetShareTarget() != "wechat" {
|
|
t.Errorf("ShareTarget mismatch: got %q", fake.lastQrcodeReq.GetShareTarget())
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// TestGetAssetQrcode_SharerMismatch
|
|
// sharer_user_id=999 但 token user_id=42,期望 403
|
|
// ============================================================
|
|
func TestGetAssetQrcode_SharerMismatch(t *testing.T) {
|
|
fake := newFakeAssetService()
|
|
ctrl := &AssetController{assetService: fake}
|
|
|
|
c, rec := newAuthedContext(42)
|
|
c.Request = httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/share/asset-qrcode/1001?sharer_user_id=999&system_type=ios", nil)
|
|
c.Params = gin.Params{{Key: "assetId", Value: "1001"}}
|
|
|
|
ctrl.GetAssetQrcode(c)
|
|
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected HTTP 403, got %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
// 鉴权失败 → 不能调下游 service
|
|
if fake.qrcodeCallCnt != 0 {
|
|
t.Errorf("expected AssetService NOT called on auth failure, got %d calls", fake.qrcodeCallCnt)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// TestGetAssetQrcode_MissingSystemType
|
|
// 不带 system_type 查询参数,期望 400
|
|
// ============================================================
|
|
func TestGetAssetQrcode_MissingSystemType(t *testing.T) {
|
|
fake := newFakeAssetService()
|
|
ctrl := &AssetController{assetService: fake}
|
|
|
|
c, rec := newAuthedContext(42)
|
|
c.Request = httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/share/asset-qrcode/1001?sharer_user_id=42", nil)
|
|
c.Params = gin.Params{{Key: "assetId", Value: "1001"}}
|
|
|
|
ctrl.GetAssetQrcode(c)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected HTTP 400, got %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if fake.qrcodeCallCnt != 0 {
|
|
t.Errorf("expected AssetService NOT called, got %d calls", fake.qrcodeCallCnt)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// TestGetAssetQrcode_InvalidAssetId
|
|
// assetId=abc(非数字),期望 400
|
|
// ============================================================
|
|
func TestGetAssetQrcode_InvalidAssetId(t *testing.T) {
|
|
fake := newFakeAssetService()
|
|
ctrl := &AssetController{assetService: fake}
|
|
|
|
c, rec := newAuthedContext(42)
|
|
c.Request = httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/share/asset-qrcode/abc?sharer_user_id=42&system_type=ios", nil)
|
|
c.Params = gin.Params{{Key: "assetId", Value: "abc"}}
|
|
|
|
ctrl.GetAssetQrcode(c)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected HTTP 400, got %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if fake.qrcodeCallCnt != 0 {
|
|
t.Errorf("expected AssetService NOT called, got %d calls", fake.qrcodeCallCnt)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// TestTrackShare_Success
|
|
// POST /share/track,token user_id=42 == sharer_user_id=42,期望 200 + share_event_id
|
|
// ============================================================
|
|
func TestTrackShare_Success(t *testing.T) {
|
|
fake := newFakeAssetService()
|
|
fake.trackResp = &pbAsset.TrackShareResponse{
|
|
Base: &pbCommon.BaseResponse{Code: 0, Message: "ok"},
|
|
ShareEventId: 8888,
|
|
}
|
|
ctrl := &AssetController{assetService: fake}
|
|
|
|
bodyJSON := `{
|
|
"asset_id": 1001,
|
|
"sharer_user_id": 42,
|
|
"system_type": "ios",
|
|
"share_target": "wechat",
|
|
"result": "success",
|
|
"client_ts": 1700000000
|
|
}`
|
|
|
|
c, rec := newAuthedContext(42)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/share/track", bytes.NewBufferString(bodyJSON))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
ctrl.TrackShare(c)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected HTTP 200, got %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var body struct {
|
|
Code uint32 `json:"code"`
|
|
Message string `json:"message"`
|
|
Data interface{} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("invalid JSON: %v, body=%s", err, rec.Body.String())
|
|
}
|
|
if body.Code != 0 {
|
|
t.Errorf("expected code=0, got %d (msg=%s)", body.Code, body.Message)
|
|
}
|
|
dataMap, ok := body.Data.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected data to be object, got %T", body.Data)
|
|
}
|
|
// share_event_id 序列化为 float64
|
|
if sid, ok := dataMap["share_event_id"].(float64); !ok || int64(sid) != 8888 {
|
|
t.Errorf("share_event_id mismatch: %v", dataMap["share_event_id"])
|
|
}
|
|
// 断言 fake 收到正确参数
|
|
if fake.trackCallCnt != 1 {
|
|
t.Errorf("expected AssetService.TrackShare to be called once, got %d", fake.trackCallCnt)
|
|
}
|
|
if fake.lastTrackReq.GetAssetId() != 1001 {
|
|
t.Errorf("AssetId mismatch: got %d", fake.lastTrackReq.GetAssetId())
|
|
}
|
|
if fake.lastTrackReq.GetSharerUserId() != 42 {
|
|
t.Errorf("SharerUserId mismatch: got %d", fake.lastTrackReq.GetSharerUserId())
|
|
}
|
|
if fake.lastTrackReq.GetSystemType() != "ios" {
|
|
t.Errorf("SystemType mismatch: got %q", fake.lastTrackReq.GetSystemType())
|
|
}
|
|
if fake.lastTrackReq.GetShareTarget() != "wechat" {
|
|
t.Errorf("ShareTarget mismatch: got %q", fake.lastTrackReq.GetShareTarget())
|
|
}
|
|
if fake.lastTrackReq.GetResult() != "success" {
|
|
t.Errorf("Result mismatch: got %q", fake.lastTrackReq.GetResult())
|
|
}
|
|
if fake.lastTrackReq.GetClientTs() != 1700000000 {
|
|
t.Errorf("ClientTs mismatch: got %d", fake.lastTrackReq.GetClientTs())
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// TestTrackShare_SharerMismatch
|
|
// POST sharer_user_id=999 但 token user_id=42,期望 403
|
|
// ============================================================
|
|
func TestTrackShare_SharerMismatch(t *testing.T) {
|
|
fake := newFakeAssetService()
|
|
ctrl := &AssetController{assetService: fake}
|
|
|
|
bodyJSON := `{
|
|
"asset_id": 1001,
|
|
"sharer_user_id": 999,
|
|
"system_type": "ios",
|
|
"share_target": "wechat",
|
|
"result": "success",
|
|
"client_ts": 1700000000
|
|
}`
|
|
|
|
c, rec := newAuthedContext(42)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/share/track", bytes.NewBufferString(bodyJSON))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
ctrl.TrackShare(c)
|
|
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected HTTP 403, got %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if fake.trackCallCnt != 0 {
|
|
t.Errorf("expected AssetService NOT called on auth failure, got %d calls", fake.trackCallCnt)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// TestTrackShare_InvalidJSON
|
|
// POST 带畸形 body,期望 400
|
|
// ============================================================
|
|
func TestTrackShare_InvalidJSON(t *testing.T) {
|
|
fake := newFakeAssetService()
|
|
ctrl := &AssetController{assetService: fake}
|
|
|
|
// 故意给一个不闭合的 JSON,触发 binding error
|
|
bodyJSON := `{"asset_id": 1001, "sharer_user_id": 42,`
|
|
|
|
c, rec := newAuthedContext(42)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/share/track", bytes.NewBufferString(bodyJSON))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
ctrl.TrackShare(c)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected HTTP 400, got %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if fake.trackCallCnt != 0 {
|
|
t.Errorf("expected AssetService NOT called on bad JSON, got %d calls", fake.trackCallCnt)
|
|
}
|
|
}
|
|
|
|
// 静默引用 structpb,避免 goimports 删除 unused import 警告
|
|
var _ = structpb.NewStruct |