topfans/backend/gateway/controller/asset_controller_test.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