2023-12-13 15:06:39 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
|
|
|
|
package index
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/hashicorp/consul/internal/controller/cache/index/indexmock"
|
|
|
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
|
|
|
"github.com/hashicorp/consul/proto/private/prototest"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
)
|
|
|
|
|
|
|
|
var fakeTestError = errors.New("fake test error")
|
|
|
|
|
|
|
|
func TestTxn(t *testing.T) {
|
|
|
|
suite.Run(t, new(txnSuite))
|
|
|
|
}
|
|
|
|
|
|
|
|
type txnSuite struct {
|
|
|
|
suite.Suite
|
|
|
|
|
|
|
|
indexer *indexmock.SingleIndexer
|
2024-02-09 18:00:21 +00:00
|
|
|
index *IndexedData
|
2023-12-13 15:06:39 +00:00
|
|
|
|
|
|
|
r1 *pbresource.Resource
|
|
|
|
r2 *pbresource.Resource
|
|
|
|
r11 *pbresource.Resource
|
|
|
|
r123 *pbresource.Resource
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) SetupTest() {
|
|
|
|
suite.indexer = indexmock.NewSingleIndexer(suite.T())
|
2024-02-09 18:00:21 +00:00
|
|
|
suite.index = New("test", suite.indexer, IndexRequired).IndexedData()
|
2023-12-13 15:06:39 +00:00
|
|
|
|
|
|
|
suite.r1 = testResource("r1")
|
|
|
|
suite.r2 = testResource("r2")
|
|
|
|
suite.r11 = testResource("r11")
|
|
|
|
suite.r123 = testResource("r123")
|
|
|
|
|
|
|
|
exp := suite.indexer.EXPECT()
|
|
|
|
exp.FromResource(suite.r1).Return(true, PrefixIndexFromRefOrID(suite.r1.Id), nil).Once()
|
|
|
|
exp.FromResource(suite.r2).Return(true, PrefixIndexFromRefOrID(suite.r2.Id), nil).Once()
|
|
|
|
exp.FromResource(suite.r11).Return(true, PrefixIndexFromRefOrID(suite.r11.Id), nil).Once()
|
|
|
|
exp.FromResource(suite.r123).Return(true, PrefixIndexFromRefOrID(suite.r123.Id), nil).Once()
|
|
|
|
|
|
|
|
txn := suite.index.Txn()
|
|
|
|
txn.Insert(suite.r1)
|
|
|
|
txn.Insert(suite.r2)
|
|
|
|
txn.Insert(suite.r11)
|
|
|
|
txn.Insert(suite.r123)
|
|
|
|
txn.Commit()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestGet() {
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromArgs(suite.r1.Id).
|
|
|
|
RunAndReturn(PrefixReferenceOrIDFromArgs).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
actual, err := suite.index.Txn().Get(suite.r1.Id)
|
|
|
|
require.NoError(suite.T(), err)
|
|
|
|
require.NotNil(suite.T(), actual)
|
|
|
|
prototest.AssertDeepEqual(suite.T(), suite.r1, actual)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestGet_NotFound() {
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromArgs(suite.r1.Id).
|
|
|
|
Return(nil, nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
actual, err := suite.index.Txn().Get(suite.r1.Id)
|
|
|
|
require.NoError(suite.T(), err)
|
|
|
|
require.Nil(suite.T(), actual)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestGet_Error() {
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromArgs(suite.r1.Id).
|
|
|
|
Return(nil, fakeTestError).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
actual, err := suite.index.Txn().Get(suite.r1.Id)
|
|
|
|
require.ErrorIs(suite.T(), err, fakeTestError)
|
|
|
|
require.Nil(suite.T(), actual)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestListIterator() {
|
|
|
|
refQuery := &pbresource.Reference{
|
|
|
|
Type: testResourceType,
|
|
|
|
}
|
|
|
|
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromArgs(refQuery).
|
|
|
|
// Calculate a prefix based query for use with the ListIterator
|
|
|
|
RunAndReturn(PrefixReferenceOrIDFromArgs).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
iter, err := suite.index.Txn().ListIterator(refQuery)
|
|
|
|
require.NoError(suite.T(), err)
|
|
|
|
|
|
|
|
r := iter.Next()
|
|
|
|
require.NotNil(suite.T(), r)
|
|
|
|
prototest.AssertDeepEqual(suite.T(), suite.r1, r)
|
|
|
|
|
|
|
|
r = iter.Next()
|
|
|
|
require.NotNil(suite.T(), r)
|
|
|
|
prototest.AssertDeepEqual(suite.T(), suite.r11, r)
|
|
|
|
|
|
|
|
r = iter.Next()
|
|
|
|
require.NotNil(suite.T(), r)
|
|
|
|
prototest.AssertDeepEqual(suite.T(), suite.r123, r)
|
|
|
|
|
|
|
|
r = iter.Next()
|
|
|
|
require.NotNil(suite.T(), r)
|
|
|
|
prototest.AssertDeepEqual(suite.T(), suite.r2, r)
|
|
|
|
|
|
|
|
r = iter.Next()
|
|
|
|
require.Nil(suite.T(), r)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestListIterator_Error() {
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
// abusing the mock to create a shortened index for us.
|
|
|
|
FromArgs("sentinel").
|
|
|
|
Return(nil, fakeTestError).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
iter, err := suite.index.Txn().ListIterator("sentinel")
|
|
|
|
require.ErrorIs(suite.T(), err, fakeTestError)
|
|
|
|
require.Nil(suite.T(), iter)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestParentsIterator() {
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
// abusing the mock to create a shortened index for us.
|
|
|
|
FromArgs(suite.r123.Id).
|
|
|
|
RunAndReturn(ReferenceOrIDFromArgs).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
iter, err := suite.index.Txn().ParentsIterator(suite.r123.Id)
|
|
|
|
require.NoError(suite.T(), err)
|
|
|
|
|
|
|
|
r := iter.Next()
|
|
|
|
require.NotNil(suite.T(), r)
|
|
|
|
prototest.AssertDeepEqual(suite.T(), suite.r1, r)
|
|
|
|
|
|
|
|
r = iter.Next()
|
|
|
|
require.NotNil(suite.T(), r)
|
|
|
|
prototest.AssertDeepEqual(suite.T(), suite.r123, r)
|
|
|
|
|
|
|
|
r = iter.Next()
|
|
|
|
require.Nil(suite.T(), r)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestParentsIterator_Error() {
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
// abusing the mock to create a shortened index for us.
|
|
|
|
FromArgs("sentinel").
|
|
|
|
Return(nil, fakeTestError).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
iter, err := suite.index.Txn().ParentsIterator("sentinel")
|
|
|
|
require.ErrorIs(suite.T(), err, fakeTestError)
|
|
|
|
require.Nil(suite.T(), iter)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestInsert_MissingRequiredIndex() {
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(suite.r1).
|
|
|
|
Return(false, nil, nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
err := suite.index.Txn().Insert(suite.r1)
|
|
|
|
require.Error(suite.T(), err)
|
|
|
|
require.ErrorIs(suite.T(), err, MissingRequiredIndexError{Name: "test"})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestInsert_IndexError() {
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(suite.r1).
|
|
|
|
Return(false, nil, fakeTestError).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
err := suite.index.Txn().Insert(suite.r1)
|
|
|
|
require.Error(suite.T(), err)
|
|
|
|
require.ErrorIs(suite.T(), err, fakeTestError)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestInsert_UpdateInternalSlice() {
|
|
|
|
// So if you look closely this is going to insert the newR
|
|
|
|
// resource but is calculating the index value of the r1
|
|
|
|
// resource. This is done to exercise the insertion functionality
|
|
|
|
// where indexes are non-unique and at each leaf in the radix
|
|
|
|
// tree we must keep a list of items.
|
|
|
|
newR := testResource("newR")
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(newR).
|
|
|
|
Return(true, PrefixIndexFromRefOrID(suite.r1.Id), nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
// here we are setting up the expecation for re-inserting r1
|
|
|
|
newR1 := testResource("r1")
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(newR1).
|
|
|
|
Return(true, PrefixIndexFromRefOrID(newR1.Id), nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
// Actually index the resource
|
|
|
|
txn := suite.index.Txn()
|
|
|
|
require.NoError(suite.T(), txn.Insert(newR))
|
|
|
|
require.NoError(suite.T(), txn.Insert(newR1))
|
|
|
|
txn.Commit()
|
|
|
|
|
|
|
|
// No validate that the insertions worked correctly.
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromArgs(newR1.Id).
|
|
|
|
RunAndReturn(PrefixReferenceOrIDFromArgs).
|
|
|
|
Once()
|
|
|
|
iter, err := suite.index.Txn().ListIterator(newR1.Id)
|
|
|
|
require.NoError(suite.T(), err)
|
|
|
|
|
|
|
|
var resources []*pbresource.Resource
|
|
|
|
for r := iter.Next(); r != nil; r = iter.Next() {
|
|
|
|
resources = append(resources, r)
|
|
|
|
}
|
|
|
|
|
|
|
|
prototest.AssertElementsMatch(suite.T(), []*pbresource.Resource{newR, newR1, suite.r11, suite.r123}, resources)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestDelete() {
|
|
|
|
// expected to index the resource during deletion
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(suite.r1).
|
|
|
|
Return(true, PrefixIndexFromRefOrID(suite.r1.Id), nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
// perform the deletion
|
|
|
|
txn := suite.index.Txn()
|
|
|
|
require.NoError(suite.T(), txn.Delete(suite.r1))
|
|
|
|
txn.Commit()
|
|
|
|
|
|
|
|
// expect to index the ID during the query
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromArgs(suite.r1.Id).
|
|
|
|
RunAndReturn(PrefixReferenceOrIDFromArgs).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
// ensure that the deletion worked
|
|
|
|
res, err := suite.index.Txn().Get(suite.r1.Id)
|
|
|
|
require.NoError(suite.T(), err)
|
|
|
|
require.Nil(suite.T(), res)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestDelete_NotFound() {
|
|
|
|
res := testResource("foo")
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(res).
|
|
|
|
Return(true, PrefixIndexFromRefOrID(res.Id), nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
// attempt the deletion
|
|
|
|
txn := suite.index.Txn()
|
|
|
|
require.NoError(suite.T(), txn.Delete(res))
|
|
|
|
txn.Commit()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestDelete_IdxPresentValNotFound() {
|
|
|
|
// The index holds a radix tree that points to a slice of resources.
|
|
|
|
// A slice is used to account for non-unique indexes. This test case
|
|
|
|
// is meant to specifically exercise the case where the radix leaf
|
|
|
|
// node exists but a resource with an equivalent ID is not present
|
|
|
|
// in the slice.
|
|
|
|
|
|
|
|
// Calculating the index from the r1 resource will ensure that a
|
|
|
|
// radix leaf exists but since newR was never inserted this should
|
|
|
|
// exercise the case where the resource is not found within the slice
|
|
|
|
newR := testResource("newR")
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(newR).
|
|
|
|
Return(true, PrefixIndexFromRefOrID(suite.r1.Id), nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
txn := suite.index.Txn()
|
|
|
|
require.NoError(suite.T(), txn.Delete(newR))
|
|
|
|
txn.Commit()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestDelete_SliceModifications() {
|
|
|
|
commonIndex := []byte("fake\x00")
|
|
|
|
|
|
|
|
injectResource := func(name string) *pbresource.Resource {
|
|
|
|
r := testResource(name)
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(r).
|
|
|
|
Return(true, commonIndex, nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
txn := suite.index.Txn()
|
|
|
|
require.NoError(suite.T(), txn.Insert(r))
|
|
|
|
txn.Commit()
|
|
|
|
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
fr1 := injectResource("fr1")
|
|
|
|
fr2 := injectResource("fr2")
|
|
|
|
fr3 := injectResource("fr3")
|
|
|
|
fr4 := injectResource("fr4")
|
|
|
|
fr5 := injectResource("fr5")
|
|
|
|
|
|
|
|
txn := suite.index.Txn()
|
|
|
|
|
|
|
|
// excercise deletion of the first slice element
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(fr1).
|
|
|
|
Return(true, commonIndex, nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
require.NoError(suite.T(), txn.Delete(fr1))
|
|
|
|
|
|
|
|
// excercise deletion of the last slice element
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(fr5).
|
|
|
|
Return(true, commonIndex, nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
require.NoError(suite.T(), txn.Delete(fr5))
|
|
|
|
|
|
|
|
// excercise deletion from the middle of the list
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(fr3).
|
|
|
|
Return(true, commonIndex, nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
require.NoError(suite.T(), txn.Delete(fr3))
|
|
|
|
|
|
|
|
txn.Commit()
|
|
|
|
|
|
|
|
// no verify that only fr2 and fr4 exist
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromArgs(fr2.Id).
|
|
|
|
Return(commonIndex, nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
iter, err := suite.index.Txn().ListIterator(fr2.Id)
|
|
|
|
require.NoError(suite.T(), err)
|
|
|
|
require.NotNil(suite.T(), iter)
|
|
|
|
|
|
|
|
var resources []*pbresource.Resource
|
|
|
|
for r := iter.Next(); r != nil; r = iter.Next() {
|
|
|
|
resources = append(resources, r)
|
|
|
|
}
|
|
|
|
|
|
|
|
prototest.AssertElementsMatch(suite.T(), []*pbresource.Resource{fr2, fr4}, resources)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestDelete_MissingRequiredIndex() {
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(suite.r1).
|
|
|
|
Return(false, nil, nil).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
err := suite.index.Txn().Delete(suite.r1)
|
|
|
|
require.Error(suite.T(), err)
|
|
|
|
require.ErrorIs(suite.T(), err, MissingRequiredIndexError{Name: "test"})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (suite *txnSuite) TestDelete_IndexError() {
|
|
|
|
suite.indexer.EXPECT().
|
|
|
|
FromResource(suite.r1).
|
|
|
|
Return(false, nil, fakeTestError).
|
|
|
|
Once()
|
|
|
|
|
|
|
|
err := suite.index.Txn().Delete(suite.r1)
|
|
|
|
require.Error(suite.T(), err)
|
|
|
|
require.ErrorIs(suite.T(), err, fakeTestError)
|
|
|
|
}
|