269 lines
8.8 KiB
Go
269 lines
8.8 KiB
Go
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{
|
||
CCCode: 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.CCCode)
|
||
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.CCCode)
|
||
|
||
// 按片库文件 ID 查询
|
||
res3, err := c.QueryByLibraryFileID("LIB-001")
|
||
require.NoError(t, err)
|
||
assert.True(t, res3.Found)
|
||
assert.Equal(t, ma, res3.Content.CCCode)
|
||
|
||
// 不存在的 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{
|
||
PrimaryCCCode: ma, SecondaryCCCodes: []string{ma2}, Reason: "重复发码",
|
||
})
|
||
assert.ErrorIs(t, err, ErrPermissionDenied)
|
||
|
||
// 监管合并
|
||
res, err := c.MergeMA(RoleRegulator, model.MergeRequest{
|
||
PrimaryCCCode: ma, SecondaryCCCodes: []string{ma2}, Reason: "重复发码", Operator: "监管局",
|
||
})
|
||
require.NoError(t, err)
|
||
assert.Equal(t, ma, res.PrimaryCCCode)
|
||
assert.Contains(t, res.MergedCCCodes, 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.CCCode)
|
||
})
|
||
|
||
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{
|
||
SourceCCCode: ma, Splits: []model.SplitTarget{{NewCCCode: "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{
|
||
SourceCCCode: ma,
|
||
Splits: []model.SplitTarget{{NewCCCode: newMA, Title: "拆分剧A", Episodes: []int{1, 2}}},
|
||
Reason: "合集拆分",
|
||
Operator: "监管局",
|
||
})
|
||
require.NoError(t, err)
|
||
assert.Contains(t, res.NewCCCodes, 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()
|
||
})
|
||
}
|