package app import ( "context" "errors" "strings" "testing" "time" "github.com/alicebob/miniredis/v2" goredis "github.com/redis/go-redis/v9" "github.com/geo-platform/tenant-api/internal/shared/config" ) type mediaSupplyOrderScanStub struct { values []any } func (s mediaSupplyOrderScanStub) Scan(dest ...any) error { for i, value := range s.values { switch target := dest[i].(type) { case *int: *target = value.(int) case *int64: *target = value.(int64) case *string: *target = value.(string) case **string: *target = value.(*string) case **int64: *target = value.(*int64) case *time.Time: *target = value.(time.Time) case **time.Time: *target = value.(*time.Time) default: panic("unsupported scan target") } } return nil } func TestMediaSupplySellPriceNeverBelowCost(t *testing.T) { got := mediaSupplySellPrice(1000, 900, true, 0, 0) if got != 1000 { t.Fatalf("expected cost floor 1000, got %d", got) } got = mediaSupplySellPrice(1000, 1100, true, 20, 0) if got != 1100 { t.Fatalf("expected explicit override above cost 1100, got %d", got) } got = mediaSupplySellPrice(1000, 1400, true, 20, 0) if got != 1400 { t.Fatalf("expected override above floor 1400, got %d", got) } } func TestMediaSupplySellPricePrefersManualOverride(t *testing.T) { got := mediaSupplySellPrice(1000, 1100, true, 50, 0) if got != 1100 { t.Fatalf("expected manual override 1100 to win over 50%% markup floor, got %d", got) } got = mediaSupplySellPrice(1000, 1101, true, 50, 0) if got != 1200 { t.Fatalf("expected manual override 1101 to round up to 1200, got %d", got) } } func TestMediaSupplySellPriceRoundsUpToWholeYuan(t *testing.T) { got := mediaSupplySellPrice(8040, 0, false, 0, 0) if got != 8100 { t.Fatalf("expected 8040 cents to round up to 8100, got %d", got) } got = mediaSupplySellPrice(7001, 0, false, 0, 0) if got != 7100 { t.Fatalf("expected 7001 cents to round up to 7100, got %d", got) } got = mediaSupplySellPrice(7000, 0, false, 0, 0) if got != 7000 { t.Fatalf("expected whole yuan price to stay 7000, got %d", got) } } func TestMediaSupplySellPriceAppliesMarkupBeforeRoundUp(t *testing.T) { got := mediaSupplySellPrice(300, 0, false, 50, 0) if got != 500 { t.Fatalf("expected 300 cents with 50%% markup to round up to 500, got %d", got) } got = mediaSupplySellPrice(800, 0, false, 50, 0) if got != 1200 { t.Fatalf("expected 800 cents with 50%% markup to be 1200, got %d", got) } got = mediaSupplySellPrice(8040, 0, false, 50, 0) if got != 12100 { t.Fatalf("expected 8040 cents with 50%% markup to round up to 12100, got %d", got) } } func TestMediaSupplyResourceOrderSQL(t *testing.T) { priceSQL := "sell_price_expr" if got := mediaSupplyResourceOrderSQL("price", priceSQL); got != "(sell_price_expr) ASC, r.id DESC" { t.Fatalf("unexpected price order SQL: %s", got) } if got := mediaSupplyResourceOrderSQL("weight", priceSQL); got != "COALESCE(r.baidu_weight, 0) DESC, r.id DESC" { t.Fatalf("unexpected weight order SQL: %s", got) } if got := mediaSupplyResourceOrderSQL("unknown", priceSQL); got != "r.updated_at DESC, r.id DESC" { t.Fatalf("unexpected default order SQL: %s", got) } } func TestFormatMeijiequanResourceID(t *testing.T) { item := MediaSupplyOrderItem{SupplierResourceID: "36112", PriceType: "price"} if got := formatMeijiequanResourceID(item); got != "36112_price_0" { t.Fatalf("unexpected default price resource id: %s", got) } item.PriceType = "priceyc" if got := formatMeijiequanResourceID(item); got != "36112_priceyc_0" { t.Fatalf("unexpected variant resource id: %s", got) } } func TestRetryableMediaSupplyErrorTreatsBusinessFailuresAsTerminal(t *testing.T) { terminalErrors := []error{ errMeijiequanSessionMissing, errMeijiequanSessionExpired, errMeijiequanAuthRequired, errMeijiequanOrderRejected, errMediaSupplySelectedResourcesMissing, errMediaSupplyLockedSellBelowCost, } for _, err := range terminalErrors { if retryableMediaSupplyError(err) { t.Fatalf("expected terminal media supply error to be non-retryable: %v", err) } } if !retryableMediaSupplyError(errors.New("temporary network failure")) { t.Fatal("expected generic temporary error to remain retryable") } } func TestScanMediaSupplyOrderRedactsPublicFields(t *testing.T) { now := time.Now().UTC() errorMessage := "failed to refresh supplier price for 1 selected resources" item, err := scanMediaSupplyOrder(mediaSupplyOrderScanStub{values: []any{ int64(1), int64(2), int64(3), int64(4), int64(5), (*int64)(nil), mediaSupplySupplierMeijiequan, 1, mediaSupplyOrderStatusFailed, "测试标题", (*string)(nil), (*string)(nil), (*string)(nil), (*string)(nil), (*string)(nil), int64(100), int64(120), int64(120), (*time.Time)(nil), (*time.Time)(nil), &errorMessage, 1, now, (*time.Time)(nil), (*time.Time)(nil), now, now, }}) if err != nil { t.Fatalf("scan failed: %v", err) } if strings.Contains(strings.ToLower(item.Supplier), "meijiequan") { t.Fatalf("supplier was not redacted: %s", item.Supplier) } if item.ErrorMessage == nil || *item.ErrorMessage != "媒体资源价格刷新失败,请稍后重试或重新选择媒体" { t.Fatalf("unexpected public error message: %#v", item.ErrorMessage) } } func TestMeijiequanCreateOrderFormMatchesUpstreamShape(t *testing.T) { form := meijiequanCreateOrderForm(MeijiequanCreateOrderRequest{ UID: "962", ModelID: 1, Title: "2026合肥全屋定制推荐 6家选型参考", Content: "# 如何选择\n\n\n\n- 正文内容\n- **重点**", ResourceIDArr: []string{"36122_price_0"}, PointTime: "2026-06-06 11:00:00", PointPriceRange: "1", Extra: map[string]any{ "source": "平台稿件", "description": "稿件描述", }, }) if got := form.Get("uid"); got != "962" { t.Fatalf("unexpected uid: %s", got) } if got := form.Get("prop[biaoti]"); got != "2026合肥全屋定制推荐 6家选型参考" { t.Fatalf("unexpected title: %s", got) } if got := form.Get("prop[laiyuan]"); got != "平台稿件" { t.Fatalf("unexpected source: %s", got) } if got := form.Get("prop[miaoshu]"); got != "稿件描述" { t.Fatalf("unexpected description: %s", got) } content := form.Get("prop[neirong]") if !strings.Contains(content, "

正文
` if got := meijiequanArticleHTML(input); got != input { t.Fatalf("expected html passthrough, got %s", got) } } func TestMediaSupplySubmitOrderExtraExtractsNestedPayload(t *testing.T) { extra := mediaSupplySubmitOrderExtra([]byte(`{ "title": "ignored", "extra": { "source": "来源", "point_time": "2026-06-06 11:00:00" }, "point_price_range": "1" }`)) if got := meijiequanAnyString(extra["source"]); got != "来源" { t.Fatalf("unexpected source: %s", got) } if got := meijiequanAnyString(extra["point_time"]); got != "2026-06-06 11:00:00" { t.Fatalf("unexpected point_time: %s", got) } if got := meijiequanAnyString(extra["point_price_range"]); got != "1" { t.Fatalf("unexpected point_price_range: %s", got) } } func TestManualMeijiequanChallengeResolverRequiresManualHandling(t *testing.T) { _, err := ManualMeijiequanChallengeCodeResolver{}.ResolveCode(context.Background(), MeijiequanChallenge{}) if !errors.Is(err, errMeijiequanChallengeRequired) { t.Fatalf("expected challenge required error, got %v", err) } } func TestMeijiequanHiddenInputs(t *testing.T) { fields := parseMeijiequanHiddenInputs(strings.NewReader(` `)) if fields["_token"] != "token-value" || fields["sk"] != "sk-value" { t.Fatalf("unexpected hidden fields: %#v", fields) } } func TestDecodeMeijiequanMediaListAcceptsStringNumbers(t *testing.T) { parsed, err := decodeMeijiequanMediaList([]byte(`{ "status": "1", "info": "ok", "page": "2", "all_page": "7", "all_num": "121", "user_id": "8899", "data": [{"id":"1","name":"测试媒体","sale_price":"12.5"}] }`)) if err != nil { t.Fatalf("decode failed: %v", err) } if parsed.Status != 1 || parsed.Page != 2 || parsed.AllPage != 7 || parsed.AllNum != 121 || parsed.UserID != 8899 { t.Fatalf("unexpected parsed page metadata: %#v", parsed) } if len(parsed.Data) != 1 { t.Fatalf("unexpected item count: %d", len(parsed.Data)) } } func TestDecodeMeijiequanSearchOptions(t *testing.T) { groups, err := decodeMeijiequanSearchOptions([]byte(`{ "pindaoleixing": {"name": "频道类型", "list": ["IT科技", "财经金融"]}, "quyu": {"name": "所在地区", "list": ["全国", "北京"]} }`)) if err != nil { t.Fatalf("decode failed: %v", err) } if len(groups) != 2 { t.Fatalf("unexpected group count: %d", len(groups)) } if groups[0].Key != "pindaoleixing" || groups[0].Name != "频道类型" || groups[0].List[1] != "财经金融" { t.Fatalf("unexpected first group: %#v", groups[0]) } } func TestDecodeMeijiequanPublishedArticleJSON(t *testing.T) { page, err := decodeMeijiequanPublishedArticlePage([]byte(`{ "status": "1", "page": "1", "all_page": "2", "all_num": "21", "data": [{ "id": "8891", "order_code": "A20260529001", "biaoti": "2026合肥品牌推荐", "resource_id": "36122_price_0", "media_name": "测试新闻网", "article_url": "https://news.example.test/a.html", "status_text": "已发表", "publish_time": "2026-05-29 16:00:00" }] }`)) if err != nil { t.Fatalf("decode failed: %v", err) } if page.Page != 1 || page.AllPage != 2 || page.AllNum != 21 || len(page.Items) != 1 { t.Fatalf("unexpected page: %#v", page) } item := page.Items[0] if item.ResourceID != "36122" || item.ExternalArticleURL != "https://news.example.test/a.html" || item.Title != "2026合肥品牌推荐" || item.Status != "已发表" || item.PublishedAt == nil { t.Fatalf("unexpected article item: %#v", item) } } func TestDecodeMeijiequanPublishedArticleHTML(t *testing.T) { page, err := decodeMeijiequanPublishedArticlePage([]byte(`| 2026合肥品牌推荐 | 测试新闻网 | WM118996 | 查看链接 | 已发表 |
| WM118996 | 2026合肥品牌推荐 | 金融树洞网 | 查看回链 | 已发布 |
| 订单号 | 标题 | 资源名称 | 价格 | 状态 | 回链 | 标签 | 创建时间 | 发稿时间 | 退稿原因 | 退稿时间 | 操作 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| WM119065 | 合肥全屋定制怎么选?5 大主流品牌优缺点对比 + 选购建议 | 博客园(GEO排名可发) | 3.00 | 退稿申请中 | 05-29 20:45 | 不想发布了 | 05-29 20:54 | 标签 | 再次发稿 | |||
| WM118996 | 全屋定制品牌哪家强?2026 年合肥品牌推荐与排名 | 金融树洞网 | 3.00 | 已发布 | 05-29 17:16 | 05-29 17:20 | 标签 | 再次发稿 | 申请退稿 |