txn: add tests for RPC endpoint

This commit is contained in:
Kyle Havlovitz 2018-12-12 05:22:25 -08:00
parent 9f4f673c4d
commit 2408f99cca
4 changed files with 461 additions and 37 deletions

View File

@ -1354,6 +1354,11 @@ func vetDeregisterWithACL(rule acl.Authorizer, subj *structs.DeregisterRequest,
// vetNodeTxnOp applies the given ACL policy to a node transaction operation. // vetNodeTxnOp applies the given ACL policy to a node transaction operation.
func vetNodeTxnOp(op *structs.TxnNodeOp, rule acl.Authorizer) error { func vetNodeTxnOp(op *structs.TxnNodeOp, rule acl.Authorizer) error {
// Fast path if ACLs are not enabled.
if rule == nil {
return nil
}
node := op.Node node := op.Node
// Filtering for GETs is done on the output side. // Filtering for GETs is done on the output side.
@ -1378,7 +1383,7 @@ func vetNodeTxnOp(op *structs.TxnNodeOp, rule acl.Authorizer) error {
} }
} }
if !rule.NodeWrite(node.Node, scope) { if rule != nil && !rule.NodeWrite(node.Node, scope) {
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
} }
@ -1387,6 +1392,11 @@ func vetNodeTxnOp(op *structs.TxnNodeOp, rule acl.Authorizer) error {
// vetServiceTxnOp applies the given ACL policy to a service transaction operation. // vetServiceTxnOp applies the given ACL policy to a service transaction operation.
func vetServiceTxnOp(op *structs.TxnServiceOp, rule acl.Authorizer) error { func vetServiceTxnOp(op *structs.TxnServiceOp, rule acl.Authorizer) error {
// Fast path if ACLs are not enabled.
if rule == nil {
return nil
}
service := op.Service service := op.Service
// Filtering for GETs is done on the output side. // Filtering for GETs is done on the output side.
@ -1416,6 +1426,11 @@ func vetServiceTxnOp(op *structs.TxnServiceOp, rule acl.Authorizer) error {
// vetCheckTxnOp applies the given ACL policy to a check transaction operation. // vetCheckTxnOp applies the given ACL policy to a check transaction operation.
func vetCheckTxnOp(op *structs.TxnCheckOp, rule acl.Authorizer) error { func vetCheckTxnOp(op *structs.TxnCheckOp, rule acl.Authorizer) error {
// Fast path if ACLs are not enabled.
if rule == nil {
return nil
}
// Filtering for GETs is done on the output side. // Filtering for GETs is done on the output side.
if op.Verb == api.CheckGet { if op.Verb == api.CheckGet {
return nil return nil

View File

@ -132,16 +132,19 @@ func (s *Store) txnNode(tx *memdb.Txn, idx uint64, op *structs.TxnNodeOp) (struc
entry, err = getNodeIDTxn(tx, op.Node.ID) entry, err = getNodeIDTxn(tx, op.Node.ID)
case api.NodeSet: case api.NodeSet:
entry = &op.Node
err = s.ensureNodeTxn(tx, idx, &op.Node) err = s.ensureNodeTxn(tx, idx, &op.Node)
if err == nil {
entry, err = getNodeIDTxn(tx, op.Node.ID)
}
case api.NodeCAS: case api.NodeCAS:
var ok bool var ok bool
entry = &op.Node
ok, err = s.ensureNodeCASTxn(tx, idx, &op.Node) ok, err = s.ensureNodeCASTxn(tx, idx, &op.Node)
if !ok && err == nil { if !ok && err == nil {
err = fmt.Errorf("failed to set node %q, index is stale", op.Node.Node) err = fmt.Errorf("failed to set node %q, index is stale", op.Node.Node)
break
} }
entry, err = getNodeIDTxn(tx, op.Node.ID)
case api.NodeDelete: case api.NodeDelete:
err = s.deleteNodeTxn(tx, idx, op.Node.Node) err = s.deleteNodeTxn(tx, idx, op.Node.Node)
@ -187,8 +190,8 @@ func (s *Store) txnService(tx *memdb.Txn, idx uint64, op *structs.TxnServiceOp)
entry, err = s.nodeServiceTxn(tx, op.Node, op.Service.ID) entry, err = s.nodeServiceTxn(tx, op.Node, op.Service.ID)
case api.ServiceSet: case api.ServiceSet:
entry = &op.Service
err = s.ensureServiceTxn(tx, idx, op.Node, &op.Service) err = s.ensureServiceTxn(tx, idx, op.Node, &op.Service)
entry, err = s.nodeServiceTxn(tx, op.Node, op.Service.ID)
case api.ServiceCAS: case api.ServiceCAS:
var ok bool var ok bool
@ -246,8 +249,10 @@ func (s *Store) txnCheck(tx *memdb.Txn, idx uint64, op *structs.TxnCheckOp) (str
} }
case api.CheckSet: case api.CheckSet:
entry = &op.Check err = s.ensureCheckTxn(tx, idx, &op.Check)
err = s.ensureCheckTxn(tx, idx, entry) if err == nil {
_, entry, err = s.nodeCheckTxn(tx, op.Check.Node, op.Check.CheckID)
}
case api.CheckCAS: case api.CheckCAS:
var ok bool var ok bool
@ -255,7 +260,9 @@ func (s *Store) txnCheck(tx *memdb.Txn, idx uint64, op *structs.TxnCheckOp) (str
ok, err = s.ensureCheckCASTxn(tx, idx, entry) ok, err = s.ensureCheckCASTxn(tx, idx, entry)
if !ok && err == nil { if !ok && err == nil {
err = fmt.Errorf("failed to set check %q on node %q, index is stale", entry.CheckID, entry.Node) err = fmt.Errorf("failed to set check %q on node %q, index is stale", entry.CheckID, entry.Node)
break
} }
_, entry, err = s.nodeCheckTxn(tx, op.Check.Node, op.Check.CheckID)
case api.CheckDelete: case api.CheckDelete:
err = s.deleteCheckTxn(tx, idx, op.Check.Node, op.Check.CheckID) err = s.deleteCheckTxn(tx, idx, op.Check.Node, op.Check.CheckID)

View File

@ -55,7 +55,7 @@ func (t *Txn) preCheck(authorizer acl.Authorizer, ops structs.TxnOps) structs.Tx
} }
case op.Service != nil: case op.Service != nil:
service := &op.Service.Service service := &op.Service.Service
if err := servicePreApply(service, authorizer); err != nil { if err := servicePreApply(service, nil); err != nil {
errors = append(errors, &structs.TxnError{ errors = append(errors, &structs.TxnError{
OpIndex: i, OpIndex: i,
What: err.Error(), What: err.Error(),

View File

@ -12,9 +12,49 @@ import (
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/testrpc" "github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/net-rpc-msgpackrpc" "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/pascaldekloe/goe/verify"
"github.com/stretchr/testify/require"
) )
var testTxnRules = `
key "" {
policy = "deny"
}
key "foo" {
policy = "read"
}
key "test" {
policy = "write"
}
key "test/priv" {
policy = "read"
}
service "" {
policy = "deny"
}
service "foo-svc" {
policy = "read"
}
service "test-svc" {
policy = "write"
}
node "" {
policy = "deny"
}
node "foo-node" {
policy = "read"
}
node "test-node" {
policy = "write"
}
`
var testNodeID = "9749a7df-fac5-46b4-8078-32a3d96c59f3"
func TestTxn_CheckNotExists(t *testing.T) { func TestTxn_CheckNotExists(t *testing.T) {
t.Parallel() t.Parallel()
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
@ -101,12 +141,76 @@ func TestTxn_Apply(t *testing.T) {
}, },
}, },
}, },
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeSet,
Node: structs.Node{
ID: types.NodeID(testNodeID),
Node: "foo",
Address: "127.0.0.1",
},
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeGet,
Node: structs.Node{
ID: types.NodeID(testNodeID),
Node: "foo",
},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceSet,
Node: "foo",
Service: structs.NodeService{
ID: "svc-foo",
Service: "svc-foo",
Address: "1.1.1.1",
},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceGet,
Node: "foo",
Service: structs.NodeService{
ID: "svc-foo",
Service: "svc-foo",
},
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckSet,
Check: structs.HealthCheck{
Node: "foo",
CheckID: types.CheckID("check-foo"),
Name: "test",
Status: "passing",
},
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckGet,
Check: structs.HealthCheck{
Node: "foo",
CheckID: types.CheckID("check-foo"),
Name: "test",
},
},
},
}, },
} }
var out structs.TxnResponse var out structs.TxnResponse
if err := msgpackrpc.CallWithCodec(codec, "Txn.Apply", &arg, &out); err != nil { if err := msgpackrpc.CallWithCodec(codec, "Txn.Apply", &arg, &out); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
if len(out.Errors) != 0 {
t.Fatalf("errs: %v", out.Errors)
}
// Verify the state store directly. // Verify the state store directly.
state := s1.fsm.State() state := s1.fsm.State()
@ -122,6 +226,30 @@ func TestTxn_Apply(t *testing.T) {
t.Fatalf("bad: %v", d) t.Fatalf("bad: %v", d)
} }
_, n, err := state.GetNode("foo")
if err != nil {
t.Fatalf("err: %v", err)
}
if n.Node != "foo" || n.Address != "127.0.0.1" {
t.Fatalf("bad: %v", err)
}
_, s, err := state.NodeService("foo", "svc-foo")
if err != nil {
t.Fatalf("err: %v", err)
}
if s.ID != "svc-foo" || s.Address != "1.1.1.1" {
t.Fatalf("bad: %v", err)
}
_, c, err := state.NodeCheck("foo", types.CheckID("check-foo"))
if err != nil {
t.Fatalf("err: %v", err)
}
if c.CheckID != "check-foo" || c.Status != "passing" || c.Name != "test" {
t.Fatalf("bad: %v", err)
}
// Verify the transaction's return value. // Verify the transaction's return value.
expected := structs.TxnResponse{ expected := structs.TxnResponse{
Results: structs.TxnResults{ Results: structs.TxnResults{
@ -147,15 +275,34 @@ func TestTxn_Apply(t *testing.T) {
}, },
}, },
}, },
&structs.TxnResult{
Node: n,
},
&structs.TxnResult{
Node: n,
},
&structs.TxnResult{
Service: s,
},
&structs.TxnResult{
Service: s,
},
&structs.TxnResult{
Check: c,
},
&structs.TxnResult{
Check: c,
},
}, },
} }
if !reflect.DeepEqual(out, expected) { verify.Values(t, "", out, expected)
t.Fatalf("bad %v", out)
}
} }
func TestTxn_Apply_ACLDeny(t *testing.T) { func TestTxn_Apply_ACLDeny(t *testing.T) {
t.Parallel() t.Parallel()
require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -167,15 +314,25 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
testrpc.WaitForLeader(t, s1.RPC, "dc1") testrpc.WaitForLeader(t, s1.RPC, "dc1")
// Put in a key to read back. // Set up some state to read back.
state := s1.fsm.State() state := s1.fsm.State()
d := &structs.DirEntry{ d := &structs.DirEntry{
Key: "nope", Key: "nope",
Value: []byte("hello"), Value: []byte("hello"),
} }
if err := state.KVSSet(1, d); err != nil { require.NoError(state.KVSSet(1, d))
t.Fatalf("err: %v", err)
node := &structs.Node{
ID: types.NodeID(testNodeID),
Node: "nope",
} }
require.NoError(state.EnsureNode(2, node))
svc := structs.NodeService{ID: "nope", Service: "nope", Address: "127.0.0.1"}
require.NoError(state.EnsureService(3, "nope", &svc))
check := structs.HealthCheck{Node: "nope", CheckID: types.CheckID("nope")}
state.EnsureCheck(4, &check)
// Create the ACL. // Create the ACL.
var id string var id string
@ -186,7 +343,7 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
ACL: structs.ACL{ ACL: structs.ACL{
Name: "User token", Name: "User token",
Type: structs.ACLTokenTypeClient, Type: structs.ACLTokenTypeClient,
Rules: testListRules, Rules: testTxnRules,
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
@ -296,6 +453,101 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
}, },
}, },
}, },
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeGet,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeSet,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeCAS,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeDelete,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeDeleteCAS,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceGet,
Node: "foo-node",
Service: svc,
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceSet,
Node: "foo-node",
Service: svc,
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceCAS,
Node: "foo-node",
Service: svc,
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceDelete,
Node: "foo-node",
Service: svc,
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceDeleteCAS,
Node: "foo-node",
Service: svc,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckGet,
Check: check,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckSet,
Check: check,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckCAS,
Check: check,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckDelete,
Check: check,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckDeleteCAS,
Check: check,
},
},
}, },
WriteRequest: structs.WriteRequest{ WriteRequest: structs.WriteRequest{
Token: id, Token: id,
@ -309,20 +561,55 @@ func TestTxn_Apply_ACLDeny(t *testing.T) {
// Verify the transaction's return value. // Verify the transaction's return value.
var expected structs.TxnResponse var expected structs.TxnResponse
for i, op := range arg.Ops { for i, op := range arg.Ops {
switch op.KV.Verb { switch {
case api.KVGet, api.KVGetTree: case op.KV != nil:
// These get filtered but won't result in an error. switch op.KV.Verb {
case api.KVGet, api.KVGetTree:
// These get filtered but won't result in an error.
default: default:
expected.Errors = append(expected.Errors, &structs.TxnError{ expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i, OpIndex: i,
What: acl.ErrPermissionDenied.Error(), What: acl.ErrPermissionDenied.Error(),
}) })
}
case op.Node != nil:
switch op.Node.Verb {
case api.NodeGet:
// These get filtered but won't result in an error.
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
}
case op.Service != nil:
switch op.Service.Verb {
case api.ServiceGet:
// These get filtered but won't result in an error.
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
}
case op.Check != nil:
switch op.Check.Verb {
case api.CheckGet:
// These get filtered but won't result in an error.
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
}
} }
} }
if !reflect.DeepEqual(out, expected) {
t.Fatalf("bad %v", out) verify.Values(t, "", out, expected)
}
} }
func TestTxn_Apply_LockDelay(t *testing.T) { func TestTxn_Apply_LockDelay(t *testing.T) {
@ -413,6 +700,9 @@ func TestTxn_Apply_LockDelay(t *testing.T) {
func TestTxn_Read(t *testing.T) { func TestTxn_Read(t *testing.T) {
t.Parallel() t.Parallel()
require := require.New(t)
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
@ -431,6 +721,19 @@ func TestTxn_Read(t *testing.T) {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
// Put in a node/check/service to read back.
node := &structs.Node{
ID: types.NodeID(testNodeID),
Node: "foo",
}
require.NoError(state.EnsureNode(2, node))
svc := structs.NodeService{ID: "svc-foo", Service: "svc-foo", Address: "127.0.0.1"}
require.NoError(state.EnsureService(3, "foo", &svc))
check := structs.HealthCheck{Node: "foo", CheckID: types.CheckID("check-foo")}
state.EnsureCheck(4, &check)
// Do a super basic request. The state store test covers the details so // Do a super basic request. The state store test covers the details so
// we just need to be sure that the transaction is sent correctly and // we just need to be sure that the transaction is sent correctly and
// the results are converted appropriately. // the results are converted appropriately.
@ -445,6 +748,25 @@ func TestTxn_Read(t *testing.T) {
}, },
}, },
}, },
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeGet,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceGet,
Node: "foo",
Service: svc,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckGet,
Check: check,
},
},
}, },
} }
var out structs.TxnReadResponse var out structs.TxnReadResponse
@ -453,6 +775,8 @@ func TestTxn_Read(t *testing.T) {
} }
// Verify the transaction's return value. // Verify the transaction's return value.
svc.Weights = &structs.Weights{Passing: 1, Warning: 1}
svc.RaftIndex = structs.RaftIndex{CreateIndex: 3, ModifyIndex: 3}
expected := structs.TxnReadResponse{ expected := structs.TxnReadResponse{
TxnResponse: structs.TxnResponse{ TxnResponse: structs.TxnResponse{
Results: structs.TxnResults{ Results: structs.TxnResults{
@ -466,19 +790,29 @@ func TestTxn_Read(t *testing.T) {
}, },
}, },
}, },
&structs.TxnResult{
Node: node,
},
&structs.TxnResult{
Service: &svc,
},
&structs.TxnResult{
Check: &check,
},
}, },
}, },
QueryMeta: structs.QueryMeta{ QueryMeta: structs.QueryMeta{
KnownLeader: true, KnownLeader: true,
}, },
} }
if !reflect.DeepEqual(out, expected) { verify.Values(t, "", out, expected)
t.Fatalf("bad %v", out)
}
} }
func TestTxn_Read_ACLDeny(t *testing.T) { func TestTxn_Read_ACLDeny(t *testing.T) {
t.Parallel() t.Parallel()
require := require.New(t)
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
@ -502,6 +836,19 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
// Put in a node/check/service to read back.
node := &structs.Node{
ID: types.NodeID(testNodeID),
Node: "nope",
}
require.NoError(state.EnsureNode(2, node))
svc := structs.NodeService{ID: "nope", Service: "nope", Address: "127.0.0.1"}
require.NoError(state.EnsureService(3, "nope", &svc))
check := structs.HealthCheck{Node: "nope", CheckID: types.CheckID("nope")}
state.EnsureCheck(4, &check)
// Create the ACL. // Create the ACL.
var id string var id string
{ {
@ -511,7 +858,7 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
ACL: structs.ACL{ ACL: structs.ACL{
Name: "User token", Name: "User token",
Type: structs.ACLTokenTypeClient, Type: structs.ACLTokenTypeClient,
Rules: testListRules, Rules: testTxnRules,
}, },
WriteRequest: structs.WriteRequest{Token: "root"}, WriteRequest: structs.WriteRequest{Token: "root"},
} }
@ -557,6 +904,25 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
}, },
}, },
}, },
&structs.TxnOp{
Node: &structs.TxnNodeOp{
Verb: api.NodeGet,
Node: structs.Node{ID: node.ID, Node: node.Node},
},
},
&structs.TxnOp{
Service: &structs.TxnServiceOp{
Verb: api.ServiceGet,
Node: "foo",
Service: svc,
},
},
&structs.TxnOp{
Check: &structs.TxnCheckOp{
Verb: api.CheckGet,
Check: check,
},
},
}, },
QueryOptions: structs.QueryOptions{ QueryOptions: structs.QueryOptions{
Token: id, Token: id,
@ -574,15 +940,51 @@ func TestTxn_Read_ACLDeny(t *testing.T) {
}, },
} }
for i, op := range arg.Ops { for i, op := range arg.Ops {
switch op.KV.Verb { switch {
case api.KVGet, api.KVGetTree: case op.KV != nil:
// These get filtered but won't result in an error. switch op.KV.Verb {
case api.KVGet, api.KVGetTree:
// These get filtered but won't result in an error.
default: default:
expected.Errors = append(expected.Errors, &structs.TxnError{ expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i, OpIndex: i,
What: acl.ErrPermissionDenied.Error(), What: acl.ErrPermissionDenied.Error(),
}) })
}
case op.Node != nil:
switch op.Node.Verb {
case api.NodeGet:
// These get filtered but won't result in an error.
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
}
case op.Service != nil:
switch op.Service.Verb {
case api.ServiceGet:
// These get filtered but won't result in an error.
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
}
case op.Check != nil:
switch op.Check.Verb {
case api.CheckGet:
// These get filtered but won't result in an error.
default:
expected.Errors = append(expected.Errors, &structs.TxnError{
OpIndex: i,
What: acl.ErrPermissionDenied.Error(),
})
}
} }
} }
if !reflect.DeepEqual(out, expected) { if !reflect.DeepEqual(out, expected) {