consul/internal/controller/cache/cache_test.go

354 lines
11 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cache
import (
"encoding/binary"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/hashicorp/consul/internal/controller/cache/index"
"github.com/hashicorp/consul/internal/controller/cache/indexers"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/resourcetest"
"github.com/hashicorp/consul/proto-public/pbresource"
pbdemo "github.com/hashicorp/consul/proto/private/pbdemo/v1"
"github.com/hashicorp/consul/proto/private/prototest"
)
const (
errQueryName = "error-query"
okQueryName = "ok-query"
)
func namePrefixIndexer() *index.Index {
return indexers.DecodedSingleIndexer(
"name_prefix",
index.SingleValueFromArgs(func(value string) ([]byte, error) {
return []byte(value), nil
}),
func(r *resource.DecodedResource[*pbdemo.Album]) (bool, []byte, error) {
return true, []byte(r.Data.Name), nil
})
}
func releaseYearIndexer() *index.Index {
return indexers.DecodedSingleIndexer(
"year",
index.SingleValueFromArgs(func(value int32) ([]byte, error) {
var b index.Builder
binary.Write(&b, binary.BigEndian, value)
return b.Bytes(), nil
}),
func(r *resource.DecodedResource[*pbdemo.Album]) (bool, []byte, error) {
var b index.Builder
binary.Write(&b, binary.BigEndian, r.Data.YearOfRelease)
return true, b.Bytes(), nil
})
}
func tracksIndexer() *index.Index {
return indexers.DecodedMultiIndexer(
"tracks",
index.SingleValueFromOneOrTwoArgs(func(value string, prefix bool) ([]byte, error) {
var b index.Builder
if prefix {
b.Raw([]byte(value))
} else {
b.String(value)
}
return b.Bytes(), nil
}),
func(r *resource.DecodedResource[*pbdemo.Album]) (bool, [][]byte, error) {
indexes := make([][]byte, len(r.Data.Tracks))
for idx, track := range r.Data.Tracks {
var b index.Builder
b.String(track)
indexes[idx] = b.Bytes()
}
return true, indexes, nil
})
}
func requireCacheIndex(t *testing.T, c *cache, rtype *pbresource.Type, indexes ...string) {
t.Helper()
indices, err := c.getTypeIndices(rtype)
require.NoError(t, err)
require.NotNil(t, indices)
for _, name := range indexes {
index, err := indices.getIndex(name)
require.NoError(t, err)
require.NotNil(t, index)
}
}
func TestCacheAddType(t *testing.T) {
c := newCache()
c.AddType(pbdemo.AlbumType)
// Adding a type will ensure that the `id` index exists
requireCacheIndex(t, c, pbdemo.AlbumType, "id")
}
func TestCacheAddIndex(t *testing.T) {
c := newCache()
require.NoError(t, c.AddIndex(pbdemo.AlbumType, releaseYearIndexer()))
require.NoError(t, c.AddIndex(pbdemo.AlbumType, tracksIndexer()))
// Adding indexes should also have the side effect of ensuring that the `id` index exists
requireCacheIndex(t, c, pbdemo.AlbumType, "id", "year", "tracks")
}
func TestCacheAddIndex_Duplicate(t *testing.T) {
c := newCache()
require.NoError(t, c.AddIndex(pbdemo.AlbumType, releaseYearIndexer()))
// should get an error due to a duplicate index name
require.Error(t, c.AddIndex(pbdemo.AlbumType, releaseYearIndexer()))
}
func noopQuery(_ ReadOnlyCache, _ ...any) (ResourceIterator, error) {
return nil, nil
}
func errQuery(_ ReadOnlyCache, _ ...any) (ResourceIterator, error) {
return nil, injectedError
}
func TestCacheAddQuery(t *testing.T) {
c := newCache()
require.NoError(t, c.AddQuery("foo", noopQuery))
require.NoError(t, c.AddQuery("bar", errQuery))
fn, found := c.queries["foo"]
require.True(t, found)
iter, err := fn(c)
require.NoError(t, err)
require.Nil(t, iter)
fn, found = c.queries["bar"]
require.True(t, found)
iter, err = fn(c)
require.ErrorIs(t, err, injectedError)
require.Nil(t, iter)
}
func TestCacheAddQuery_Duplicate(t *testing.T) {
c := newCache()
require.NoError(t, c.AddQuery("foo", noopQuery))
// should get an error due to a duplicate query name
require.Error(t, c.AddQuery("foo", noopQuery))
}
func TestCacheAddQuery_Nil(t *testing.T) {
c := newCache()
require.ErrorIs(t, c.AddQuery("foo", nil), QueryRequired)
}
func TestQuery_NotFound(t *testing.T) {
c := newCache()
iter, err := c.Query("foo", "something")
require.ErrorIs(t, err, QueryNotFoundError{"foo"})
require.Nil(t, iter)
}
func TestCache(t *testing.T) {
suite.Run(t, &cacheSuite{})
}
type cacheSuite struct {
suite.Suite
c Cache
album1 *pbresource.Resource
album2 *pbresource.Resource
album3 *pbresource.Resource
album4 *pbresource.Resource
}
func (suite *cacheSuite) SetupTest() {
suite.c = New()
require.NoError(suite.T(), suite.c.AddIndex(pbdemo.AlbumType, namePrefixIndexer()))
require.NoError(suite.T(), suite.c.AddQuery(okQueryName, func(c ReadOnlyCache, args ...any) (ResourceIterator, error) {
return c.ParentsIterator(pbdemo.AlbumType, "name_prefix", args...)
}))
require.NoError(suite.T(), suite.c.AddIndex(pbdemo.AlbumType, releaseYearIndexer()))
require.NoError(suite.T(), suite.c.AddQuery(errQueryName, errQuery))
require.NoError(suite.T(), suite.c.AddIndex(pbdemo.AlbumType, tracksIndexer()))
suite.album1 = resourcetest.Resource(pbdemo.AlbumType, "one").
WithTenancy(resource.DefaultNamespacedTenancy()).
WithData(suite.T(), &pbdemo.Album{
Name: "one",
YearOfRelease: 2023,
Tracks: []string{"foo", "bar", "baz"},
}).
Build()
suite.album2 = resourcetest.Resource(pbdemo.AlbumType, "two").
WithTenancy(resource.DefaultNamespacedTenancy()).
WithData(suite.T(), &pbdemo.Album{
Name: "two",
YearOfRelease: 2023,
Tracks: []string{"fangorn", "zoo"},
}).
Build()
suite.album3 = resourcetest.Resource(pbdemo.AlbumType, "third").
WithTenancy(resource.DefaultNamespacedTenancy()).
WithData(suite.T(), &pbdemo.Album{
Name: "foo",
YearOfRelease: 2022,
Tracks: []string{"blah", "something", "else"},
}).
Build()
suite.album4 = resourcetest.Resource(pbdemo.AlbumType, "four").
WithTenancy(resource.DefaultNamespacedTenancy()).
WithData(suite.T(), &pbdemo.Album{
Name: "food",
YearOfRelease: 2020,
Tracks: []string{"nothing", "food"},
}).
Build()
require.NoError(suite.T(), suite.c.Insert(suite.album1))
require.NoError(suite.T(), suite.c.Insert(suite.album2))
require.NoError(suite.T(), suite.c.Insert(suite.album3))
require.NoError(suite.T(), suite.c.Insert(suite.album4))
}
func (suite *cacheSuite) TestGet() {
res, err := suite.c.Get(pbdemo.AlbumType, "id", suite.album1.Id)
require.NoError(suite.T(), err)
prototest.AssertDeepEqual(suite.T(), suite.album1, res)
res, err = suite.c.Get(pbdemo.AlbumType, "year", int32(2022))
require.NoError(suite.T(), err)
prototest.AssertDeepEqual(suite.T(), suite.album3, res)
res, err = suite.c.Get(pbdemo.AlbumType, "tracks", "fangorn")
require.NoError(suite.T(), err)
prototest.AssertDeepEqual(suite.T(), suite.album2, res)
}
func (suite *cacheSuite) TestGet_NilType() {
res, err := suite.c.Get(nil, "id", suite.album1.Id)
require.Error(suite.T(), err)
require.ErrorIs(suite.T(), err, TypeUnspecifiedError)
require.Nil(suite.T(), res)
}
func (suite *cacheSuite) TestGet_UncachedType() {
res, err := suite.c.Get(pbdemo.ArtistType, "id", suite.album1.Id)
require.Error(suite.T(), err)
require.ErrorIs(suite.T(), err, TypeNotIndexedError)
require.Nil(suite.T(), res)
}
func (suite *cacheSuite) TestGet_IndexNotFound() {
res, err := suite.c.Get(pbdemo.AlbumType, "blah", suite.album1.Id)
require.Error(suite.T(), err)
require.ErrorIs(suite.T(), err, IndexNotFoundError{name: "blah"})
require.Nil(suite.T(), res)
}
func (suite *cacheSuite) TestList() {
resources, err := suite.c.List(pbdemo.AlbumType, "year", int32(2023))
require.NoError(suite.T(), err)
prototest.AssertElementsMatch(suite.T(), []*pbresource.Resource{suite.album1, suite.album2}, resources)
resources, err = suite.c.List(pbdemo.AlbumType, "tracks", "f", true)
require.NoError(suite.T(), err)
prototest.AssertElementsMatch(suite.T(), []*pbresource.Resource{suite.album1, suite.album2, suite.album4}, resources)
}
func (suite *cacheSuite) TestList_NilType() {
res, err := suite.c.List(nil, "id", suite.album1.Id)
require.Error(suite.T(), err)
require.ErrorIs(suite.T(), err, TypeUnspecifiedError)
require.Nil(suite.T(), res)
}
func (suite *cacheSuite) TestList_UncachedType() {
res, err := suite.c.List(pbdemo.ArtistType, "id", suite.album1.Id)
require.Error(suite.T(), err)
require.ErrorIs(suite.T(), err, TypeNotIndexedError)
require.Nil(suite.T(), res)
}
func (suite *cacheSuite) TestList_IndexNotFound() {
res, err := suite.c.List(pbdemo.AlbumType, "blah", suite.album1.Id)
require.Error(suite.T(), err)
require.ErrorIs(suite.T(), err, IndexNotFoundError{name: "blah"})
require.Nil(suite.T(), res)
}
func (suite *cacheSuite) TestParents() {
resources, err := suite.c.Parents(pbdemo.AlbumType, "name_prefix", "food")
require.NoError(suite.T(), err)
prototest.AssertElementsMatch(suite.T(), []*pbresource.Resource{suite.album3, suite.album4}, resources)
}
func (suite *cacheSuite) TestQuery() {
resources, err := expandIterator(suite.c.Query(okQueryName, "food"))
require.NoError(suite.T(), err)
prototest.AssertElementsMatch(suite.T(), []*pbresource.Resource{suite.album3, suite.album4}, resources)
}
func (suite *cacheSuite) TestParents_NilType() {
res, err := suite.c.Parents(nil, "id", suite.album1.Id)
require.Error(suite.T(), err)
require.ErrorIs(suite.T(), err, TypeUnspecifiedError)
require.Nil(suite.T(), res)
}
func (suite *cacheSuite) TestParents_UncachedType() {
res, err := suite.c.Parents(pbdemo.ArtistType, "id", suite.album1.Id)
require.Error(suite.T(), err)
require.ErrorIs(suite.T(), err, TypeNotIndexedError)
require.Nil(suite.T(), res)
}
func (suite *cacheSuite) TestParents_IndexNotFound() {
res, err := suite.c.Parents(pbdemo.AlbumType, "blah", suite.album1.Id)
require.Error(suite.T(), err)
require.ErrorIs(suite.T(), err, IndexNotFoundError{name: "blah"})
require.Nil(suite.T(), res)
}
func (suite *cacheSuite) TestInsert_UncachedType() {
err := suite.c.Insert(resourcetest.Resource(pbdemo.ArtistType, "blah").Build())
require.Error(suite.T(), err)
require.ErrorIs(suite.T(), err, TypeNotIndexedError)
}
func (suite *cacheSuite) TestDelete() {
err := suite.c.Delete(suite.album1)
require.NoError(suite.T(), err)
res, err := suite.c.Get(pbdemo.AlbumType, "id", suite.album1.Id)
require.NoError(suite.T(), err)
require.Nil(suite.T(), res)
resources, err := suite.c.List(pbdemo.AlbumType, "year", int32(2023))
require.NoError(suite.T(), err)
prototest.AssertElementsMatch(suite.T(), []*pbresource.Resource{suite.album2}, resources)
resources, err = suite.c.Parents(pbdemo.AlbumType, "name_prefix", "onesie")
require.NoError(suite.T(), err)
require.Nil(suite.T(), resources)
}
func (suite *cacheSuite) TestDelete_UncachedType() {
err := suite.c.Delete(resourcetest.Resource(pbdemo.ArtistType, "blah").Build())
require.Error(suite.T(), err)
require.ErrorIs(suite.T(), err, TypeNotIndexedError)
}