Files
grafana/pkg/storage/unified/sql/backend_test.go
2025-05-15 21:36:52 +02:00

859 lines
25 KiB
Go

package sql
import (
"context"
"database/sql/driver"
"errors"
"fmt"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
"github.com/grafana/grafana/pkg/storage/unified/sql/test"
"github.com/grafana/grafana/pkg/util/testutil"
)
var (
errTest = errors.New("things happened")
resKey = &resourcepb.ResourceKey{
Namespace: "ns",
Group: "gr",
Resource: "rs",
Name: "nm",
}
)
type (
Cols = []string // column names
Rows = [][]driver.Value // row values returned
)
type testBackend struct {
*backend
test.TestDBProvider
}
func (b testBackend) ExecWithResult(expectedSQL string, lastInsertID int64, rowsAffected int64) {
b.SQLMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(lastInsertID, rowsAffected))
}
func (b testBackend) ExecWithErr(expectedSQL string, err error) {
b.SQLMock.ExpectExec(expectedSQL).WillReturnError(err)
}
func (b testBackend) QueryWithResult(expectedSQL string, numCols int, rs Rows) {
rows := b.SQLMock.NewRows(make([]string, numCols))
if len(rs) > 0 {
rows = rows.AddRows(rs...)
}
b.SQLMock.ExpectQuery(expectedSQL).WillReturnRows(rows)
}
func (b testBackend) QueryWithErr(expectedSQL string, err error) {
b.SQLMock.ExpectQuery(expectedSQL).WillReturnError(err)
}
func setupBackendTest(t *testing.T) (testBackend, context.Context) {
t.Helper()
ctx := testutil.NewDefaultTestContext(t)
dbp := test.NewDBProviderMatchWords(t)
b, err := NewBackend(BackendOptions{DBProvider: dbp})
require.NoError(t, err)
require.NotNil(t, b)
err = b.Init(ctx)
require.NoError(t, err)
bb, ok := b.(*backend)
require.True(t, ok)
require.NotNil(t, bb)
return testBackend{
backend: bb,
TestDBProvider: dbp,
}, ctx
}
func TestNewBackend(t *testing.T) {
t.Parallel()
t.Run("happy path", func(t *testing.T) {
t.Parallel()
dbp := test.NewDBProviderNopSQL(t)
b, err := NewBackend(BackendOptions{DBProvider: dbp})
require.NoError(t, err)
require.NotNil(t, b)
})
t.Run("no db provider", func(t *testing.T) {
t.Parallel()
b, err := NewBackend(BackendOptions{})
require.Nil(t, b)
require.Error(t, err)
require.ErrorContains(t, err, "no db provider")
})
}
func TestBackend_Init(t *testing.T) {
t.Parallel()
t.Run("happy path", func(t *testing.T) {
t.Parallel()
ctx := testutil.NewDefaultTestContext(t)
dbp := test.NewDBProviderWithPing(t)
b, err := NewBackend(BackendOptions{DBProvider: dbp})
require.NoError(t, err)
require.NotNil(t, b)
dbp.SQLMock.ExpectPing().WillReturnError(nil)
err = b.Init(ctx)
require.NoError(t, err)
// if it isn't idempotent, then it will make a second ping and the
// expectation will fail
err = b.Init(ctx)
require.NoError(t, err, "should be idempotent")
err = b.Stop(ctx)
require.NoError(t, err)
})
t.Run("no db provider", func(t *testing.T) {
t.Parallel()
ctx := testutil.NewDefaultTestContext(t)
dbp := test.TestDBProvider{
Err: errTest,
}
b, err := NewBackend(BackendOptions{DBProvider: dbp})
require.NoError(t, err)
require.NotNil(t, b)
err = b.Init(ctx)
require.Error(t, err)
require.ErrorContains(t, err, "initialize resource DB")
})
t.Run("no dialect for driver", func(t *testing.T) {
t.Parallel()
ctx := testutil.NewDefaultTestContext(t)
mockDB, _, err := sqlmock.New()
require.NoError(t, err)
dbp := test.TestDBProvider{
DB: dbimpl.NewDB(mockDB, "juancarlo"),
}
b, err := NewBackend(BackendOptions{DBProvider: dbp})
require.NoError(t, err)
require.NotNil(t, b)
err = b.Init(ctx)
require.Error(t, err)
require.ErrorContains(t, err, "no dialect for driver")
})
t.Run("database unreachable", func(t *testing.T) {
t.Parallel()
ctx := testutil.NewDefaultTestContext(t)
dbp := test.NewDBProviderWithPing(t)
b, err := NewBackend(BackendOptions{DBProvider: dbp})
require.NoError(t, err)
require.NotNil(t, dbp.DB)
dbp.SQLMock.ExpectPing().WillReturnError(errTest)
err = b.Init(ctx)
require.Error(t, err)
require.ErrorIs(t, err, errTest)
})
}
func TestBackend_IsHealthy(t *testing.T) {
t.Parallel()
ctx := testutil.NewDefaultTestContext(t)
dbp := test.NewDBProviderWithPing(t)
b, err := NewBackend(BackendOptions{DBProvider: dbp})
require.NoError(t, err)
require.NotNil(t, dbp.DB)
dbp.SQLMock.ExpectPing().WillReturnError(nil)
err = b.Init(ctx)
require.NoError(t, err)
dbp.SQLMock.ExpectPing().WillReturnError(nil)
res, err := b.IsHealthy(ctx, nil)
require.NoError(t, err)
require.NotNil(t, res)
dbp.SQLMock.ExpectPing().WillReturnError(errTest)
res, err = b.IsHealthy(ctx, nil)
require.Nil(t, res)
require.Error(t, err)
require.ErrorIs(t, err, errTest)
}
func TestBackend_create(t *testing.T) {
t.Parallel()
meta, err := utils.MetaAccessor(&unstructured.Unstructured{
Object: map[string]any{},
})
require.NoError(t, err)
event := resource.WriteEvent{
Type: resourcepb.WatchEvent_ADDED,
Key: resKey,
Object: meta,
}
t.Run("happy path", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectBegin()
expectSuccessfulResourceVersionExec(t, b.TestDBProvider,
func() { b.ExecWithResult("insert resource", 0, 1) },
func() { b.ExecWithResult("insert resource_history", 0, 1) },
)
b.SQLMock.ExpectCommit()
v, err := b.create(ctx, event)
require.NoError(t, err)
require.Equal(t, int64(200), v)
})
t.Run("resource already exists", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectBegin()
expectSuccessfulResourceVersionExec(t, b.TestDBProvider,
func() { b.ExecWithResult("insert resource", 0, 1) },
func() { b.ExecWithResult("insert resource_history", 0, 1) },
)
b.SQLMock.ExpectCommit()
b.SQLMock.ExpectBegin()
b.SQLMock.ExpectExec("insert resource").WillReturnError(sqlite3.Error{Code: sqlite3.ErrConstraint, ExtendedCode: sqlite3.ErrConstraintUnique})
b.SQLMock.ExpectRollback()
// First we insert the resource successfully. This is what the happy path test does as well.
v, err := b.create(ctx, event)
require.NoError(t, err)
require.Equal(t, int64(200), v)
// Then we try to insert the same resource again. This should fail.
_, err = b.create(ctx, event)
require.ErrorIs(t, err, resource.ErrResourceAlreadyExists)
})
t.Run("error inserting into resource", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectBegin()
b.ExecWithErr("insert resource", errTest)
b.SQLMock.ExpectRollback()
v, err := b.create(ctx, event)
require.Zero(t, v)
require.Error(t, err)
require.ErrorContains(t, err, "insert into resource")
})
t.Run("error inserting into resource_history", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectBegin()
b.ExecWithResult("insert resource", 0, 1)
b.ExecWithErr("insert resource_history", errTest)
b.SQLMock.ExpectRollback()
v, err := b.create(ctx, event)
require.Zero(t, v)
require.Error(t, err)
require.ErrorContains(t, err, "insert into resource history")
})
}
func TestBackend_update(t *testing.T) {
t.Parallel()
meta, err := utils.MetaAccessor(&unstructured.Unstructured{
Object: map[string]any{},
})
require.NoError(t, err)
meta.SetFolder("folderuid")
event := resource.WriteEvent{
Type: resourcepb.WatchEvent_MODIFIED,
Key: resKey,
Object: meta,
}
t.Run("happy path", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectBegin()
expectSuccessfulResourceVersionExec(t, b.TestDBProvider,
func() { b.ExecWithResult("update resource", 0, 1) },
func() { b.ExecWithResult("insert resource_history", 0, 1) },
)
b.SQLMock.ExpectCommit()
v, err := b.update(ctx, event)
require.NoError(t, err)
require.Equal(t, int64(200), v)
})
t.Run("error in first update to resource", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectBegin()
b.ExecWithErr("update resource", errTest)
b.SQLMock.ExpectRollback()
v, err := b.update(ctx, event)
require.Zero(t, v)
require.Error(t, err)
require.ErrorContains(t, err, "resource update")
})
t.Run("error inserting into resource history", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectBegin()
b.ExecWithResult("update resource", 0, 1)
b.ExecWithErr("insert resource_history", errTest)
b.SQLMock.ExpectRollback()
v, err := b.update(ctx, event)
require.Zero(t, v)
require.Error(t, err)
require.ErrorContains(t, err, "insert into resource history")
})
}
func TestBackend_delete(t *testing.T) {
t.Parallel()
meta, err := utils.MetaAccessor(&unstructured.Unstructured{
Object: map[string]any{},
})
require.NoError(t, err)
event := resource.WriteEvent{
Type: resourcepb.WatchEvent_DELETED,
Key: resKey,
Object: meta,
}
t.Run("happy path", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectBegin()
expectSuccessfulResourceVersionExec(t, b.TestDBProvider,
func() { b.ExecWithResult("delete resource", 0, 1) },
func() { b.ExecWithResult("insert resource_history", 0, 1) },
)
b.SQLMock.ExpectCommit()
v, err := b.delete(ctx, event)
require.NoError(t, err)
require.Equal(t, int64(200), v)
})
t.Run("error deleting resource", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectBegin()
b.ExecWithErr("delete resource", errTest)
b.SQLMock.ExpectCommit()
v, err := b.delete(ctx, event)
require.Zero(t, v)
require.Error(t, err)
require.ErrorContains(t, err, "delete resource")
})
t.Run("error inserting into resource history", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectBegin()
b.ExecWithResult("delete resource", 0, 1)
b.ExecWithErr("insert resource_history", errTest)
b.SQLMock.ExpectCommit()
v, err := b.delete(ctx, event)
require.Zero(t, v)
require.Error(t, err)
require.ErrorContains(t, err, "insert into resource history")
})
}
type readHistoryRow struct {
guid string
namespace string
group string
resource string
name string
folder string
resource_version string
value string
}
func TestBackend_ReadResource(t *testing.T) {
t.Parallel()
t.Run("happy path", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
expectedReadRow := readHistoryRow{
guid: "guid",
namespace: "ns",
group: "gr",
resource: "rs",
name: "nm",
folder: "folder",
resource_version: "300",
value: "rv-300",
}
readResource := []string{"guid", "namespace", "group", "resource", "name", "folder", "resource_version", "value"}
b.SQLMock.ExpectBegin()
b.SQLMock.ExpectQuery("SELECT .* FROM resource").
WillReturnRows(sqlmock.NewRows(readResource).
AddRow(
expectedReadRow.guid,
expectedReadRow.namespace,
expectedReadRow.group,
expectedReadRow.resource,
expectedReadRow.name,
expectedReadRow.folder,
expectedReadRow.resource_version,
expectedReadRow.value,
))
b.SQLMock.ExpectCommit()
req := &resourcepb.ReadRequest{
Key: resKey,
}
rps := b.ReadResource(ctx, req)
require.NotNil(t, rps)
require.Equal(t, int64(300), rps.ResourceVersion)
require.Equal(t, "rv-300", string(rps.Value))
require.Equal(t, "folder", rps.Folder)
})
t.Run("no resource found", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectBegin()
b.SQLMock.ExpectQuery("SELECT .* FROM resource").
WillReturnRows(sqlmock.NewRows([]string{}))
b.SQLMock.ExpectCommit()
req := &resourcepb.ReadRequest{
Key: resKey,
}
res := b.ReadResource(ctx, req)
require.NotNil(t, res.Error)
require.Equal(t, res.Error.Code, int32(404))
})
t.Run("with resource version", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
expectedReadRow := readHistoryRow{
guid: "guid",
namespace: "ns",
group: "gr",
resource: "rs",
name: "nm",
folder: "folder",
resource_version: "300",
value: "rv-300",
}
readHistoryColumns := []string{"guid", "namespace", "group", "resource", "name", "folder", "resource_version", "value"}
b.SQLMock.ExpectBegin()
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").
WillReturnRows(sqlmock.NewRows(readHistoryColumns).
AddRow(
expectedReadRow.guid,
expectedReadRow.namespace,
expectedReadRow.group,
expectedReadRow.resource,
expectedReadRow.name,
expectedReadRow.folder,
expectedReadRow.resource_version,
expectedReadRow.value,
))
b.SQLMock.ExpectCommit()
req := &resourcepb.ReadRequest{
Key: resKey,
ResourceVersion: 300,
}
rps := b.ReadResource(ctx, req)
require.NotNil(t, rps)
require.Equal(t, int64(300), rps.ResourceVersion)
require.Equal(t, "rv-300", string(rps.Value))
require.Equal(t, "folder", rps.Folder)
})
t.Run("error reading resource", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").
WillReturnError(errTest)
req := &resourcepb.ReadRequest{
Key: resKey,
}
rps := b.ReadResource(ctx, req)
require.NotNil(t, rps.Error)
})
}
func TestBackend_getHistory(t *testing.T) {
t.Parallel()
// Common setup
key := &resourcepb.ResourceKey{
Namespace: "ns",
Group: "gr",
Resource: "rs",
Name: "nm",
}
rv1, rv2, rv3 := int64(100), int64(200), int64(300)
cols := []string{"guid", "resource_version", "namespace", "group", "resource", "name", "folder", "value"}
tests := []struct {
name string
source resourcepb.ListRequest_Source
versionMatch resourcepb.ResourceVersionMatchV2
resourceVersion int64
expectedVersions []int64
expectedListRv int64
expectedRowsCount int
expectedErr string
expectedLatestDeletionAsMinRV bool
}{
{
name: "with ResourceVersionMatch_NotOlderThan",
source: resourcepb.ListRequest_HISTORY,
versionMatch: resourcepb.ResourceVersionMatchV2_NotOlderThan,
resourceVersion: rv2,
expectedVersions: []int64{rv2, rv3}, // Should be in ASC order due to NotOlderThan
expectedListRv: rv3,
expectedRowsCount: 2,
},
{
name: "with ResourceVersionMatch_NotOlderThan and ResourceVersion=0",
source: resourcepb.ListRequest_HISTORY,
versionMatch: resourcepb.ResourceVersionMatchV2_NotOlderThan,
resourceVersion: 0,
expectedVersions: []int64{rv1, rv2, rv3}, // Should be in ASC order due to NotOlderThan
expectedListRv: rv3,
expectedRowsCount: 3,
expectedLatestDeletionAsMinRV: true,
},
{
name: "with ResourceVersionMatch_Exact",
source: resourcepb.ListRequest_HISTORY,
versionMatch: resourcepb.ResourceVersionMatchV2_Exact,
resourceVersion: rv2,
expectedVersions: []int64{rv2},
expectedListRv: rv3,
expectedRowsCount: 1,
},
{
name: "with ResourceVersionMatch_Unset (default)",
source: resourcepb.ListRequest_HISTORY,
expectedVersions: []int64{rv3, rv2, rv1}, // Should be in DESC order by default
expectedListRv: rv3,
expectedRowsCount: 3,
expectedLatestDeletionAsMinRV: true,
},
{
name: "error with ResourceVersionMatch_Exact and ResourceVersion <= 0",
source: resourcepb.ListRequest_HISTORY,
versionMatch: resourcepb.ResourceVersionMatchV2_Exact,
resourceVersion: 0,
expectedErr: "expecting an explicit resource version query when using Exact matching",
},
{
name: "with ListRequest_TRASH",
source: resourcepb.ListRequest_TRASH,
expectedVersions: []int64{rv3, rv2, rv1}, // Should be in DESC order by default
expectedListRv: rv3,
expectedRowsCount: 3,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
// Build request with appropriate matcher
req := &resourcepb.ListRequest{
Options: &resourcepb.ListOptions{Key: key},
ResourceVersion: tc.resourceVersion,
VersionMatchV2: tc.versionMatch,
Source: tc.source,
}
// Set up mock expectations only if we don't expect an error
if tc.expectedErr == "" {
// Build expected values map
expectedValues := make(map[int64]string)
for _, rv := range tc.expectedVersions {
expectedValues[rv] = fmt.Sprintf("rv-%d", rv)
}
// Callback that tracks returned items
callback := func(iter resource.ListIterator) error {
count := 0
var seenVersions []int64
for iter.Next() {
count++
currentRV := iter.ResourceVersion()
seenVersions = append(seenVersions, currentRV)
expectedValue, ok := expectedValues[currentRV]
require.True(t, ok, "Got unexpected RV: %d", currentRV)
require.Equal(t, expectedValue, string(iter.Value()))
}
require.Equal(t, tc.expectedRowsCount, count)
// Verify the order matches what we expect
require.Equal(t, tc.expectedVersions, seenVersions, "Resource versions returned in incorrect order")
return nil
}
b.SQLMock.ExpectBegin()
// Expect fetch latest RV call
latestRVRows := sqlmock.NewRows([]string{"resource_version", "unix_timestamp"}).
AddRow(rv3, 0)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_version").WillReturnRows(latestRVRows)
if tc.expectedLatestDeletionAsMinRV {
latestHistoryRVRows := sqlmock.NewRows([]string{"resource_version"}).
AddRow(rv1)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").
WillReturnRows(latestHistoryRVRows)
}
// Expect history query
historyRows := sqlmock.NewRows(cols)
for _, rv := range tc.expectedVersions {
historyRows.AddRow(
"guid", // guid
rv, // resource_version
"ns", // namespace
"gr", // group
"rs", // resource
"nm", // name
"folder", // folder
[]byte(fmt.Sprintf("rv-%d", rv)), // value
)
}
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").WillReturnRows(historyRows)
b.SQLMock.ExpectCommit()
// Execute the test
listRv, err := b.getHistory(ctx, req, callback)
require.NoError(t, err)
require.Equal(t, tc.expectedListRv, listRv)
} else {
// For error cases, we use a simple empty callback
callback := func(iter resource.ListIterator) error { return nil }
// Execute the test expecting an error
listRv, err := b.getHistory(ctx, req, callback)
require.Zero(t, listRv)
require.Error(t, err)
require.ErrorContains(t, err, tc.expectedErr)
}
})
}
}
// TestBackend_getHistoryPagination tests the ordering behavior for ResourceVersionMatch_NotOlderThan
// when using pagination, ensuring entries are returned in oldest-to-newest order.
func TestBackend_getHistoryPagination(t *testing.T) {
t.Parallel()
// Common setup
key := &resourcepb.ResourceKey{
Namespace: "ns",
Group: "gr",
Resource: "rs",
Name: "nm",
}
// Create resource versions that will be returned in our test
versions := make([]int64, 10)
for i := range versions {
versions[i] = int64(51 + i)
}
rv51, rv52, rv53, rv54, rv55, rv56, rv57, rv58, rv59, rv60 := versions[0], versions[1], versions[2], versions[3], versions[4], versions[5], versions[6], versions[7], versions[8], versions[9]
t.Run("pagination with NotOlderThan should return entries from oldest to newest", func(t *testing.T) {
b, ctx := setupBackendTest(t)
// Define all pages we want to test
pages := []struct {
versions []int64
token *resource.ContinueToken
}{
{
versions: []int64{rv51, rv52, rv53, rv54},
token: nil,
},
{
versions: []int64{rv55, rv56, rv57, rv58},
token: &resource.ContinueToken{
ResourceVersion: rv54,
StartOffset: 4,
SortAscending: true,
},
},
{
versions: []int64{rv59, rv60},
token: &resource.ContinueToken{
ResourceVersion: rv58,
StartOffset: 8,
SortAscending: true,
},
},
}
var allItems []int64
initialRV := rv51
// Test each page
for _, page := range pages {
req := &resourcepb.ListRequest{
Options: &resourcepb.ListOptions{Key: key},
ResourceVersion: initialRV,
VersionMatchV2: resourcepb.ResourceVersionMatchV2_NotOlderThan,
Source: resourcepb.ListRequest_HISTORY,
Limit: 4,
}
if page.token != nil {
req.NextPageToken = page.token.String()
}
expectedLatestDeletionAsMinRV := false
items := make([]int64, 0)
callback := func(iter resource.ListIterator) error {
for iter.Next() {
items = append(items, iter.ResourceVersion())
}
return nil
}
b.SQLMock.ExpectBegin()
historyRows := setupHistoryTest(b, page.versions, rv60, expectedLatestDeletionAsMinRV)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").WillReturnRows(historyRows)
b.SQLMock.ExpectCommit()
listRv, err := b.getHistory(ctx, req, callback)
require.NoError(t, err)
require.Equal(t, rv60, listRv, "Head version should be the latest resource version (rv60)")
require.Equal(t, page.versions, items, "Items should be in ASC order")
allItems = append(allItems, items...)
}
// Verify complete sequence
expectedAllItems := []int64{rv51, rv52, rv53, rv54, rv55, rv56, rv57, rv58, rv59, rv60}
require.Equal(t, expectedAllItems, allItems)
})
t.Run("pagination with ResourceVersion=0 and NotOlderThan should return entries in ASC order", func(t *testing.T) {
b, ctx := setupBackendTest(t)
req := &resourcepb.ListRequest{
Options: &resourcepb.ListOptions{Key: key},
ResourceVersion: 0,
VersionMatchV2: resourcepb.ResourceVersionMatchV2_NotOlderThan,
Source: resourcepb.ListRequest_HISTORY,
Limit: 4,
}
expectedLatestDeletionAsMinRV := true
// First batch of items we expect, in ASC order (because of NotOlderThan flag)
// Even with ResourceVersion=0, the order is ASC because we use SortAscending=true
expectedVersions := []int64{rv51, rv52, rv53, rv54}
items := make([]int64, 0)
callback := func(iter resource.ListIterator) error {
for iter.Next() {
items = append(items, iter.ResourceVersion())
}
return nil
}
b.SQLMock.ExpectBegin()
historyRows := setupHistoryTest(b, expectedVersions, rv60, expectedLatestDeletionAsMinRV)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").WillReturnRows(historyRows)
b.SQLMock.ExpectCommit()
listRv, err := b.getHistory(ctx, req, callback)
require.NoError(t, err)
require.Equal(t, rv60, listRv, "Head version should be the latest resource version (rv60)")
require.Equal(t, expectedVersions, items, "Items should be in ASC order even with ResourceVersion=0")
})
}
// setupHistoryTest creates the necessary mock expectations for a history test
func setupHistoryTest(b testBackend, resourceVersions []int64, latestRV int64, expectedLatestDeletionAsMinRV4 bool) *sqlmock.Rows {
// Expect fetch latest RV call - set to the highest resource version
latestRVRows := sqlmock.NewRows([]string{"resource_version", "unix_timestamp"}).
AddRow(latestRV, 0)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_version").WillReturnRows(latestRVRows)
if expectedLatestDeletionAsMinRV4 {
latestHistoryRVRows := sqlmock.NewRows([]string{"resource_version"}).
AddRow(latestRV)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").
WillReturnRows(latestHistoryRVRows)
}
// Create the mock rows for the history items
cols := []string{"guid", "resource_version", "namespace", "group", "resource", "name", "folder", "value"}
historyRows := sqlmock.NewRows(cols)
for _, rv := range resourceVersions {
historyRows.AddRow(
"guid", // guid
rv, // resource_version
"ns", // namespace
"gr", // group
"rs", // resource
"nm", // name
"folder", // folder
[]byte(fmt.Sprintf("rv-%d", rv)), // value
)
}
return historyRows
}