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() }) }