真机验证时 55066525387 报「回读商品视频失败:货憨憨尚未显示已关联的视频」, 但视频其实已经上传成功。两个写接口都返回了 SUCCESS,是回读判据写错了。 video 字段装的是已经在 Shopee 上生效的视频,不是刚设置的那个。货憨憨推送 到 Shopee 是异步的,保存成功后 video 必然还是空的,视频这时在 tempVideoUrl 和 videoUploadIdStr 里。证据是 payloads/huohanhan_save_product_info.har 的 第 3 个请求——一次保存成功后立刻发起的 getDetail,那里 video=[] 而 tempVideoUrl 有值。这条证据本来就在仓库里,之前定判据时没有用上。 HasShopProductVideo 改为 CheckShopProductVideo,返回四个字段: videoFailReason 非空 → 失败,把原因写进 last_error video 非空 → 已在 Shopee 生效 tempVideoUrl 或 videoUploadIdStr 非空 → 已提交,等待同步,判为成功 四者皆空 → 确实没设上,失败 upload_status=done 的语义随之明确为「已提交并回读确认」, 而不是「Shopee 已可见」——后者是异步结果,本地无法立刻断言, 日志会分别提示这两种情况。 确认框的覆盖警告改用 Confirmed():等待同步中的视频同样会被覆盖, 只看 video 会漏掉这种情况,导致该警告不显示。 同时订正 55066525387 被误判写成的 failed 状态。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LbdtsD3ohhSMy3KPoCgARq
185 lines
7.3 KiB
Go
185 lines
7.3 KiB
Go
package huohanhan
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"testing"
|
||
|
||
"cmsp/internal/logx"
|
||
)
|
||
|
||
func Test上传素材使用HAR规定的multipart字段(t *testing.T) {
|
||
client := newBusinessTestClient(t, logx.New(100), func(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Path != "/api/product/material/uploadFiles" {
|
||
t.Errorf("上传路径不正确:%s", r.URL.Path)
|
||
}
|
||
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||
t.Fatalf("解析上传表单失败:%v", err)
|
||
}
|
||
file, header, err := r.FormFile("files")
|
||
if err != nil {
|
||
t.Fatalf("上传表单缺少 files:%v", err)
|
||
}
|
||
defer file.Close()
|
||
if header.Filename != "本地视频.mp4" {
|
||
t.Errorf("文件名应取本地文件名,实际 %q", header.Filename)
|
||
}
|
||
if got := header.Header.Get("Content-Type"); got != "video/mp4" {
|
||
t.Errorf("视频 Content-Type 应为 video/mp4,实际 %q", got)
|
||
}
|
||
content, _ := io.ReadAll(file)
|
||
if string(content) != "fake-mp4" {
|
||
t.Errorf("视频内容不正确:%q", content)
|
||
}
|
||
if r.Form.Get("isLocalFile") != "true" || r.Form.Get("fileType") != "1" {
|
||
t.Errorf("上传固定字段不正确:%v", r.Form)
|
||
}
|
||
writeJSON(w, map[string]any{"type": "SUCCESS", "code": "200", "bean": []string{"https://cos.example.invalid/video.mp4"}})
|
||
})
|
||
|
||
url, err := client.UploadVideo(context.Background(), `C:\下载\本地视频.mp4`, []byte("fake-mp4"))
|
||
if err != nil || url != "https://cos.example.invalid/video.mp4" {
|
||
t.Fatalf("上传结果不正确:url=%q err=%v", url, err)
|
||
}
|
||
}
|
||
|
||
func Test上传素材bean为空必须失败(t *testing.T) {
|
||
client := newBusinessTestClient(t, logx.New(100), func(w http.ResponseWriter, r *http.Request) {
|
||
writeJSON(w, map[string]any{"type": "SUCCESS", "code": "200", "bean": []string{}})
|
||
})
|
||
_, err := client.UploadVideo(context.Background(), "empty.mp4", []byte("fake-mp4"))
|
||
if err == nil || !strings.Contains(err.Error(), "未返回视频地址") {
|
||
t.Fatalf("bean 为空必须返回可读错误,实际 %v", err)
|
||
}
|
||
}
|
||
|
||
func Test关联商品视频请求必须是三个字段的裸数组(t *testing.T) {
|
||
client := newBusinessTestClient(t, logx.New(100), func(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Path != "/api/product/batchEdit/batchUpdateShopProductVideo" {
|
||
t.Errorf("关联路径不正确:%s", r.URL.Path)
|
||
}
|
||
var body []map[string]string
|
||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||
t.Fatalf("关联请求不是 JSON:%v", err)
|
||
}
|
||
if len(body) != 1 {
|
||
t.Fatalf("关联请求必须是仅含一个元素的裸数组,实际 %v", body)
|
||
}
|
||
want := map[string]string{"id": "货憨憨内部ID", "platformShopId": "店铺ID", "videoUrl": "https://cos.example.invalid/video.mp4"}
|
||
if len(body[0]) != len(want) {
|
||
t.Fatalf("关联元素必须恰好三个字段,实际 %v", body[0])
|
||
}
|
||
for key, value := range want {
|
||
if body[0][key] != value {
|
||
t.Errorf("字段 %s 期望 %q,实际 %q", key, value, body[0][key])
|
||
}
|
||
}
|
||
writeJSON(w, map[string]any{"type": "SUCCESS", "code": "200"})
|
||
})
|
||
if err := client.UpdateShopProductVideo(context.Background(), "货憨憨内部ID", "店铺ID", "https://cos.example.invalid/video.mp4"); err != nil {
|
||
t.Fatalf("关联应成功,实际 %v", err)
|
||
}
|
||
}
|
||
|
||
func Test非SUCCESS响应只请求一次并返回中文错误(t *testing.T) {
|
||
calls := 0
|
||
client := newBusinessTestClient(t, logx.New(100), func(w http.ResponseWriter, r *http.Request) {
|
||
calls++
|
||
writeJSON(w, map[string]any{"type": "ERROR", "code": "LIMIT", "message": "空间不足"})
|
||
})
|
||
err := client.UpdateShopProductVideo(context.Background(), "内部ID", "店铺ID", "https://cos.example.invalid/video.mp4")
|
||
if err == nil || !strings.Contains(err.Error(), "关联商品视频失败") || !strings.Contains(err.Error(), "空间不足") {
|
||
t.Fatalf("非 SUCCESS 应返回可读中文错误,实际 %v", err)
|
||
}
|
||
if calls != 1 {
|
||
t.Fatalf("非 SUCCESS 不得重试,实际请求 %d 次", calls)
|
||
}
|
||
}
|
||
|
||
// 回读用的假服务器,records[0] 直接用给定字段。
|
||
func newVideoCheckClient(t *testing.T, record map[string]any) *Client {
|
||
return newBusinessTestClient(t, logx.New(100), func(w http.ResponseWriter, r *http.Request) {
|
||
if err := r.ParseForm(); err != nil {
|
||
t.Fatalf("解析回读表单失败:%v", err)
|
||
}
|
||
if r.Form.Get("ids") != "货憨憨内部ID" || r.Form.Get("fields") != "video,videoUploadIdStr,videoFailReason,tempVideoUrl" {
|
||
t.Errorf("回读表单不符合 HAR:%v", r.Form)
|
||
}
|
||
writeJSON(w, map[string]any{"records": []any{record}, "total": "1"})
|
||
})
|
||
}
|
||
|
||
func Test回读四个字段全空判定为未关联(t *testing.T) {
|
||
client := newVideoCheckClient(t, map[string]any{
|
||
"video": []any{}, "tempVideoUrl": "", "videoUploadIdStr": "", "videoFailReason": "",
|
||
})
|
||
check, err := client.CheckShopProductVideo(context.Background(), "货憨憨内部ID")
|
||
if err != nil {
|
||
t.Fatalf("回读失败:%v", err)
|
||
}
|
||
if check.Confirmed() || check.LiveOnShopee() {
|
||
t.Fatalf("四个字段全空必须判为未关联:%+v", check)
|
||
}
|
||
}
|
||
|
||
// 这条是本项目踩过的真实坑:保存成功后货憨憨推送到 Shopee 是异步的,
|
||
// video 必然还是空的,视频这时在 tempVideoUrl 里。
|
||
// 只认 video 会把成功的上传误判成失败,55066525387 就是这样报错的。
|
||
// 证据:payloads/huohanhan_save_product_info.har 第 3 个请求。
|
||
func Test刚保存完video为空但tempVideoUrl有值应判为成功(t *testing.T) {
|
||
client := newVideoCheckClient(t, map[string]any{
|
||
"video": []any{},
|
||
"tempVideoUrl": "https://hhh-prod-1307856765.cos.ap-guangzhou.myqcloud.com/video/1126859448838946817.mp4",
|
||
"videoUploadIdStr": "sg-11110106-6vbma-msnl9mrjxxqd2d",
|
||
"videoFailReason": "",
|
||
})
|
||
check, err := client.CheckShopProductVideo(context.Background(), "货憨憨内部ID")
|
||
if err != nil {
|
||
t.Fatalf("回读失败:%v", err)
|
||
}
|
||
if !check.Confirmed() {
|
||
t.Fatalf("tempVideoUrl 有值必须判为已设置成功:%+v", check)
|
||
}
|
||
if check.LiveOnShopee() {
|
||
t.Fatalf("video 为空时不得声称已在 Shopee 生效:%+v", check)
|
||
}
|
||
}
|
||
|
||
func Test只有videoUploadIdStr有值也判为成功(t *testing.T) {
|
||
client := newVideoCheckClient(t, map[string]any{
|
||
"video": []any{}, "tempVideoUrl": "", "videoUploadIdStr": "sg-11110106-abc", "videoFailReason": "",
|
||
})
|
||
check, _ := client.CheckShopProductVideo(context.Background(), "货憨憨内部ID")
|
||
if !check.Confirmed() {
|
||
t.Fatalf("videoUploadIdStr 有值必须判为已设置成功:%+v", check)
|
||
}
|
||
}
|
||
|
||
func Test字段video有值判为已在Shopee生效(t *testing.T) {
|
||
client := newVideoCheckClient(t, map[string]any{
|
||
"video": []any{map[string]any{"videoUrl": "https://cvf.shopee.tw/file/xxx.mp4"}},
|
||
"tempVideoUrl": "", "videoUploadIdStr": "", "videoFailReason": "",
|
||
})
|
||
check, _ := client.CheckShopProductVideo(context.Background(), "货憨憨内部ID")
|
||
if !check.Confirmed() || !check.LiveOnShopee() {
|
||
t.Fatalf("video 有值必须同时判为已确认且已生效:%+v", check)
|
||
}
|
||
}
|
||
|
||
func Test回读带失败原因时暴露原因(t *testing.T) {
|
||
client := newVideoCheckClient(t, map[string]any{
|
||
"video": []any{}, "tempVideoUrl": "", "videoUploadIdStr": "",
|
||
"videoFailReason": "视频时长超过限制",
|
||
})
|
||
check, err := client.CheckShopProductVideo(context.Background(), "货憨憨内部ID")
|
||
if err != nil {
|
||
t.Fatalf("回读失败:%v", err)
|
||
}
|
||
if check.FailReason != "视频时长超过限制" {
|
||
t.Fatalf("失败原因必须原样带出:%+v", check)
|
||
}
|
||
}
|