Files
MAcode/tcs-iptv/internal/chain/conformance_test.go
T

269 lines
8.8 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 chain
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tcs-iptv/tcs/internal/model"
)
// RunClientConformance 是 chain.Client 的契约测试套件,校验任意实现都满足同一组
// 不可变业务规则(权限/1:1强绑定/防换壳/状态机/集级粒度)。
//
// 各实现(MemoryChain / PersistentChain / ChainMakerClient)复用本套件,
// 保证「换实现不换行为」,这是平滑替换真实链的安全保障。
//
// newClient 必须返回一个干净(空状态)的 Client 实例。
func RunClientConformance(t *testing.T, newClient func(t *testing.T) Client) {
// 构造一条标准发码请求(集级 3 集)。
issueReq := func(ma, ctid, fh string) IssueRequest {
return IssueRequest{
MACode: ma, ContentTwinID: ctid, MerkleRoot: "mr-" + fh, FileHash: fh,
PerceptualHash: "ph-" + fh,
Episodes: []model.EpisodeHash{
{Episode: 1, FileSHA256: fh + "-E1"},
{Episode: 2, FileSHA256: fh + "-E2"},
{Episode: 3, FileSHA256: fh + "-E3"},
},
Content: model.Content{Title: "契约测试剧", EpisodeCount: 3, MAType: "WD", Issuer: "测试局"},
}
}
const ma = "MA.156.8531.6101/WD/20260000001"
const ctid = "ctid-conf-001"
const fh = "fh-conf-001"
t.Run("IssueMA_仅监管可发码", func(t *testing.T) {
c := newClient(t)
_, err := c.IssueMA(RoleCP, issueReq(ma, ctid, fh))
assert.ErrorIs(t, err, ErrPermissionDenied)
})
t.Run("IssueMA_成功并可查询", func(t *testing.T) {
c := newClient(t)
tx, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
assert.NotEmpty(t, tx)
got, err := c.QueryContent(ma)
require.NoError(t, err)
assert.Equal(t, "契约测试剧", got.Title)
assert.Equal(t, model.StatusApproved, got.Status)
})
t.Run("IssueMA_不可重复签发", func(t *testing.T) {
c := newClient(t)
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
_, err = c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
assert.ErrorIs(t, err, ErrMAAlreadyIssued)
})
t.Run("防换壳_同哈希不可绑不同MA", func(t *testing.T) {
c := newClient(t)
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
_, exists := c.HashExists(fh)
assert.True(t, exists)
_, err = c.IssueMA(RoleRegulator, issueReq("MA.156.8531.6101/WD/20260000002", "ctid-x", fh))
assert.ErrorIs(t, err, ErrHashExists)
})
t.Run("VerifyHash_匹配与不匹配", func(t *testing.T) {
c := newClient(t)
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
r, err := c.VerifyHash(ma, fh)
require.NoError(t, err)
assert.True(t, r.Match)
r2, _ := c.VerifyHash(ma, "tampered")
assert.False(t, r2.Match)
})
t.Run("集级验真与列出", func(t *testing.T) {
c := newClient(t)
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
r, err := c.VerifyEpisodeHash(ma, 2, fh+"-E2")
require.NoError(t, err)
assert.True(t, r.Match)
eps, err := c.ListEpisodes(ma)
require.NoError(t, err)
assert.Len(t, eps, 3)
})
t.Run("映射注册需先发码且可查", func(t *testing.T) {
c := newClient(t)
// 未发码不可注册映射
_, err := c.RegisterMapping(RoleCP, model.Mapping{ContentTwinID: "ctid-none", Party: model.PartyCP, PartyID: "X"})
assert.ErrorIs(t, err, ErrMANotIssued)
// 发码后可注册并查询
_, err = c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
_, err = c.RegisterMapping(RoleOperator, model.Mapping{
ContentTwinID: ctid, Party: model.PartyOperator, PartyID: "OP-1", CDNEndpoint: "cdn://x",
})
require.NoError(t, err)
mr, err := c.QueryMappings(ma)
require.NoError(t, err)
assert.GreaterOrEqual(t, len(mr.Mappings), 1)
assert.Contains(t, mr.CDNEndpoints, "cdn://x")
})
t.Run("下架_仅监管且状态变更", func(t *testing.T) {
c := newClient(t)
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
_, err = c.Revoke(RoleCP, ma, "x")
assert.ErrorIs(t, err, ErrPermissionDenied)
_, err = c.Revoke(RoleRegulator, ma, "违规")
require.NoError(t, err)
got, _ := c.QueryContent(ma)
assert.Equal(t, model.StatusRevoked, got.Status)
})
t.Run("集级下架与恢复", func(t *testing.T) {
c := newClient(t)
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
require.NoError(t, c.RevokeEpisode(RoleRegulator, ma, 2, "违规集"))
eps, _ := c.ListEpisodes(ma)
for _, e := range eps {
if e.Episode == 2 {
assert.True(t, e.Revoked)
}
}
require.NoError(t, c.RestoreEpisode(RoleRegulator, ma, 2))
eps, _ = c.ListEpisodes(ma)
for _, e := range eps {
if e.Episode == 2 {
assert.False(t, e.Revoked)
}
}
})
t.Run("状态流转与按状态列举", func(t *testing.T) {
c := newClient(t)
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
require.NoError(t, c.SetContentStatus(ma, model.StatusPublished))
pub, err := c.ListContents(model.StatusPublished)
require.NoError(t, err)
assert.Len(t, pub, 1)
})
t.Run("多维度查询_按Hash反查", func(t *testing.T) {
c := newClient(t)
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
// 注册 CP 映射(省级内容编码)
_, err = c.RegisterMapping(RoleCP, model.Mapping{
ContentTwinID: ctid, Party: model.PartyCP, PartyID: "PROV-001", PartyName: "陕西CP",
})
require.NoError(t, err)
// 注册媒资库映射(片库文件 ID
_, err = c.RegisterMapping(RoleReviewer, model.Mapping{
ContentTwinID: ctid, Party: model.PartyReviewer, PartyID: "LIB-001", PartyName: "陕西媒资库",
})
require.NoError(t, err)
// 按 Hash 查询
res, err := c.QueryByHash(fh)
require.NoError(t, err)
assert.True(t, res.Found)
assert.Equal(t, ma, res.Content.MACode)
assert.Equal(t, "契约测试剧", res.Content.Title)
// 按省级内容编码查询
res2, err := c.QueryByProvincialCode("PROV-001")
require.NoError(t, err)
assert.True(t, res2.Found)
assert.Equal(t, ma, res2.Content.MACode)
// 按片库文件 ID 查询
res3, err := c.QueryByLibraryFileID("LIB-001")
require.NoError(t, err)
assert.True(t, res3.Found)
assert.Equal(t, ma, res3.Content.MACode)
// 不存在的 Hash
_, err = c.QueryByHash("nonexistent")
assert.ErrorIs(t, err, ErrNotFound)
})
t.Run("MA合并_仅监管且迁移绑定", func(t *testing.T) {
c := newClient(t)
// 发码两个 MA
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
ma2 := "MA.156.8531.6101/WD/20260000002"
ctid2 := "ctid-conf-002"
fh2 := "fh-conf-002"
_, err = c.IssueMA(RoleRegulator, issueReq(ma2, ctid2, fh2))
require.NoError(t, err)
// 非监管不可合并
_, err = c.MergeMA(RoleCP, model.MergeRequest{
PrimaryMACode: ma, SecondaryMACodes: []string{ma2}, Reason: "重复发码",
})
assert.ErrorIs(t, err, ErrPermissionDenied)
// 监管合并
res, err := c.MergeMA(RoleRegulator, model.MergeRequest{
PrimaryMACode: ma, SecondaryMACodes: []string{ma2}, Reason: "重复发码", Operator: "监管局",
})
require.NoError(t, err)
assert.Equal(t, ma, res.PrimaryMACode)
assert.Contains(t, res.MergedMACodes, ma2)
assert.Greater(t, res.MigratedBindings, 0)
// 被合并的 MA 状态为 merged
got, _ := c.QueryContent(ma2)
assert.Equal(t, model.StatusMerged, got.Status)
// 按 fh2 查询应指向主 MA 码
qr, err := c.QueryByHash(fh2)
require.NoError(t, err)
assert.Equal(t, ma, qr.Content.MACode)
})
t.Run("MA拆分_仅监管且按集迁移", func(t *testing.T) {
c := newClient(t)
_, err := c.IssueMA(RoleRegulator, issueReq(ma, ctid, fh))
require.NoError(t, err)
// 非监管不可拆分
_, err = c.SplitMA(RoleCP, model.SplitRequest{
SourceMACode: ma, Splits: []model.SplitTarget{{NewMACode: "MA.156.8531.6101/WD/20260000010", Title: "拆分剧A", Episodes: []int{1, 2}}},
})
assert.ErrorIs(t, err, ErrPermissionDenied)
// 监管拆分
newMA := "MA.156.8531.6101/WD/20260000011"
res, err := c.SplitMA(RoleRegulator, model.SplitRequest{
SourceMACode: ma,
Splits: []model.SplitTarget{{NewMACode: newMA, Title: "拆分剧A", Episodes: []int{1, 2}}},
Reason: "合集拆分",
Operator: "监管局",
})
require.NoError(t, err)
assert.Contains(t, res.NewMACodes, newMA)
assert.Greater(t, res.MigratedBindings, 0)
// 源 MA 状态为 split
got, _ := c.QueryContent(ma)
assert.Equal(t, model.StatusSplit, got.Status)
// 新 MA 可查询
newContent, err := c.QueryContent(newMA)
require.NoError(t, err)
assert.Equal(t, "拆分剧A", newContent.Title)
})
}
// TestMemoryChain_Conformance 让内存实现跑契约套件(始终运行)。
func TestMemoryChain_Conformance(t *testing.T) {
RunClientConformance(t, func(t *testing.T) Client {
return NewMemoryChain()
})
}