Commit 4d649c54 authored by Alessandro Sorniotti's avatar Alessandro Sorniotti Committed by Matthias Neugschwandtner
Browse files

[FAB-11548] Change TransactionValidator interface



The TransactionValidator needs to be adapted to match that of the default 
validation plugin. This way, state-based validation can be integrated 
inside the validation logic, leaving the default plugin implementation 
clean and readable.

Change-Id: Ia17763bef11f5c285f68810472ede1501a7e8090
Signed-off-by: default avatarAlessandro Sorniotti <ale.linux@sopit.net>
Signed-off-by: default avatarMatthias Neugschwandtner <eug@zurich.ibm.com>
parent 923afecd
......@@ -33,7 +33,7 @@ type DefaultValidation struct {
//go:generate mockery -dir . -name TransactionValidator -case underscore -output mocks/
type TransactionValidator interface {
Validate(txData []byte, policy []byte) commonerrors.TxValidationError
Validate(block *common.Block, namespace string, txPosition int, actionPosition int, policy []byte) commonerrors.TxValidationError
}
func (v *DefaultValidation) Validate(block *common.Block, namespace string, txPosition int, actionPosition int, contextData ...validation.ContextDatum) error {
......@@ -54,7 +54,7 @@ func (v *DefaultValidation) Validate(block *common.Block, namespace string, txPo
if block.Header == nil {
return errors.Errorf("no block header")
}
err := v.TxValidator.Validate(block.Data.Data[txPosition], serializedPolicy.Bytes())
err := v.TxValidator.Validate(block, namespace, txPosition, actionPosition, serializedPolicy.Bytes())
logger.Debugf("block %d, namespace: %s, tx %d validation results is: %v", block.Header.Number, namespace, txPosition, err)
return convertErrorTypeOrPanic(err)
}
......
......@@ -51,23 +51,23 @@ func TestErrorConversion(t *testing.T) {
// Scenario I: An error that isn't *commonerrors.ExecutionFailureError or *commonerrors.VSCCEndorsementPolicyError
// should cause a panic
validator.On("Validate", mock.Anything, mock.Anything).Return(errors.New("bla bla")).Once()
validator.On("Validate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("bla bla")).Once()
assert.Panics(t, func() {
validation.Validate(block, "", 0, 0, txvalidator.SerializedPolicy("policy"))
})
// Scenario II: Non execution errors are returned as is
validator.On("Validate", mock.Anything, mock.Anything).Return(&commonerrors.VSCCEndorsementPolicyError{Err: errors.New("foo")}).Once()
validator.On("Validate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&commonerrors.VSCCEndorsementPolicyError{Err: errors.New("foo")}).Once()
err := validation.Validate(block, "", 0, 0, txvalidator.SerializedPolicy("policy"))
assert.Equal(t, (&commonerrors.VSCCEndorsementPolicyError{Err: errors.New("foo")}).Error(), err.Error())
// Scenario III: Execution errors are converted to the plugin error type
validator.On("Validate", mock.Anything, mock.Anything).Return(&commonerrors.VSCCExecutionFailureError{Err: errors.New("bar")}).Once()
validator.On("Validate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&commonerrors.VSCCExecutionFailureError{Err: errors.New("bar")}).Once()
err = validation.Validate(block, "", 0, 0, txvalidator.SerializedPolicy("policy"))
assert.Equal(t, &ExecutionFailureError{Reason: "bar"}, err)
// Scenario IV: No errors are forwarded
validator.On("Validate", mock.Anything, mock.Anything).Return(nil).Once()
validator.On("Validate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
assert.NoError(t, validation.Validate(block, "", 0, 0, txvalidator.SerializedPolicy("policy")))
}
......
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import common "github.com/hyperledger/fabric/protos/common"
import errors "github.com/hyperledger/fabric/common/errors"
import mock "github.com/stretchr/testify/mock"
......@@ -9,13 +10,13 @@ type TransactionValidator struct {
mock.Mock
}
// Validate provides a mock function with given fields: txData, policy
func (_m *TransactionValidator) Validate(txData []byte, policy []byte) errors.TxValidationError {
ret := _m.Called(txData, policy)
// Validate provides a mock function with given fields: block, namespace, txPosition, actionPosition, policy
func (_m *TransactionValidator) Validate(block *common.Block, namespace string, txPosition int, actionPosition int, policy []byte) errors.TxValidationError {
ret := _m.Called(block, namespace, txPosition, actionPosition, policy)
var r0 errors.TxValidationError
if rf, ok := ret.Get(0).(func([]byte, []byte) errors.TxValidationError); ok {
r0 = rf(txData, policy)
if rf, ok := ret.Get(0).(func(*common.Block, string, int, int, []byte) errors.TxValidationError); ok {
r0 = rf(block, namespace, txPosition, actionPosition, policy)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(errors.TxValidationError)
......
......@@ -75,9 +75,15 @@ type ValidatorOneValidSignature struct {
// Validate validates the given envelope corresponding to a transaction with an endorsement
// policy as given in its serialized form
func (vscc *ValidatorOneValidSignature) Validate(envelopeBytes []byte, policyBytes []byte) commonerrors.TxValidationError {
func (vscc *ValidatorOneValidSignature) Validate(
block *common.Block,
namespace string,
txPosition int,
actionPosition int,
policyBytes []byte,
) commonerrors.TxValidationError {
// get the envelope...
env, err := utils.GetEnvelopeFromBlock(envelopeBytes)
env, err := utils.GetEnvelopeFromBlock(block.Data.Data[txPosition])
if err != nil {
logger.Errorf("VSCC error: GetEnvelope failed, err %s", err)
return policyErr(err)
......@@ -108,46 +114,38 @@ func (vscc *ValidatorOneValidSignature) Validate(envelopeBytes []byte, policyByt
return policyErr(err)
}
// loop through each of the actions within
for _, act := range tx.Actions {
cap, err := utils.GetChaincodeActionPayload(act.Payload)
if err != nil {
logger.Errorf("VSCC error: GetChaincodeActionPayload failed, err %s", err)
return policyErr(err)
}
cap, err := utils.GetChaincodeActionPayload(tx.Actions[actionPosition].Payload)
if err != nil {
logger.Errorf("VSCC error: GetChaincodeActionPayload failed, err %s", err)
return policyErr(err)
}
signatureSet, err := vscc.deduplicateIdentity(cap)
if err != nil {
return policyErr(err)
}
signatureSet, err := vscc.deduplicateIdentity(cap)
if err != nil {
return policyErr(err)
}
// evaluate the signature set against the policy
err = vscc.policyEvaluator.Evaluate(policyBytes, signatureSet)
if err != nil {
logger.Warningf("Endorsement policy failure for transaction txid=%s, err: %s", chdr.GetTxId(), err.Error())
if len(signatureSet) < len(cap.Action.Endorsements) {
// Warning: duplicated identities exist, endorsement failure might be cause by this reason
return policyErr(errors.New(DUPLICATED_IDENTITY_ERROR))
}
return policyErr(fmt.Errorf("VSCC error: endorsement policy failure, err: %s", err))
// evaluate the signature set against the policy
err = vscc.policyEvaluator.Evaluate(policyBytes, signatureSet)
if err != nil {
logger.Warningf("Endorsement policy failure for transaction txid=%s, err: %s", chdr.GetTxId(), err.Error())
if len(signatureSet) < len(cap.Action.Endorsements) {
// Warning: duplicated identities exist, endorsement failure might be cause by this reason
return policyErr(errors.New(DUPLICATED_IDENTITY_ERROR))
}
return policyErr(fmt.Errorf("VSCC error: endorsement policy failure, err: %s", err))
}
hdrExt, err := utils.GetChaincodeHeaderExtension(payl.Header)
// do some extra validation that is specific to lscc
if namespace == "lscc" {
logger.Debugf("VSCC info: doing special validation for LSCC")
err := vscc.ValidateLSCCInvocation(chdr.ChannelId, env, cap, payl, vscc.capabilities)
if err != nil {
logger.Errorf("VSCC error: GetChaincodeHeaderExtension failed, err %s", err)
return policyErr(err)
}
// do some extra validation that is specific to lscc
if hdrExt.ChaincodeId.Name == "lscc" {
logger.Debugf("VSCC info: doing special validation for LSCC")
err := vscc.ValidateLSCCInvocation(chdr.ChannelId, env, cap, payl, vscc.capabilities)
if err != nil {
logger.Errorf("VSCC error: ValidateLSCCInvocation failed, err %s", err)
return err
}
logger.Errorf("VSCC error: ValidateLSCCInvocation failed, err %s", err)
return err
}
}
return nil
}
......
......@@ -381,16 +381,19 @@ func TestInvoke(t *testing.T) {
// broken Envelope
var err error
err = v.Validate([]byte("a"), []byte("a"))
b := &common.Block{Data: &common.BlockData{Data: [][]byte{[]byte("a")}}}
err = v.Validate(b, "foo", 0, 0, []byte("a"))
assert.Error(t, err)
// (still) broken Envelope
err = v.Validate(utils.MarshalOrPanic(&common.Envelope{Payload: []byte("barf")}), []byte("a"))
b = &common.Block{Data: &common.BlockData{Data: [][]byte{utils.MarshalOrPanic(&common.Envelope{Payload: []byte("barf")})}}}
err = v.Validate(b, "foo", 0, 0, []byte("a"))
assert.Error(t, err)
// (still) broken Envelope
b := utils.MarshalOrPanic(&common.Envelope{Payload: utils.MarshalOrPanic(&common.Payload{Header: &common.Header{ChannelHeader: []byte("barf")}})})
err = v.Validate(b, []byte("a"))
e := utils.MarshalOrPanic(&common.Envelope{Payload: utils.MarshalOrPanic(&common.Payload{Header: &common.Header{ChannelHeader: []byte("barf")}})})
b = &common.Block{Data: &common.BlockData{Data: [][]byte{e}}}
err = v.Validate(b, "foo", 0, 0, []byte("a"))
assert.Error(t, err)
tx, err := createTx(false)
......@@ -404,7 +407,8 @@ func TestInvoke(t *testing.T) {
}
// broken policy
err = v.Validate(envBytes, []byte("barf"))
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "foo", 0, 0, []byte("barf"))
assert.Error(t, err)
policy, err := getSignedByMSPMemberPolicy(mspid)
......@@ -413,17 +417,20 @@ func TestInvoke(t *testing.T) {
}
// broken type
b = utils.MarshalOrPanic(&common.Envelope{Payload: utils.MarshalOrPanic(&common.Payload{Header: &common.Header{ChannelHeader: utils.MarshalOrPanic(&common.ChannelHeader{Type: int32(common.HeaderType_ORDERER_TRANSACTION)})}})})
err = v.Validate(b, policy)
e = utils.MarshalOrPanic(&common.Envelope{Payload: utils.MarshalOrPanic(&common.Payload{Header: &common.Header{ChannelHeader: utils.MarshalOrPanic(&common.ChannelHeader{Type: int32(common.HeaderType_ORDERER_TRANSACTION)})}})})
b = &common.Block{Data: &common.BlockData{Data: [][]byte{e}}}
err = v.Validate(b, "foo", 0, 0, policy)
assert.Error(t, err)
// broken tx payload
b = utils.MarshalOrPanic(&common.Envelope{Payload: utils.MarshalOrPanic(&common.Payload{Header: &common.Header{ChannelHeader: utils.MarshalOrPanic(&common.ChannelHeader{Type: int32(common.HeaderType_ORDERER_TRANSACTION)})}})})
err = v.Validate(b, policy)
e = utils.MarshalOrPanic(&common.Envelope{Payload: utils.MarshalOrPanic(&common.Payload{Header: &common.Header{ChannelHeader: utils.MarshalOrPanic(&common.ChannelHeader{Type: int32(common.HeaderType_ORDERER_TRANSACTION)})}})})
b = &common.Block{Data: &common.BlockData{Data: [][]byte{e}}}
err = v.Validate(b, "foo", 0, 0, policy)
assert.Error(t, err)
// good path: signed by the right MSP
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "foo", 0, 0, policy)
assert.NoError(t, err)
// bad path: signed by the wrong MSP
......@@ -432,7 +439,7 @@ func TestInvoke(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(b, policy)
err = v.Validate(b, "foo", 0, 0, policy)
assert.Error(t, err)
// bad path: signed by duplicated MSP identity
......@@ -448,7 +455,8 @@ func TestInvoke(t *testing.T) {
if err != nil {
t.Fatalf("GetBytesEnvelope returned err %s", err)
}
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "foo", 0, 0, policy)
assert.Error(t, err)
}
......@@ -508,7 +516,8 @@ func TestRWSetTooBig(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
}
......@@ -549,7 +558,8 @@ func TestValidateDeployFail(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
/************************/
......@@ -578,7 +588,8 @@ func TestValidateDeployFail(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
/**********************/
......@@ -604,7 +615,8 @@ func TestValidateDeployFail(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
/**********************/
......@@ -630,7 +642,8 @@ func TestValidateDeployFail(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
/***********************/
......@@ -656,7 +669,8 @@ func TestValidateDeployFail(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
/*************/
......@@ -679,7 +693,8 @@ func TestValidateDeployFail(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
/********************/
......@@ -704,7 +719,8 @@ func TestValidateDeployFail(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
/**********************/
......@@ -729,7 +745,8 @@ func TestValidateDeployFail(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
/************************/
......@@ -765,7 +782,8 @@ func TestValidateDeployFail(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
}
......@@ -829,7 +847,8 @@ func TestAlreadyDeployed(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
bl := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(bl, "lscc", 0, 0, policy)
assert.Error(t, err)
}
......@@ -862,7 +881,8 @@ func TestValidateDeployNoLedger(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
}
......@@ -905,7 +925,8 @@ func TestValidateDeployOK(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "foo", 0, 0, policy)
assert.NoError(t, err)
}
......@@ -978,7 +999,8 @@ func TestValidateDeployWithCollection(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "foo", 0, 0, policy)
assert.NoError(t, err)
// Test 2: Deploy the chaincode with duplicate collection configs --> no error as the
......@@ -1001,7 +1023,8 @@ func TestValidateDeployWithCollection(t *testing.T) {
t.Fatalf("GetBytesEnvelope returned err %s", err)
}
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "foo", 0, 0, policy)
assert.NoError(t, err)
// Test 3: Once the V1_2Validation is enabled, validation should fail due to duplicate collection configs
......@@ -1027,7 +1050,8 @@ func TestValidateDeployWithCollection(t *testing.T) {
t.FailNow()
}
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "foo", 0, 0, policy)
}
func TestValidateDeployWithPolicies(t *testing.T) {
......@@ -1071,7 +1095,8 @@ func TestValidateDeployWithPolicies(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "foo", 0, 0, policy)
assert.NoError(t, err)
/********************************************/
......@@ -1097,7 +1122,8 @@ func TestValidateDeployWithPolicies(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
}
......@@ -1138,7 +1164,8 @@ func TestInvalidUpgrade(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
b := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(b, "lscc", 0, 0, policy)
assert.Error(t, err)
}
......@@ -1204,7 +1231,8 @@ func TestValidateUpgradeOK(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
bl := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(bl, "foo", 0, 0, policy)
assert.NoError(t, err)
}
......@@ -1268,7 +1296,8 @@ func TestInvalidateUpgradeBadVersion(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
bl := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(bl, "lscc", 0, 0, policy)
assert.Error(t, err)
}
......@@ -1371,7 +1400,8 @@ func validateUpgradeWithCollection(t *testing.T, V1_2Validation bool) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
bl := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(bl, "lscc", 0, 0, policy)
if V1_2Validation {
assert.NoError(t, err)
} else {
......@@ -1406,7 +1436,8 @@ func validateUpgradeWithCollection(t *testing.T, V1_2Validation bool) {
t.Fatalf("GetBytesEnvelope returned err %s", err)
}
err = v.Validate(envBytes, policy)
bl = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(bl, "lscc", 0, 0, policy)
assert.Error(t, err, "Some existing collection configurations are missing in the new collection configuration package")
ccver = "3"
......@@ -1432,7 +1463,8 @@ func validateUpgradeWithCollection(t *testing.T, V1_2Validation bool) {
}
args = [][]byte{[]byte("dv"), envBytes, policy, ccpBytes}
err = v.Validate(envBytes, policy)
bl = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(bl, "lscc", 0, 0, policy)
assert.Error(t, err, "existing collection named mycollection2 is missing in the new collection configuration package")
ccver = "3"
......@@ -1456,7 +1488,8 @@ func validateUpgradeWithCollection(t *testing.T, V1_2Validation bool) {
t.Fatalf("GetBytesEnvelope returned err %s", err)
}
err = v.Validate(envBytes, policy)
bl = &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(bl, "lscc", 0, 0, policy)
assert.NoError(t, err)
}
}
......@@ -1533,7 +1566,8 @@ func TestValidateUpgradeWithPoliciesOK(t *testing.T) {
}
args = [][]byte{[]byte("dv"), envBytes, policy}
err = v.Validate(envBytes, policy)
bl := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(bl, "foo", 0, 0, policy)
assert.NoError(t, err)
}
......@@ -1629,10 +1663,12 @@ func validateUpgradeWithNewFailAllIP(t *testing.T, v11capability, expecterr bool
// execute the upgrade tx
if expecterr {
err = v.Validate(envBytes, policy)
bl := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(bl, "lscc", 0, 0, policy)
assert.Error(t, err)
} else {
err = v.Validate(envBytes, policy)
bl := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(bl, "lscc", 0, 0, policy)
assert.NoError(t, err)
}
}
......@@ -1701,7 +1737,8 @@ func TestValidateUpgradeWithPoliciesFail(t *testing.T) {
t.Fatalf("failed getting policy, err %s", err)
}
err = v.Validate(envBytes, policy)
bl := &common.Block{Data: &common.BlockData{Data: [][]byte{envBytes}}}
err = v.Validate(bl, "lscc", 0, 0, policy)
assert.Error(t, err)
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment