package api_test import ( "bytes" "encoding/json" "fmt" "mime/multipart" "net/http/httptest" "net/textproto" "strings" "testing" "github.com/gin-gonic/gin" "everypublish/server/internal/models" ) func multipartBody(t *testing.T, filename, contentType string, content []byte) *bytes.Buffer { t.Helper() buf := &bytes.Buffer{} w := multipart.NewWriter(buf) h := make(textproto.MIMEHeader) h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, filename)) h.Set("Content-Type", contentType) part, err := w.CreatePart(h) if err != nil { t.Fatalf("create part: %v", err) } _, _ = part.Write(content) _ = w.Close() return buf } func uploadMaterial(t *testing.T, access string, filename string, content []byte) (uint64, string) { t.Helper() buf := multipartBody(t, filename, "video/mp4", content) req := httptest.NewRequest("POST", "/api/v1/materials", buf) req.Header.Set("Content-Type", "multipart/form-data; boundary="+strings.Split(buf.String(), "\r\n")[0][2:]) req.Header.Set("Authorization", "Bearer "+access) w := httptest.NewRecorder() testRouter.ServeHTTP(w, req) var env envelope _ = json.Unmarshal(w.Body.Bytes(), &env) if w.Code != 200 || env.Code != 0 { t.Fatalf("upload failed: %d %s", w.Code, env.Message) } var data struct { Material models.Material `json:"material"` Dedup bool `json:"dedup"` } _ = json.Unmarshal(env.Data, &data) if data.Dedup { t.Fatal("first upload should not be dedup") } return data.Material.ID, data.Material.SHA256 } func TestAccountChallengeFlow(t *testing.T) { resetTestDB() access, _ := register(t, "ops@test.com") code, env := doReq(t, "POST", "/api/v1/accounts", access, gin.H{"platform": "douyin", "accountName": "抖音测试号"}) if code != 200 || env.Code != 0 { t.Fatalf("create account failed: %d %s", code, env.Message) } var acc models.Account _ = json.Unmarshal(env.Data, &acc) // 不支持的平台 code, _ = doReq(t, "POST", "/api/v1/accounts", access, gin.H{"platform": "tiktok", "accountName": "x"}) if code != 400 { t.Fatalf("unsupported platform should 400, got %d", code) } // 绑定 → 挑战 code, env = doReq(t, "POST", fmt.Sprintf("/api/v1/accounts/%d/bind", acc.ID), access, nil) if code != 200 || env.Code != 0 { t.Fatalf("bind failed: %d %s", code, env.Message) } // 绑定中账号不可删 code, _ = doReq(t, "DELETE", fmt.Sprintf("/api/v1/accounts/%d", acc.ID), access, nil) if code != 409 { t.Fatalf("delete binding account should 409, got %d", code) } // 挑战列表 code, env = doReq(t, "GET", "/api/v1/challenges", access, nil) if code != 200 || env.Code != 0 { t.Fatalf("challenge list failed: %d %s", code, env.Message) } var cl struct { List []models.Challenge `json:"list"` } _ = json.Unmarshal(env.Data, &cl) if len(cl.List) != 1 || cl.List[0].Kind != "pending" { t.Fatalf("expect 1 pending challenge, got %d", len(cl.List)) } // 挂起 → 重发 code, env = doReq(t, "POST", fmt.Sprintf("/api/v1/challenges/%d/suspend", cl.List[0].ID), access, nil) if code != 200 { t.Fatalf("suspend failed: %d", code) } code, env = doReq(t, "POST", fmt.Sprintf("/api/v1/challenges/%d/resend", cl.List[0].ID), access, nil) if code != 200 { t.Fatalf("resend failed: %d", code) } } func TestMaterialAndTaskFlow(t *testing.T) { resetTestDB() access, _ := register(t, "pub@test.com") content := []byte("fake-video-bytes-for-sha256-test-0001") matID, sha := uploadMaterial(t, access, "demo.mp4", content) if len(sha) != 64 { t.Fatalf("bad sha256: %s", sha) } // 重复上传 → 去重 buf := multipartBody(t, "demo2.mp4", "video/mp4", content) req := httptest.NewRequest("POST", "/api/v1/materials", buf) req.Header.Set("Content-Type", "multipart/form-data; boundary="+strings.Split(buf.String(), "\r\n")[0][2:]) req.Header.Set("Authorization", "Bearer "+access) w := httptest.NewRecorder() testRouter.ServeHTTP(w, req) var env envelope _ = json.Unmarshal(w.Body.Bytes(), &env) var data struct { Material models.Material `json:"material"` Dedup bool `json:"dedup"` } _ = json.Unmarshal(env.Data, &data) if !data.Dedup || data.Material.ID != matID { t.Fatalf("dedup expect same id %d, got dedup=%v id=%d", matID, data.Dedup, data.Material.ID) } // 签名直链(一次性) code, env := doReq(t, "GET", fmt.Sprintf("/api/v1/materials/%d/url", matID), access, nil) if code != 200 { t.Fatalf("url failed: %d", code) } var urld struct { URL string `json:"url"` } _ = json.Unmarshal(env.Data, &urld) path := strings.TrimPrefix(urld.URL, "http://127.0.0.1:8090") if path == urld.URL { t.Fatalf("bad url: %s", urld.URL) } code, _ = doReq(t, "GET", path, "", nil) if code != 200 { t.Fatalf("file fetch should 200, got %d", code) } code, _ = doReq(t, "GET", path, "", nil) if code != 404 { t.Fatalf("one-time token should 404 on reuse, got %d", code) } // 建任务 → 提交 → 驳回 → 重提 → 通过 → 取消 code, env = doReq(t, "POST", "/api/v1/tasks", access, gin.H{ "title": "新品发布", "content": "正文", "tags": []string{"新品"}, "accountIds": []uint64{1}, "materialIds": []uint64{matID}, }) if code != 200 || env.Code != 0 { t.Fatalf("create task failed: %d %s", code, env.Message) } var taskRow models.Task _ = json.Unmarshal(env.Data, &taskRow) if taskRow.Status != "draft" { t.Fatalf("new task should be draft, got %s", taskRow.Status) } // 非法转移:draft 直接 approve 应 409 code, _ = doReq(t, "POST", fmt.Sprintf("/api/v1/tasks/%d/approve", taskRow.ID), access, nil) if code != 409 { t.Fatalf("draft approve should 409, got %d", code) } code, _ = doReq(t, "POST", fmt.Sprintf("/api/v1/tasks/%d/submit", taskRow.ID), access, nil) if code != 200 { t.Fatalf("submit failed: %d", code) } code, _ = doReq(t, "POST", fmt.Sprintf("/api/v1/tasks/%d/reject", taskRow.ID), access, gin.H{"note": "标题需修改"}) if code != 200 { t.Fatalf("reject failed: %d", code) } code, env = doReq(t, "GET", fmt.Sprintf("/api/v1/tasks/%d", taskRow.ID), access, nil) _ = json.Unmarshal(env.Data, &taskRow) if taskRow.Status != "rejected" { t.Fatalf("expect rejected, got %s", taskRow.Status) } code, _ = doReq(t, "POST", fmt.Sprintf("/api/v1/tasks/%d/resubmit", taskRow.ID), access, nil) if code != 200 { t.Fatalf("resubmit failed: %d", code) } code, _ = doReq(t, "POST", fmt.Sprintf("/api/v1/tasks/%d/approve", taskRow.ID), access, nil) if code != 200 { t.Fatalf("approve failed: %d", code) } code, env = doReq(t, "GET", fmt.Sprintf("/api/v1/tasks/%d", taskRow.ID), access, nil) _ = json.Unmarshal(env.Data, &taskRow) if taskRow.Status != "queued" { t.Fatalf("expect queued, got %s", taskRow.Status) } code, _ = doReq(t, "POST", fmt.Sprintf("/api/v1/tasks/%d/cancel", taskRow.ID), access, nil) if code != 200 { t.Fatalf("cancel failed: %d", code) } // 通知:驳回+通过 2 条 code, env = doReq(t, "GET", "/api/v1/notifications", access, nil) if code != 200 { t.Fatalf("notifications failed: %d", code) } var nl struct { List []models.Notification `json:"list"` Unread int64 `json:"unread"` } _ = json.Unmarshal(env.Data, &nl) if nl.Unread < 2 { t.Fatalf("expect >=2 unread, got %d", nl.Unread) } code, _ = doReq(t, "POST", "/api/v1/notifications/read-all", access, nil) if code != 200 { t.Fatalf("read-all failed: %d", code) } code, env = doReq(t, "GET", "/api/v1/notifications", access, nil) _ = json.Unmarshal(env.Data, &nl) if nl.Unread != 0 { t.Fatalf("expect 0 unread after read-all, got %d", nl.Unread) } }