server: Go 服务器(REST+WSS 网关+JWT+Argon2+配对+素材直链+任务状态机);修复并发下线 send-on-closed-channel、下发查询 SQL 优先级、配对码原子占用、上传体积上限、JWT 默认密钥告警
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user