package main
import (
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
"cmautobuy/admin/service"
)
func TestPaginationCSS_桌面右对齐且窄窗口不遮挡内容(t *testing.T) {
content, err := os.ReadFile("static/css/app.css")
if err != nil {
t.Fatal(err)
}
css := string(content)
statusbarRightAligned := `.statusbar {
position: fixed;
left: 0; right: 0; bottom: 0;
display: flex;
align-items: center;
justify-content: flex-end;`
for _, want := range []string{
statusbarRightAligned, "flex: 0 1 auto", "text-align: right",
"margin-left: 0", "flex: 0 0 auto", "@media (max-width: 760px)",
"position: static", ".statusbar > span:first-child { flex-basis: 100%; }",
} {
if !strings.Contains(css, want) {
t.Fatalf("分页布局 CSS 缺少 %q", want)
}
}
}
func TestPurchaseModal_未选规格行保持隐藏且不提交(t *testing.T) {
files := map[string][]string{
"static/css/app.css": {
".purchase-confirm-row[hidden] { display: none; }",
},
"templates/syb/list.html": {
`class="purchase-confirm-row" data-purchase-row="{{.SybID}}" hidden`,
`name="mapping_option_key" value="{{.MappingOptionKey}}" disabled`,
`name="context_version" value="{{.ContextVersion}}" disabled`,
`name="max_price_yuan" min="0.01" step="0.01" value="{{.DefaultMaxPrice}}" required disabled`,
},
"static/js/app.js": {
"row.hidden = !enabled;",
"input.disabled = !enabled;",
},
}
for path, wants := range files {
content, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
for _, want := range wants {
if !strings.Contains(string(content), want) {
t.Errorf("%s 缺少采购弹窗回归约束 %q", path, want)
}
}
}
}
func TestPddDetailRecollect_使用独立入口并可选客户端(t *testing.T) {
content, err := os.ReadFile("templates/pdd/edit_modal.html")
if err != nil {
t.Fatal(err)
}
page := string(content)
for _, want := range []string{
`action="/pdd/recollect"`, `name="goods_id"`, `name="client_id"`,
"不指定(任意客户端可领取)", "成功后再用新结果覆盖",
} {
if !strings.Contains(page, want) {
t.Errorf("PDD 详情重新采集缺少 %q", want)
}
}
if strings.Contains(page, `id="pdd-recollect-form" method="post" action="/pdd/collect"`) {
t.Error("重新采集不能继续复用会跳过已采集商品的普通批量入口")
}
}
func TestTaskDetail_采购核单结果使用独立只读分组(t *testing.T) {
content, err := os.ReadFile("templates/task/detail_modal.html")
if err != nil {
t.Fatal(err)
}
page := string(content)
for _, want := range []string{
"PDD 核单结果", "PDD 订单编号", "下单时间", "上报时付款状态",
"{{if .IsPurchase}}", "{{if .HasPddOrder}}", "完整结果(result_data)",
} {
if !strings.Contains(page, want) {
t.Errorf("采购任务详情缺少 %q", want)
}
}
}
func TestTaskPage_创建人筛选列表列和详情字段齐全(t *testing.T) {
files := map[string][]string{
"templates/task/list.html": {
`{{if .ShowCreator}}`, `name="creator"`, `{{.CreatorText}}`, `>筛选`,
},
"templates/task/detail_modal.html": {`
创建人{{.CreatorText}}`},
}
for path, wants := range files {
content, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
for _, want := range wants {
if !strings.Contains(string(content), want) {
t.Errorf("%s 缺少任务创建人界面约束 %q", path, want)
}
}
}
}
func TestShopeePage_移除Excel入口且保留目录记录入口(t *testing.T) {
content, err := os.ReadFile("templates/shopee/list.html")
if err != nil {
t.Fatal(err)
}
page := string(content)
for _, removed := range []string{`action="/shopee/import"`, `type="file"`, "导入 Excel"} {
if strings.Contains(page, removed) {
t.Errorf("蝦皮页面仍包含已移除入口 %q", removed)
}
}
for _, want := range []string{`href="/integrations/catalog"`, "商品目录接口"} {
if !strings.Contains(page, want) {
t.Errorf("蝦皮页面缺少 %q", want)
}
}
}
func TestShopeeDetail_区分顺运宝观测与正式SKU(t *testing.T) {
content, err := os.ReadFile("templates/shopee/detail_modal.html")
if err != nil {
t.Fatal(err)
}
page := string(content)
for _, want := range []string{"顺运宝观测规格", "不是正式蝦皮 SKU", "最近货运单售价(台币)", "{{.MatchText}}"} {
if !strings.Contains(page, want) {
t.Errorf("蝦皮详情缺少 %q", want)
}
}
}
func TestCatalogHistory_展示更新策略和分级统计(t *testing.T) {
list, err := os.ReadFile("templates/catalog/list.html")
if err != nil {
t.Fatal(err)
}
detail, err := os.ReadFile("templates/catalog/detail_modal.html")
if err != nil {
t.Fatal(err)
}
content := string(list) + string(detail)
for _, want := range []string{"更新策略", "SKUFilled", "SKUSameSourceUpdated", "SKUManualSkipped", "SKUStaleSkipped"} {
if !strings.Contains(content, want) {
t.Errorf("目录记录缺少 %q", want)
}
}
}
func TestShopeeImportRoute_已移除(t *testing.T) {
router, err := newRouter(nil)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPost, "/shopee/import", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusNotFound {
t.Fatalf("旧蝦皮导入路由应为 404,实际 %d", resp.Code)
}
}
func TestTemplatesAndRoutesCanBeBuiltWithoutDatabaseConnection(t *testing.T) {
if _, err := newRouter(nil); err != nil {
t.Fatalf("模板或路由组装失败: %v", err)
}
}
// 六个主页面都走一次真实路由和模板渲染。
// 这样模板字段写错或新增列漏接时,测试阶段就会失败,不必等人工点页面。
func TestMainPagesReturnOK(t *testing.T) {
db := newMySQLTestDB(t)
router, err := newRouter(db)
if err != nil {
t.Fatalf("组装路由失败: %v", err)
}
// 全新库的业务页必须先去初始化,不能匿名打开。
unauthenticated := httptest.NewRequest(http.MethodGet, "/shopee", nil)
unauthenticatedResponse := httptest.NewRecorder()
router.ServeHTTP(unauthenticatedResponse, unauthenticated)
if unauthenticatedResponse.Code != http.StatusSeeOther ||
unauthenticatedResponse.Header().Get("Location") != "/setup" {
t.Fatalf("全新库访问业务页应跳转 /setup,实际 %d %s",
unauthenticatedResponse.Code, unauthenticatedResponse.Header().Get("Location"))
}
if err := service.SetupInitialAdmin(db, "admin", "test-password", "test-password", time.Now()); err != nil {
t.Fatalf("准备测试管理员失败: %v", err)
}
token, adminUser, _, err := service.Login(db, "admin", "test-password", time.Now())
if err != nil {
t.Fatalf("准备测试登录 Session 失败: %v", err)
}
addAuth := func(request *http.Request) {
request.AddCookie(&http.Cookie{Name: "cmautobuy_session", Value: token})
}
for _, path := range []string{"/shopee", "/pdd", "/syb", "/tasks", "/clients", "/users", "/integrations/catalog"} {
t.Run(path, func(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, path, nil)
addAuth(request)
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("GET %s = %d,期望 200,响应:%s",
path, response.Code, response.Body.String())
}
body := response.Body.String()
if !strings.Contains(body, `