Files
jiu/backend/internal/handler/public_page_test.go
T

157 lines
5.2 KiB
Go

package handler
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"gorm.io/gorm"
"github.com/wangjia/jiu/backend/testutil"
)
// SSR 公开页(2026-07-07 去 Flutter 化):商品页 /product/:id 与店铺页 /shop/:code
// 由后端直出轻量 HTML;断言内容、OG 标签、敏感字段零暴露、搜索与分页。
func setupPageRouter(db *gorm.DB) *gin.Engine {
h := NewPublicHandler(db)
r := gin.New()
r.Use(gin.Recovery())
r.GET("/product/:public_id", h.ProductPage)
r.GET("/shop/:shop_code", h.ShopPage)
return r
}
func getPage(r *gin.Engine, path string) *httptest.ResponseRecorder {
req := httptest.NewRequest("GET", path, nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
func TestProductPage_SSR(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "PPG01")
wh := testutil.CreateTestWarehouse(db, shop.ID, "仓")
r := setupPageRouter(db)
p := testutil.CreateTestProduct(db, shop.ID, "茅台飞天53度")
setPublicID(db, p.ID, "pub-ssr-001")
addInventory(db, shop.ID, wh.ID, p.ID, 6)
w := getPage(r, "/product/pub-ssr-001")
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Header().Get("Content-Type"), "text/html")
assert.Equal(t, "no-store", w.Header().Get("Cache-Control"))
body := w.Body.String()
// 轻量页:不再是 Flutter 壳
assert.NotContains(t, body, "flutter_bootstrap")
assert.NotContains(t, body, `<base href="/app/">`)
// 内容
assert.Contains(t, body, "茅台飞天53度")
assert.Contains(t, body, shop.Name)
// OG 标签(og:url 指向 SSR 短链)
assert.Contains(t, body, `property="og:title"`)
assert.Contains(t, body, "/product/pub-ssr-001")
// 门店互跳;「在 App 中查看」已移除(2026-07-07 用户拍板)
assert.Contains(t, body, "/shop/"+shop.Code)
assert.NotContains(t, body, "/app/product/")
// 敏感字段零暴露(成本/进价永不出现在公开页)
for _, s := range []string{"cost", "purchase_price", "profit"} {
assert.NotContains(t, body, s, "公开页不得出现敏感字段名 %s", s)
}
}
func TestProductPage_NotFound(t *testing.T) {
db := testutil.SetupTestDB()
r := setupPageRouter(db)
w := getPage(r, "/product/no-such-id")
// 历史行为保持:HTTP 200 + 友好文案(对爬虫不报错)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "商品不存在")
}
func TestShopPage_SSR_SearchAndPaging(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SPG01")
wh := testutil.CreateTestWarehouse(db, shop.ID, "仓")
r := setupPageRouter(db)
// 25 个有库存商品 → 2 页;外加一个名称可搜索的
for i := 0; i < 25; i++ {
p := testutil.CreateTestProduct(db, shop.ID, "批量酒"+strings.Repeat("x", i%3))
setPublicID(db, p.ID, "sp-"+strings.Repeat("a", 1)+string(rune('A'+i%26))+strings.Repeat("b", i/26+1))
addInventory(db, shop.ID, wh.ID, p.ID, 3)
}
target := testutil.CreateTestProduct(db, shop.ID, "五粮液普五")
setPublicID(db, target.ID, "sp-target")
addInventory(db, shop.ID, wh.ID, target.ID, 2)
// 第 1 页:20 条 + 下一页链接
w := getPage(r, "/shop/"+shop.Code)
assert.Equal(t, http.StatusOK, w.Code)
body := w.Body.String()
assert.Contains(t, body, shop.Name)
assert.Contains(t, body, "26 件")
assert.Contains(t, body, "page=2")
assert.Equal(t, 20, strings.Count(body, `class="item"`), "第一页应有 20 张商品卡")
assert.NotContains(t, body, "flutter_bootstrap")
// 第 2 页:6 条 + 上一页链接
w = getPage(r, "/shop/"+shop.Code+"?page=2")
body = w.Body.String()
assert.Equal(t, 6, strings.Count(body, `class="item"`))
assert.Contains(t, body, "上一页")
// 搜索:只命中目标商品,商品卡链接到 SSR 商品页
w = getPage(r, "/shop/"+shop.Code+"?q="+urlQueryEscape("五粮液"))
body = w.Body.String()
assert.Equal(t, 1, strings.Count(body, `class="item"`))
assert.Contains(t, body, "/product/sp-target")
// 搜索无结果:空态文案
w = getPage(r, "/shop/"+shop.Code+"?q=NOPE")
assert.Contains(t, w.Body.String(), "没有找到")
}
func TestShopPage_NotFound(t *testing.T) {
db := testutil.SetupTestDB()
r := setupPageRouter(db)
w := getPage(r, "/shop/NOSHOP")
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "店铺不存在")
}
// API 与 SSR 共用装配后的回归:ListShopProducts 行为不变(不受 keyword 影响)
func TestListShopProducts_UnaffectedByRefactor(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SPG02")
wh := testutil.CreateTestWarehouse(db, shop.ID, "仓")
p := testutil.CreateTestProduct(db, shop.ID, "剑南春")
setPublicID(db, p.ID, "sp2-x")
addInventory(db, shop.ID, wh.ID, p.ID, 1)
r := setupPublicRouter(db)
w := getPage(r, "/api/v1/public/shops/"+shop.Code+"/products")
assert.Equal(t, http.StatusOK, w.Code)
resp := parseResponse(w)
assert.Equal(t, float64(1), resp["total"].(float64))
}
func urlQueryEscape(s string) string {
// 测试用最小转义(仅中文场景)
var b strings.Builder
for _, c := range []byte(s) {
b.WriteString("%")
const hex = "0123456789ABCDEF"
b.WriteByte(hex[c>>4])
b.WriteByte(hex[c&0xF])
}
return b.String()
}