package controller import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "dubbo.apache.org/dubbo-go/v3/client" "github.com/gin-gonic/gin" pbAsset "github.com/topfans/backend/pkg/proto/asset" pbCommon "github.com/topfans/backend/pkg/proto/common" ) // ============================================================ // fakeStarbookClient — 实现 pbAsset.AssetService // 只 stub 关注的 GetMyAssets / GetAssetsByType 两个方法, // 其余方法通过嵌入 nil 接口在调用时触发 panic(测试不会触发) // ============================================================ type fakeStarbookClient struct { pbAsset.AssetService // 嵌入 nil 接口 // GetMyAssets 配置 homeResp *pbAsset.GetMyAssetsResponse homeErr error lastHomeReq *pbAsset.GetMyAssetsRequest homeCallCnt int // GetAssetsByType 配置 itemsResp *pbAsset.GetAssetsByTypeResponse itemsErr error lastItemsReq *pbAsset.GetAssetsByTypeRequest itemsCallCnt int } func newFakeStarbookClient() *fakeStarbookClient { return &fakeStarbookClient{} } func (f *fakeStarbookClient) GetMyAssets(ctx context.Context, req *pbAsset.GetMyAssetsRequest, opts ...client.CallOption) (*pbAsset.GetMyAssetsResponse, error) { f.homeCallCnt++ f.lastHomeReq = req if f.homeErr != nil { return nil, f.homeErr } return f.homeResp, nil } func (f *fakeStarbookClient) GetAssetsByType(ctx context.Context, req *pbAsset.GetAssetsByTypeRequest, opts ...client.CallOption) (*pbAsset.GetAssetsByTypeResponse, error) { f.itemsCallCnt++ f.lastItemsReq = req if f.itemsErr != nil { return nil, f.itemsErr } return f.itemsResp, nil } // newAuthedStarbookContext 构造带 user_id + star_id 的 gin.Context(模拟 JWT 中间件已写入) func newAuthedStarbookContext(userID, starID int64, query string) (*gin.Context, *httptest.ResponseRecorder) { rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Set("user_id", userID) c.Set("star_id", starID) c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/starbook/home"+query, nil) return c, rec } // ============================================================ // TestStarbookHome_Success // mock client,GET /starbook/home // 期望:HTTP 200 + JSON 含 data.groups(老 starbook controller 的 HomeItems 即 AssetGroup 列表) // 期望:fake 收到 GetMyAssetsRequest(空参数 = 全部藏品) // ============================================================ func TestStarbookHome_Success(t *testing.T) { fake := newFakeStarbookClient() fake.homeResp = &pbAsset.GetMyAssetsResponse{ Base: &pbCommon.BaseResponse{Code: 0, Message: "ok"}, Data: &pbAsset.AssetListData{ Groups: []*pbAsset.AssetGroup{ {Type: "regular", Category: "castlove", CategoryName: "普通藏品", TotalCount: 3}, {Type: "collection", Category: "love_card", CategoryName: "套装", TotalCount: 1}, }, Total: 4, Page: 1, PageSize: 20, HasMore: false, }, } ctrl := &StarbookController{assetClient: fake} c, rec := newAuthedStarbookContext(42, 7, "") ctrl.GetStarbookHome(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) } // 嵌套结构:Response.Data → proto resp.Data(AssetListData) → groups // JSON 形如 {"code":0,"message":"ok","data":{"base":{...},"data":{"groups":[...],"total":4,...}}} innerData, ok := dataMap["data"].(map[string]interface{}) if !ok { t.Fatalf("expected inner data.data to be object, got %T", dataMap["data"]) } // groups 即老 starbook controller 的 HomeItems(同 JSON 字段名) groups, ok := innerData["groups"].([]interface{}) if !ok || len(groups) != 2 { t.Fatalf("expected groups array len 2, got %v", innerData["groups"]) } if innerData["total"].(float64) != 4 { t.Errorf("expected total=4, got %v", innerData["total"]) } // 断言 fake 收到 GetMyAssets 调用 if fake.homeCallCnt != 1 { t.Errorf("expected GetMyAssets called once, got %d", fake.homeCallCnt) } } // ============================================================ // TestStarbookHome_Unauthorized // 没有 user_id(star_id 还在),期望 401,不调下游 // ============================================================ func TestStarbookHome_Unauthorized(t *testing.T) { fake := newFakeStarbookClient() ctrl := &StarbookController{assetClient: fake} rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) // 故意不 set user_id c.Set("star_id", int64(7)) c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/starbook/home", nil) ctrl.GetStarbookHome(c) if rec.Code != http.StatusUnauthorized { t.Fatalf("expected HTTP 401, got %d, body=%s", rec.Code, rec.Body.String()) } if fake.homeCallCnt != 0 { t.Errorf("expected GetMyAssets NOT called, got %d calls", fake.homeCallCnt) } } // ============================================================ // TestStarbookHome_RPCError // fake 返回 error,期望 500,响应中含错误信息 // ============================================================ func TestStarbookHome_RPCError(t *testing.T) { fake := newFakeStarbookClient() fake.homeErr = errString("assetService unreachable") ctrl := &StarbookController{assetClient: fake} c, rec := newAuthedStarbookContext(42, 7, "") ctrl.GetStarbookHome(c) if rec.Code != http.StatusInternalServerError { t.Fatalf("expected HTTP 500, got %d, body=%s", rec.Code, rec.Body.String()) } if fake.homeCallCnt != 1 { t.Errorf("expected GetMyAssets called once, got %d", fake.homeCallCnt) } } // ============================================================ // TestStarbookItems_Success // mock client,GET /starbook/items?type=regular&category=castlove&grade=2&page=1&page_size=20 // 期望:HTTP 200 + data.groups + 断言 fake 收到的 type/category/grade/page/pageSize // ============================================================ func TestStarbookItems_Success(t *testing.T) { fake := newFakeStarbookClient() fake.itemsResp = &pbAsset.GetAssetsByTypeResponse{ Base: &pbCommon.BaseResponse{Code: 0, Message: "ok"}, Data: &pbAsset.AssetListData{ Groups: []*pbAsset.AssetGroup{ {Type: "regular", Category: "castlove", CategoryName: "铸爱", TotalCount: 5}, }, Total: 5, Page: 1, PageSize: 20, HasMore: false, }, } ctrl := &StarbookController{assetClient: fake} c, rec := newAuthedStarbookContext(42, 7, "?type=regular&category=castlove&grade=2&page=1&page_size=20") ctrl.GetStarbookItems(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) } innerData, ok := dataMap["data"].(map[string]interface{}) if !ok { t.Fatalf("expected inner data.data to be object, got %T", dataMap["data"]) } groups, ok := innerData["groups"].([]interface{}) if !ok || len(groups) != 1 { t.Fatalf("expected groups array len 1, got %v", innerData["groups"]) } // 断言 fake 收到正确的 RPC 参数 if fake.itemsCallCnt != 1 { t.Errorf("expected GetAssetsByType called once, got %d", fake.itemsCallCnt) } if fake.lastItemsReq.GetType() != "regular" { t.Errorf("Type mismatch: got %q", fake.lastItemsReq.GetType()) } if fake.lastItemsReq.GetCategory() != "castlove" { t.Errorf("Category mismatch: got %q", fake.lastItemsReq.GetCategory()) } if fake.lastItemsReq.GetGrade() != 2 { t.Errorf("Grade mismatch: got %d", fake.lastItemsReq.GetGrade()) } if fake.lastItemsReq.GetPage() != 1 { t.Errorf("Page mismatch: got %d", fake.lastItemsReq.GetPage()) } if fake.lastItemsReq.GetPageSize() != 20 { t.Errorf("PageSize mismatch: got %d", fake.lastItemsReq.GetPageSize()) } } // ============================================================ // TestStarbookItems_MissingType // 不带 type 参数,期望 400,不调下游 // ============================================================ func TestStarbookItems_MissingType(t *testing.T) { fake := newFakeStarbookClient() ctrl := &StarbookController{assetClient: fake} c, rec := newAuthedStarbookContext(42, 7, "") ctrl.GetStarbookItems(c) if rec.Code != http.StatusBadRequest { t.Fatalf("expected HTTP 400, got %d, body=%s", rec.Code, rec.Body.String()) } if fake.itemsCallCnt != 0 { t.Errorf("expected GetAssetsByType NOT called, got %d calls", fake.itemsCallCnt) } } // ============================================================ // errString 简单错误包装(避免引入 errors 包造成额外依赖) // ============================================================ type errString string func (e errString) Error() string { return string(e) }