etcd/server/etcdserver/txn/txn_test.go
Wei Fu aa97484166 *: enable goimports in verify-lint
Signed-off-by: Wei Fu <fuweid89@gmail.com>
2023-09-21 21:14:09 +08:00

652 lines
16 KiB
Go

// Copyright 2022 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package txn
import (
"context"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
"go.etcd.io/etcd/api/v3/authpb"
pb "go.etcd.io/etcd/api/v3/etcdserverpb"
"go.etcd.io/etcd/pkg/v3/traceutil"
"go.etcd.io/etcd/server/v3/auth"
"go.etcd.io/etcd/server/v3/lease"
"go.etcd.io/etcd/server/v3/storage/backend"
betesting "go.etcd.io/etcd/server/v3/storage/backend/testing"
"go.etcd.io/etcd/server/v3/storage/mvcc"
"go.etcd.io/etcd/server/v3/storage/schema"
)
type testCase struct {
name string
setup testSetup
op *pb.RequestOp
expectError string
}
type testSetup struct {
compactRevision int64
lease int64
key []byte
}
var futureRev int64 = 1000
var rangeTestCases = []testCase{
{
name: "Range with revision 0 should succeed",
op: &pb.RequestOp{
Request: &pb.RequestOp_RequestRange{
RequestRange: &pb.RangeRequest{
Revision: 0,
},
},
},
},
{
name: "Range on future rev should fail",
op: &pb.RequestOp{
Request: &pb.RequestOp_RequestRange{
RequestRange: &pb.RangeRequest{
Revision: futureRev,
},
},
},
expectError: "mvcc: required revision is a future revision",
},
{
name: "Range on compacted rev should fail",
setup: testSetup{compactRevision: 10},
op: &pb.RequestOp{
Request: &pb.RequestOp_RequestRange{
RequestRange: &pb.RangeRequest{
Revision: 9,
},
},
},
expectError: "mvcc: required revision has been compacted",
},
}
var putTestCases = []testCase{
{
name: "Put without lease should succeed",
op: &pb.RequestOp{
Request: &pb.RequestOp_RequestPut{
RequestPut: &pb.PutRequest{},
},
},
},
{
name: "Put with non-existing lease should fail",
op: &pb.RequestOp{
Request: &pb.RequestOp_RequestPut{
RequestPut: &pb.PutRequest{
Lease: 123,
},
},
},
expectError: "lease not found",
},
{
name: "Put with existing lease should succeed",
setup: testSetup{lease: 123},
op: &pb.RequestOp{
Request: &pb.RequestOp_RequestPut{
RequestPut: &pb.PutRequest{
Lease: 123,
},
},
},
},
{
name: "Put with ignore value without previous key should fail",
op: &pb.RequestOp{
Request: &pb.RequestOp_RequestPut{
RequestPut: &pb.PutRequest{
IgnoreValue: true,
},
},
},
expectError: "etcdserver: key not found",
},
{
name: "Put with ignore lease without previous key should fail",
op: &pb.RequestOp{
Request: &pb.RequestOp_RequestPut{
RequestPut: &pb.PutRequest{
IgnoreLease: true,
},
},
},
expectError: "etcdserver: key not found",
},
{
name: "Put with ignore value with previous key should succeeded",
setup: testSetup{key: []byte("ignore-value")},
op: &pb.RequestOp{
Request: &pb.RequestOp_RequestPut{
RequestPut: &pb.PutRequest{
IgnoreValue: true,
Key: []byte("ignore-value"),
},
},
},
},
{
name: "Put with ignore lease with previous key should succeed ",
setup: testSetup{key: []byte("ignore-lease")},
op: &pb.RequestOp{
Request: &pb.RequestOp_RequestPut{
RequestPut: &pb.PutRequest{
IgnoreLease: true,
Key: []byte("ignore-lease"),
},
},
},
},
}
func TestCheckTxn(t *testing.T) {
type txnTestCase struct {
name string
setup testSetup
txn *pb.TxnRequest
expectError string
}
testCases := []txnTestCase{}
for _, tc := range append(rangeTestCases, putTestCases...) {
testCases = append(testCases, txnTestCase{
name: tc.name,
setup: tc.setup,
txn: &pb.TxnRequest{
Success: []*pb.RequestOp{
tc.op,
},
},
expectError: tc.expectError,
})
}
invalidOperation := &pb.RequestOp{
Request: &pb.RequestOp_RequestRange{
RequestRange: &pb.RangeRequest{
Revision: futureRev,
},
},
}
testCases = append(testCases, txnTestCase{
name: "Invalid operation on failed path should succeed",
txn: &pb.TxnRequest{
Failure: []*pb.RequestOp{
invalidOperation,
},
},
})
testCases = append(testCases, txnTestCase{
name: "Invalid operation on subtransaction should fail",
txn: &pb.TxnRequest{
Success: []*pb.RequestOp{
{
Request: &pb.RequestOp_RequestTxn{
RequestTxn: &pb.TxnRequest{
Success: []*pb.RequestOp{
invalidOperation,
},
},
},
},
},
},
expectError: "mvcc: required revision is a future revision",
})
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s, lessor := setup(t, tc.setup)
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
_, _, err := Txn(ctx, zaptest.NewLogger(t), tc.txn, false, s, lessor)
gotErr := ""
if err != nil {
gotErr = err.Error()
}
if gotErr != tc.expectError {
t.Errorf("Error not matching, got %q, expected %q", gotErr, tc.expectError)
}
})
}
}
func TestCheckPut(t *testing.T) {
for _, tc := range putTestCases {
t.Run(tc.name, func(t *testing.T) {
s, lessor := setup(t, tc.setup)
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
_, _, err := Put(ctx, zaptest.NewLogger(t), lessor, s, tc.op.GetRequestPut())
gotErr := ""
if err != nil {
gotErr = err.Error()
}
if gotErr != tc.expectError {
t.Errorf("Error not matching, got %q, expected %q", gotErr, tc.expectError)
}
})
}
}
func TestCheckRange(t *testing.T) {
for _, tc := range rangeTestCases {
t.Run(tc.name, func(t *testing.T) {
s, _ := setup(t, tc.setup)
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
_, _, err := Range(ctx, zaptest.NewLogger(t), s, tc.op.GetRequestRange())
gotErr := ""
if err != nil {
gotErr = err.Error()
}
if gotErr != tc.expectError {
t.Errorf("Error not matching, got %q, expected %q", gotErr, tc.expectError)
}
})
}
}
func setup(t *testing.T, setup testSetup) (mvcc.KV, lease.Lessor) {
b, _ := betesting.NewDefaultTmpBackend(t)
t.Cleanup(func() {
betesting.Close(t, b)
})
lessor := &lease.FakeLessor{LeaseSet: map[lease.LeaseID]struct{}{}}
s := mvcc.NewStore(zaptest.NewLogger(t), b, lessor, mvcc.StoreConfig{})
t.Cleanup(func() {
s.Close()
})
if setup.compactRevision != 0 {
for i := 0; int64(i) < setup.compactRevision; i++ {
s.Put([]byte("a"), []byte("b"), 0)
}
s.Compact(traceutil.TODO(), setup.compactRevision)
}
if setup.lease != 0 {
lessor.Grant(lease.LeaseID(setup.lease), 0)
}
if len(setup.key) != 0 {
s.Put(setup.key, []byte("b"), 0)
}
return s, lessor
}
func TestReadonlyTxnError(t *testing.T) {
b, _ := betesting.NewDefaultTmpBackend(t)
defer betesting.Close(t, b)
s := mvcc.NewStore(zaptest.NewLogger(t), b, &lease.FakeLessor{}, mvcc.StoreConfig{})
defer s.Close()
// setup cancelled context
ctx, cancel := context.WithCancel(context.TODO())
cancel()
// put some data to prevent early termination in rangeKeys
// we are expecting failure on cancelled context check
s.Put([]byte("foo"), []byte("bar"), lease.NoLease)
txn := &pb.TxnRequest{
Success: []*pb.RequestOp{
{
Request: &pb.RequestOp_RequestRange{
RequestRange: &pb.RangeRequest{
Key: []byte("foo"),
},
},
},
},
}
_, _, err := Txn(ctx, zaptest.NewLogger(t), txn, false, s, &lease.FakeLessor{})
if err == nil || !strings.Contains(err.Error(), "applyTxn: failed Range: rangeKeys: context cancelled: context canceled") {
t.Fatalf("Expected context canceled error, got %v", err)
}
}
func TestWriteTxnPanic(t *testing.T) {
b, _ := betesting.NewDefaultTmpBackend(t)
defer betesting.Close(t, b)
s := mvcc.NewStore(zaptest.NewLogger(t), b, &lease.FakeLessor{}, mvcc.StoreConfig{})
defer s.Close()
// setup cancelled context
ctx, cancel := context.WithCancel(context.TODO())
cancel()
// write txn that puts some data and then fails in range due to cancelled context
txn := &pb.TxnRequest{
Success: []*pb.RequestOp{
{
Request: &pb.RequestOp_RequestPut{
RequestPut: &pb.PutRequest{
Key: []byte("foo"),
Value: []byte("bar"),
},
},
},
{
Request: &pb.RequestOp_RequestRange{
RequestRange: &pb.RangeRequest{
Key: []byte("foo"),
},
},
},
},
}
assert.Panics(t, func() { Txn(ctx, zaptest.NewLogger(t), txn, false, s, &lease.FakeLessor{}) }, "Expected panic in Txn with writes")
}
func TestCheckTxnAuth(t *testing.T) {
be, _ := betesting.NewDefaultTmpBackend(t)
defer betesting.Close(t, be)
as := setupAuth(t, be)
tests := []struct {
name string
txnRequest *pb.TxnRequest
err error
}{
{
name: "Out of range compare is unauthorized",
txnRequest: &pb.TxnRequest{
Compare: []*pb.Compare{outOfRangeCompare},
},
err: auth.ErrPermissionDenied,
},
{
name: "In range compare is authorized",
txnRequest: &pb.TxnRequest{
Compare: []*pb.Compare{inRangeCompare},
},
err: nil,
},
{
name: "Nil request range is always authorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{nilRequestRange},
},
err: nil,
},
{
name: "Range request in range is authorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{inRangeRequestRange},
Failure: []*pb.RequestOp{inRangeRequestRange},
},
err: nil,
},
{
name: "Range request out of range success case is unauthorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{outOfRangeRequestRange},
Failure: []*pb.RequestOp{inRangeRequestRange},
},
err: auth.ErrPermissionDenied,
},
{
name: "Range request out of range failure case is unauthorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{inRangeRequestRange},
Failure: []*pb.RequestOp{outOfRangeRequestRange},
},
err: auth.ErrPermissionDenied,
},
{
name: "Nil Put request is always authorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{nilRequestPut},
},
err: nil,
},
{
name: "Put request in range in authorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{inRangeRequestPut},
Failure: []*pb.RequestOp{inRangeRequestPut},
},
err: nil,
},
{
name: "Put request out of range success case is unauthorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{outOfRangeRequestPut},
Failure: []*pb.RequestOp{inRangeRequestPut},
},
err: auth.ErrPermissionDenied,
},
{
name: "Put request out of range failure case is unauthorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{inRangeRequestPut},
Failure: []*pb.RequestOp{outOfRangeRequestPut},
},
err: auth.ErrPermissionDenied,
},
{
name: "Nil delete request is authorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{nilRequestDeleteRange},
},
err: nil,
},
{
name: "Delete range request in range is authorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{inRangeRequestDeleteRange},
Failure: []*pb.RequestOp{inRangeRequestDeleteRange},
},
err: nil,
},
{
name: "Delete range request out of range success case is unauthorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{outOfRangeRequestDeleteRange},
Failure: []*pb.RequestOp{inRangeRequestDeleteRange},
},
err: auth.ErrPermissionDenied,
},
{
name: "Delete range request out of range failure case is unauthorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{inRangeRequestDeleteRange},
Failure: []*pb.RequestOp{outOfRangeRequestDeleteRange},
},
err: auth.ErrPermissionDenied,
},
{
name: "Delete range request out of range and PrevKv false success case is unauthorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{outOfRangeRequestDeleteRangeKvFalse},
Failure: []*pb.RequestOp{inRangeRequestDeleteRange},
},
err: auth.ErrPermissionDenied,
},
{
name: "Delete range request out of range and PrevKv false failure case is unauthorized",
txnRequest: &pb.TxnRequest{
Success: []*pb.RequestOp{inRangeRequestDeleteRange},
Failure: []*pb.RequestOp{outOfRangeRequestDeleteRangeKvFalse},
},
err: auth.ErrPermissionDenied,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := CheckTxnAuth(as, &auth.AuthInfo{Username: "foo", Revision: 8}, tt.txnRequest)
assert.Equal(t, tt.err, err)
})
}
}
// CheckTxnAuth test setup.
func setupAuth(t *testing.T, be backend.Backend) auth.AuthStore {
lg := zaptest.NewLogger(t)
simpleTokenTTLDefault := 300 * time.Second
tokenTypeSimple := "simple"
dummyIndexWaiter := func(index uint64) <-chan struct{} {
ch := make(chan struct{}, 1)
go func() {
ch <- struct{}{}
}()
return ch
}
tp, _ := auth.NewTokenProvider(zaptest.NewLogger(t), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault)
as := auth.NewAuthStore(lg, schema.NewAuthBackend(lg, be), tp, 4)
// create "root" user and "foo" user with limited range
_, err := as.RoleAdd(&pb.AuthRoleAddRequest{Name: "root"})
require.NoError(t, err)
_, err = as.RoleAdd(&pb.AuthRoleAddRequest{Name: "rw"})
require.NoError(t, err)
_, err = as.RoleGrantPermission(&pb.AuthRoleGrantPermissionRequest{
Name: "rw",
Perm: &authpb.Permission{
PermType: authpb.READWRITE,
Key: []byte("foo"),
RangeEnd: []byte("zoo"),
},
})
require.NoError(t, err)
_, err = as.UserAdd(&pb.AuthUserAddRequest{Name: "root", Password: "foo"})
require.NoError(t, err)
_, err = as.UserAdd(&pb.AuthUserAddRequest{Name: "foo", Password: "foo"})
require.NoError(t, err)
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "root", Role: "root"})
require.NoError(t, err)
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo", Role: "rw"})
require.NoError(t, err)
err = as.AuthEnable()
require.NoError(t, err)
return as
}
// CheckTxnAuth variables setup.
var (
inRangeCompare = &pb.Compare{
Key: []byte("foo"),
RangeEnd: []byte("zoo"),
}
outOfRangeCompare = &pb.Compare{
Key: []byte("boo"),
RangeEnd: []byte("zoo"),
}
nilRequestPut = &pb.RequestOp{
Request: &pb.RequestOp_RequestPut{
RequestPut: nil,
},
}
inRangeRequestPut = &pb.RequestOp{
Request: &pb.RequestOp_RequestPut{
RequestPut: &pb.PutRequest{
Key: []byte("foo"),
},
},
}
outOfRangeRequestPut = &pb.RequestOp{
Request: &pb.RequestOp_RequestPut{
RequestPut: &pb.PutRequest{
Key: []byte("boo"),
},
},
}
nilRequestRange = &pb.RequestOp{
Request: &pb.RequestOp_RequestRange{
RequestRange: nil,
},
}
inRangeRequestRange = &pb.RequestOp{
Request: &pb.RequestOp_RequestRange{
RequestRange: &pb.RangeRequest{
Key: []byte("foo"),
RangeEnd: []byte("zoo"),
},
},
}
outOfRangeRequestRange = &pb.RequestOp{
Request: &pb.RequestOp_RequestRange{
RequestRange: &pb.RangeRequest{
Key: []byte("boo"),
RangeEnd: []byte("zoo"),
},
},
}
nilRequestDeleteRange = &pb.RequestOp{
Request: &pb.RequestOp_RequestDeleteRange{
RequestDeleteRange: nil,
},
}
inRangeRequestDeleteRange = &pb.RequestOp{
Request: &pb.RequestOp_RequestDeleteRange{
RequestDeleteRange: &pb.DeleteRangeRequest{
Key: []byte("foo"),
RangeEnd: []byte("zoo"),
PrevKv: true,
},
},
}
outOfRangeRequestDeleteRange = &pb.RequestOp{
Request: &pb.RequestOp_RequestDeleteRange{
RequestDeleteRange: &pb.DeleteRangeRequest{
Key: []byte("boo"),
RangeEnd: []byte("zoo"),
PrevKv: true,
},
},
}
outOfRangeRequestDeleteRangeKvFalse = &pb.RequestOp{
Request: &pb.RequestOp_RequestDeleteRange{
RequestDeleteRange: &pb.DeleteRangeRequest{
Key: []byte("boo"),
RangeEnd: []byte("zoo"),
PrevKv: false,
},
},
}
)