166f460d57
- 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 契约套件均通过,行为一致
161 lines
5.3 KiB
Go
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()
|
|
})
|
|
}
|