Files
cmsp/internal/huohanhan/upload_test.go
T
QiuSWandClaude Opus 5 0fe24f70f4 fix: 回读不能只看 video 字段,会把成功的上传误判为失败 (#17)
真机验证时 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
2026-09-03 16:02:27 +08:00

185 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}