Files
MAcode/tcs-iptv/internal/chain/conformance_test.go
T
selfrelease 166f460d57 feat(chain): ChainMaker 真实链接入脚手架(build tag 隔离)+ 契约测试
- internal/chain/chainmaker.go [//go:build chainmaker]: ChainMakerClient 适配器骨架,
  实现 chain.Client 全部方法到合约 Invoke/Query,按角色证书做链上鉴权,错误映射回标准错误
- internal/chain/chainmaker_stub.go [//go:build !chainmaker]: 占位构造函数,
  保证默认构建不依赖 SDK、主工程始终可编译
- contracts/tcs_registry/registry.go: 补齐合约方法
  RegisterHashBinding/VerifyEpisodeHash/ListEpisodes/HashExists/RecordVersionChange/
  RevokeEpisode/Restore/RestoreEpisode/SetContentStatus/QueryMappings/ListContents
  并增加集级哈希/映射/版本计数索引 KV 设计
- config: TCS_CHAIN_BACKEND=memory|pg|chainmaker + TCS_CHAINMAKER_SDK_CONF 开关
- cmd/api-svc: newChain 按 backend 选择,chainmaker 失败逐级降级 pg 到内存
- internal/chain/conformance_test.go: chain.Client 契约测试套件,双实现共用
  MemoryChain 默认跑;PersistentChain 经 TCS_TEST_PG_DSN;ChainMaker 经 -tags 与 env
- 验证: 默认 build/vet/test 全绿;MemoryChain 与 PersistentChain 契约套件均通过,行为一致
2026-06-14 20:47:21 +08:00

161 lines
5.3 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{
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)
})
}
// TestMemoryChain_Conformance 让内存实现跑契约套件(始终运行)。
func TestMemoryChain_Conformance(t *testing.T) {
RunClientConformance(t, func(t *testing.T) Client {
return NewMemoryChain()
})
}