222 lines
7.4 KiB
Go
222 lines
7.4 KiB
Go
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)
|
|
}
|
|
}
|