Files
MAcode/tcs-iptv/internal/api/integration_test.go
T
selfrelease a329d4906b init: AIGC-Hub/AVCC 方案文档 + TCS-IPTV 内容可信锁定系统 MVP
- 方案文档: AVCC 体系建设、IPTV TCS 需求(0-req)/PRD(1-prd)/任务(2-task)/二三四期任务
- tcs-iptv: Go 后端(哈希SDK/MA码生成/可信数据空间mock/业务编排/HTTP API+HMAC鉴权)
- web-console: React+AntD 监管大屏(角色工作台/全流程演示/监管片库)
- 一剧一码+集级哈希, 集级下架/恢复, 全栈测试通过
2026-06-14 16:50:31 +08:00

272 lines
11 KiB
Go

package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/hash"
"github.com/tcs-iptv/tcs/internal/httpx"
"github.com/tcs-iptv/tcs/internal/macode"
"github.com/tcs-iptv/tcs/internal/service"
)
// testServer 组装完整 API 栈(鉴权 + 路由 + service + 内存链/号段)。
func testServer(t *testing.T) (*httptest.Server, *httpx.MemoryKeyStore) {
t.Helper()
gin.SetMode(gin.TestMode)
ch := chain.NewMemoryChain()
gen := macode.NewGenerator(macode.NewMemoryStore())
require.NoError(t, gen.RegisterSegment(macode.Segment{
IndustryNode: "8531", OrgNode: "4401",
Category: macode.CategoryMicroDrama, Start: 1, End: 9999999, SeqWidth: 7,
}))
svc := service.New(ch, gen)
h := NewHandler(svc)
keys := httpx.NewMemoryKeyStore()
keys.Add("ak-regulator", "sk-regulator", string(chain.RoleRegulator))
keys.Add("ak-reviewer", "sk-reviewer", string(chain.RoleReviewer))
keys.Add("ak-cp", "sk-cp", string(chain.RoleCP))
keys.Add("ak-operator", "sk-operator", string(chain.RoleOperator))
r := gin.New()
v1 := r.Group("/api/v1", httpx.AuthMiddleware(keys))
h.Register(v1)
return httptest.NewServer(r), keys
}
// signedCall 发起带 HMAC 签名的请求,返回状态码与解析后的响应。
func signedCall(t *testing.T, base, apiKey, secret, method, path string, body any) (int, map[string]any) {
t.Helper()
var buf []byte
if body != nil {
buf, _ = json.Marshal(body)
}
signPath := "/api/v1" + path
if i := indexByte(path, '?'); i >= 0 {
signPath = "/api/v1" + path[:i]
}
sig := httpx.Sign(secret, method, signPath)
req, err := http.NewRequest(method, base+"/api/v1"+path, bytes.NewReader(buf))
require.NoError(t, err)
req.Header.Set("Authorization", "TCS "+apiKey+":"+sig)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
var out map[string]any
_ = json.NewDecoder(resp.Body).Decode(&out)
return resp.StatusCode, out
}
func indexByte(s string, b byte) int {
for i := 0; i < len(s); i++ {
if s[i] == b {
return i
}
}
return -1
}
func dataOf(m map[string]any) map[string]any {
if d, ok := m["data"].(map[string]any); ok {
return d
}
return map[string]any{}
}
// TestE2E_FullLifecycle 覆盖 MVP 全闭环:送审→发码→审核→入库→发布→注入→下架。
func TestE2E_FullLifecycle(t *testing.T) {
srv, _ := testServer(t)
defer srv.Close()
b := srv.URL
// 1) CP 送审
st, resp := signedCall(t, b, "ak-cp", "sk-cp", "POST", "/content/register", map[string]any{
"title": "示例微短剧", "episode_count": 24, "category": "WD",
"file_sha256": "fh-e2e", "merkle_root": "mr-e2e", "perceptual_hash": "ph-e2e",
"cp_media_id": "FS-77821", "cp_name": "飞翮信息",
})
require.Equal(t, http.StatusAccepted, st)
reviewID := dataOf(resp)["review_id"].(string)
ctid := dataOf(resp)["content_twin_id"].(string)
require.NotEmpty(t, reviewID)
// 2) CSPS 合规审核(发码前)
st, _ = signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/csps-result", map[string]any{
"review_id": reviewID, "approved": true, "reviewer_id": "rv-1",
})
require.Equal(t, http.StatusOK, st)
// 3) 监管发码签发(审核通过后)
st, resp = signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/issue", map[string]any{
"review_id": reviewID, "issuer": "北京市广播电视局",
})
require.Equal(t, http.StatusOK, st)
maCode := dataOf(resp)["ma_code"].(string)
cert := dataOf(resp)["certificate"].(string)
assert.True(t, macode.IsValid(maCode))
// 4) 入媒资库
st, _ = signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/ingest", map[string]any{
"ma_code": maCode, "content_twin_id": ctid, "media_asset_id": "MEDIA-001", "lib_name": "广东IPTV媒资库",
})
require.Equal(t, http.StatusOK, st)
// 5) 发布
st, _ = signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/publish", map[string]any{
"ma_code": maCode, "certificate": cert,
})
require.Equal(t, http.StatusOK, st)
// 6) CDN 注入(匹配)
st, resp = signedCall(t, b, "ak-operator", "sk-operator", "POST", "/content/inject", map[string]any{
"content_twin_id": ctid, "ma_code": maCode, "file_sha256": "fh-e2e",
"operator_id": "CT-IPTV-GD", "cdn_endpoint": "cdn://ct-gd/vod/1",
})
require.Equal(t, http.StatusOK, st)
assert.Equal(t, true, dataOf(resp)["allowed"])
// 7) 映射查询
st, resp = signedCall(t, b, "ak-regulator", "sk-regulator", "GET", "/content/mappings?ma_code="+maCode, nil)
require.Equal(t, http.StatusOK, st)
mappings := dataOf(resp)["mappings"].([]any)
assert.Len(t, mappings, 3) // cp + reviewer + operator
// 8) 监管下架
st, resp = signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/takedown", map[string]any{
"ma_code": maCode, "reason": "违规",
})
require.Equal(t, http.StatusOK, st)
assert.NotEmpty(t, dataOf(resp)["cdn_endpoints"])
}
// TestE2E_TamperRejected 版本篡改专项:注入篡改文件应被拒绝(需求7/15/18-AC4)。
func TestE2E_TamperRejected(t *testing.T) {
srv, _ := testServer(t)
defer srv.Close()
b := srv.URL
st, resp := signedCall(t, b, "ak-cp", "sk-cp", "POST", "/content/register", map[string]any{
"title": "X", "episode_count": 1, "category": "WD",
"file_sha256": "fh-orig", "merkle_root": "mr-orig",
})
require.Equal(t, http.StatusAccepted, st)
reviewID := dataOf(resp)["review_id"].(string)
ctid := dataOf(resp)["content_twin_id"].(string)
signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/csps-result", map[string]any{"review_id": reviewID, "approved": true})
_, resp = signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/issue", map[string]any{
"review_id": reviewID, "issuer": "x",
})
maCode := dataOf(resp)["ma_code"].(string)
cert := dataOf(resp)["certificate"].(string)
signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/ingest", map[string]any{"ma_code": maCode, "content_twin_id": ctid, "media_asset_id": "M", "lib_name": "L"})
signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/publish", map[string]any{"ma_code": maCode, "certificate": cert})
// 篡改文件注入 → 拒绝
st, _ = signedCall(t, b, "ak-operator", "sk-operator", "POST", "/content/inject", map[string]any{
"content_twin_id": ctid, "ma_code": maCode, "file_sha256": "fh-TAMPERED",
"operator_id": "OP", "cdn_endpoint": "cdn://x",
})
assert.Equal(t, http.StatusBadRequest, st, "篡改注入必须被拒")
}
// TestE2E_VersionChangeLocatesEpisode 版本变更定位被篡改集(需求12-AC3)。
func TestE2E_VersionChangeLocatesEpisode(t *testing.T) {
srv, _ := testServer(t)
defer srv.Close()
b := srv.URL
st, resp := signedCall(t, b, "ak-cp", "sk-cp", "POST", "/content/register", map[string]any{
"title": "多集剧", "episode_count": 3, "category": "WD",
"file_sha256": "fh-multi", "merkle_root": "mr-multi",
})
require.Equal(t, http.StatusAccepted, st)
reviewID := dataOf(resp)["review_id"].(string)
ctid := dataOf(resp)["content_twin_id"].(string)
signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/csps-result", map[string]any{"review_id": reviewID, "approved": true})
signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/issue", map[string]any{"review_id": reviewID, "issuer": "x"})
old := []string{hash.SHA256Hex([]byte("ep1")), hash.SHA256Hex([]byte("ep2")), hash.SHA256Hex([]byte("ep3"))}
neu := []string{hash.SHA256Hex([]byte("ep1")), hash.SHA256Hex([]byte("ep2-X")), hash.SHA256Hex([]byte("ep3"))}
st, resp = signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/version-change", map[string]any{
"content_twin_id": ctid, "reason": "第2集替换",
"prev_hash": "mr-multi", "new_hash": "mr-new",
"old_segments": old, "new_segments": neu,
})
require.Equal(t, http.StatusOK, st)
eps := dataOf(resp)["affected_episodes"].([]any)
require.Len(t, eps, 1)
assert.Equal(t, float64(2), eps[0], "应定位到第2集")
}
// TestE2E_PermissionMatrix 权限矩阵:越权操作被拒(需求14)。
func TestE2E_PermissionMatrix(t *testing.T) {
srv, _ := testServer(t)
defer srv.Close()
b := srv.URL
// 准备一条已签发内容
_, resp := signedCall(t, b, "ak-cp", "sk-cp", "POST", "/content/register", map[string]any{
"title": "P", "episode_count": 1, "category": "WD", "file_sha256": "fh-perm", "merkle_root": "mr-perm",
})
reviewID := dataOf(resp)["review_id"].(string)
// 先过 CSPS 审核(否则发码会因未审核被拒,无法测到角色权限)
signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/csps-result", map[string]any{"review_id": reviewID, "approved": true})
// CP 越权发码(签发仅监管主体)→ 403
st, _ := signedCall(t, b, "ak-cp", "sk-cp", "POST", "/content/issue", map[string]any{"review_id": reviewID, "issuer": "x"})
assert.Equal(t, http.StatusForbidden, st, "CP 不得发码")
// 正常签发
_, resp = signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/issue", map[string]any{"review_id": reviewID, "issuer": "x"})
maCode := dataOf(resp)["ma_code"].(string)
// 运营商越权下架 → 403
st, _ = signedCall(t, b, "ak-operator", "sk-operator", "POST", "/content/takedown", map[string]any{"ma_code": maCode, "reason": "越权"})
assert.Equal(t, http.StatusForbidden, st, "运营商不得发起下架")
// 无效签名 → 401
req, _ := http.NewRequest("GET", b+"/api/v1/content/mappings?ma_code="+maCode, nil)
req.Header.Set("Authorization", "TCS ak-regulator:badsig")
r, _ := http.DefaultClient.Do(req)
assert.Equal(t, http.StatusUnauthorized, r.StatusCode, "错误签名应 401")
r.Body.Close()
}
// TestE2E_TakedownLatency 下架时效:端到端应远快于分钟级(需求11/18-AC1)。
func TestE2E_TakedownLatency(t *testing.T) {
srv, _ := testServer(t)
defer srv.Close()
b := srv.URL
_, resp := signedCall(t, b, "ak-cp", "sk-cp", "POST", "/content/register", map[string]any{
"title": "L", "episode_count": 1, "category": "WD", "file_sha256": "fh-lat", "merkle_root": "mr-lat",
})
reviewID := dataOf(resp)["review_id"].(string)
signedCall(t, b, "ak-reviewer", "sk-reviewer", "POST", "/content/csps-result", map[string]any{"review_id": reviewID, "approved": true})
_, resp = signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/issue", map[string]any{"review_id": reviewID, "issuer": "x"})
maCode := dataOf(resp)["ma_code"].(string)
start := time.Now()
st, _ := signedCall(t, b, "ak-regulator", "sk-regulator", "POST", "/content/takedown", map[string]any{"ma_code": maCode, "reason": "违规"})
elapsed := time.Since(start)
require.Equal(t, http.StatusOK, st)
assert.Less(t, elapsed, time.Second, "下架端到端应在秒级内(目标分钟级)")
}