| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | package sql | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	"database/sql/driver" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 	"testing" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	sqlmock "github.com/DATA-DOG/go-sqlmock" | 
					
						
							| 
									
										
										
										
											2025-03-26 21:44:44 +08:00
										 |  |  | 	"github.com/mattn/go-sqlite3" | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-12 19:52:04 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/apimachinery/utils" | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/storage/unified/resource" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/storage/unified/sql/test" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/util/testutil" | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 	"github.com/stretchr/testify/require" | 
					
						
							|  |  |  | 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var ( | 
					
						
							|  |  |  | 	errTest = errors.New("things happened") | 
					
						
							|  |  |  | 	resKey  = &resource.ResourceKey{ | 
					
						
							|  |  |  | 		Namespace: "ns", | 
					
						
							|  |  |  | 		Group:     "gr", | 
					
						
							|  |  |  | 		Resource:  "rs", | 
					
						
							|  |  |  | 		Name:      "nm", | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | type ( | 
					
						
							|  |  |  | 	Cols = []string         // column names
 | 
					
						
							|  |  |  | 	Rows = [][]driver.Value // row values returned
 | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type testBackend struct { | 
					
						
							|  |  |  | 	*backend | 
					
						
							|  |  |  | 	test.TestDBProvider | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-11 17:11:33 +08:00
										 |  |  | func (b testBackend) ExecWithResult(expectedSQL string, lastInsertID int64, rowsAffected int64) { | 
					
						
							|  |  |  | 	b.SQLMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(lastInsertID, rowsAffected)) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | func (b testBackend) ExecWithErr(expectedSQL string, err error) { | 
					
						
							|  |  |  | 	b.SQLMock.ExpectExec(expectedSQL).WillReturnError(err) | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 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) | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | func (b testBackend) QueryWithErr(expectedSQL string, err error) { | 
					
						
							|  |  |  | 	b.SQLMock.ExpectQuery(expectedSQL).WillReturnError(err) | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | func setupBackendTest(t *testing.T) (testBackend, context.Context) { | 
					
						
							|  |  |  | 	t.Helper() | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	ctx := testutil.NewDefaultTestContext(t) | 
					
						
							|  |  |  | 	dbp := test.NewDBProviderMatchWords(t) | 
					
						
							| 
									
										
										
										
											2025-01-23 18:34:48 +08:00
										 |  |  | 	b, err := NewBackend(BackendOptions{DBProvider: dbp}) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	require.NoError(t, err) | 
					
						
							|  |  |  | 	require.NotNil(t, b) | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	err = b.Init(ctx) | 
					
						
							|  |  |  | 	require.NoError(t, err) | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	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) | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	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") | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 	}) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 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) | 
					
						
							| 
									
										
										
										
											2025-01-23 18:34:48 +08:00
										 |  |  | 		b, err := NewBackend(BackendOptions{DBProvider: dbp}) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 		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) | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	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") | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	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") | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	t.Run("database unreachable", func(t *testing.T) { | 
					
						
							|  |  |  | 		t.Parallel() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		ctx := testutil.NewDefaultTestContext(t) | 
					
						
							|  |  |  | 		dbp := test.NewDBProviderWithPing(t) | 
					
						
							| 
									
										
										
										
											2025-01-23 18:34:48 +08:00
										 |  |  | 		b, err := NewBackend(BackendOptions{DBProvider: dbp}) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 		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) | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | func TestBackend_IsHealthy(t *testing.T) { | 
					
						
							|  |  |  | 	t.Parallel() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	ctx := testutil.NewDefaultTestContext(t) | 
					
						
							|  |  |  | 	dbp := test.NewDBProviderWithPing(t) | 
					
						
							| 
									
										
										
										
											2025-01-23 18:34:48 +08:00
										 |  |  | 	b, err := NewBackend(BackendOptions{DBProvider: dbp}) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	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() | 
					
						
							| 
									
										
										
										
											2024-11-12 19:52:04 +08:00
										 |  |  | 	meta, err := utils.MetaAccessor(&unstructured.Unstructured{ | 
					
						
							|  |  |  | 		Object: map[string]any{}, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 	require.NoError(t, err) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	event := resource.WriteEvent{ | 
					
						
							| 
									
										
										
										
											2024-11-12 19:52:04 +08:00
										 |  |  | 		Type:   resource.WatchEvent_ADDED, | 
					
						
							|  |  |  | 		Key:    resKey, | 
					
						
							|  |  |  | 		Object: meta, | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	t.Run("happy path", func(t *testing.T) { | 
					
						
							|  |  |  | 		t.Parallel() | 
					
						
							|  |  |  | 		b, ctx := setupBackendTest(t) | 
					
						
							|  |  |  | 		b.SQLMock.ExpectBegin() | 
					
						
							| 
									
										
										
										
											2025-03-13 16:24:12 +08:00
										 |  |  | 		expectSuccessfulResourceVersionExec(t, b.TestDBProvider, | 
					
						
							|  |  |  | 			func() { b.ExecWithResult("insert resource", 0, 1) }, | 
					
						
							|  |  |  | 			func() { b.ExecWithResult("insert resource_history", 0, 1) }, | 
					
						
							|  |  |  | 		) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 		b.SQLMock.ExpectCommit() | 
					
						
							|  |  |  | 		v, err := b.create(ctx, event) | 
					
						
							|  |  |  | 		require.NoError(t, err) | 
					
						
							| 
									
										
										
										
											2025-03-13 16:24:12 +08:00
										 |  |  | 		require.Equal(t, int64(200), v) | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:44:44 +08:00
										 |  |  | 	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) | 
					
						
							| 
									
										
										
										
											2025-04-08 21:35:11 +08:00
										 |  |  | 		require.ErrorIs(t, err, resource.ErrResourceAlreadyExists) | 
					
						
							| 
									
										
										
										
											2025-03-26 21:44:44 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	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) | 
					
						
							| 
									
										
										
										
											2025-03-13 16:24:12 +08:00
										 |  |  | 		require.ErrorContains(t, err, "insert into resource") | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	t.Run("error inserting into resource_history", func(t *testing.T) { | 
					
						
							|  |  |  | 		t.Parallel() | 
					
						
							|  |  |  | 		b, ctx := setupBackendTest(t) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		b.SQLMock.ExpectBegin() | 
					
						
							| 
									
										
										
										
											2024-10-11 17:11:33 +08:00
										 |  |  | 		b.ExecWithResult("insert resource", 0, 1) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 		b.ExecWithErr("insert resource_history", errTest) | 
					
						
							|  |  |  | 		b.SQLMock.ExpectRollback() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		v, err := b.create(ctx, event) | 
					
						
							|  |  |  | 		require.Zero(t, v) | 
					
						
							|  |  |  | 		require.Error(t, err) | 
					
						
							| 
									
										
										
										
											2025-03-13 16:24:12 +08:00
										 |  |  | 		require.ErrorContains(t, err, "insert into resource history") | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | func TestBackend_update(t *testing.T) { | 
					
						
							|  |  |  | 	t.Parallel() | 
					
						
							| 
									
										
										
										
											2024-11-12 19:52:04 +08:00
										 |  |  | 	meta, err := utils.MetaAccessor(&unstructured.Unstructured{ | 
					
						
							|  |  |  | 		Object: map[string]any{}, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 	require.NoError(t, err) | 
					
						
							|  |  |  | 	meta.SetFolder("folderuid") | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	event := resource.WriteEvent{ | 
					
						
							| 
									
										
										
										
											2024-11-12 19:52:04 +08:00
										 |  |  | 		Type:   resource.WatchEvent_MODIFIED, | 
					
						
							|  |  |  | 		Key:    resKey, | 
					
						
							|  |  |  | 		Object: meta, | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	t.Run("happy path", func(t *testing.T) { | 
					
						
							|  |  |  | 		t.Parallel() | 
					
						
							|  |  |  | 		b, ctx := setupBackendTest(t) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		b.SQLMock.ExpectBegin() | 
					
						
							| 
									
										
										
										
											2025-03-13 16:24:12 +08:00
										 |  |  | 		expectSuccessfulResourceVersionExec(t, b.TestDBProvider, | 
					
						
							|  |  |  | 			func() { b.ExecWithResult("update resource", 0, 1) }, | 
					
						
							|  |  |  | 			func() { b.ExecWithResult("insert resource_history", 0, 1) }, | 
					
						
							|  |  |  | 		) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 		b.SQLMock.ExpectCommit() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		v, err := b.update(ctx, event) | 
					
						
							|  |  |  | 		require.NoError(t, err) | 
					
						
							| 
									
										
										
										
											2025-03-13 16:24:12 +08:00
										 |  |  | 		require.Equal(t, int64(200), v) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	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) | 
					
						
							| 
									
										
										
										
											2025-03-13 16:24:12 +08:00
										 |  |  | 		require.ErrorContains(t, err, "resource update") | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	t.Run("error inserting into resource history", func(t *testing.T) { | 
					
						
							|  |  |  | 		t.Parallel() | 
					
						
							|  |  |  | 		b, ctx := setupBackendTest(t) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		b.SQLMock.ExpectBegin() | 
					
						
							| 
									
										
										
										
											2024-10-11 17:11:33 +08:00
										 |  |  | 		b.ExecWithResult("update resource", 0, 1) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 		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") | 
					
						
							|  |  |  | 	}) | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | func TestBackend_delete(t *testing.T) { | 
					
						
							|  |  |  | 	t.Parallel() | 
					
						
							| 
									
										
										
										
											2024-11-12 19:52:04 +08:00
										 |  |  | 	meta, err := utils.MetaAccessor(&unstructured.Unstructured{ | 
					
						
							|  |  |  | 		Object: map[string]any{}, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 	require.NoError(t, err) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	event := resource.WriteEvent{ | 
					
						
							| 
									
										
										
										
											2024-11-12 19:52:04 +08:00
										 |  |  | 		Type:   resource.WatchEvent_DELETED, | 
					
						
							|  |  |  | 		Key:    resKey, | 
					
						
							|  |  |  | 		Object: meta, | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	t.Run("happy path", func(t *testing.T) { | 
					
						
							|  |  |  | 		t.Parallel() | 
					
						
							|  |  |  | 		b, ctx := setupBackendTest(t) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		b.SQLMock.ExpectBegin() | 
					
						
							| 
									
										
										
										
											2025-03-13 16:24:12 +08:00
										 |  |  | 		expectSuccessfulResourceVersionExec(t, b.TestDBProvider, | 
					
						
							|  |  |  | 			func() { b.ExecWithResult("delete resource", 0, 1) }, | 
					
						
							|  |  |  | 			func() { b.ExecWithResult("insert resource_history", 0, 1) }, | 
					
						
							|  |  |  | 		) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 		b.SQLMock.ExpectCommit() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		v, err := b.delete(ctx, event) | 
					
						
							|  |  |  | 		require.NoError(t, err) | 
					
						
							| 
									
										
										
										
											2025-03-13 16:24:12 +08:00
										 |  |  | 		require.Equal(t, int64(200), v) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	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() | 
					
						
							| 
									
										
										
										
											2024-10-11 17:11:33 +08:00
										 |  |  | 		b.ExecWithResult("delete resource", 0, 1) | 
					
						
							| 
									
										
										
										
											2024-07-23 01:08:30 +08:00
										 |  |  | 		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") | 
					
						
							|  |  |  | 	}) | 
					
						
							| 
									
										
										
										
											2024-07-18 23:03:18 +08:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2024-12-14 06:55:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | type readHistoryRow struct { | 
					
						
							|  |  |  | 	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{ | 
					
						
							|  |  |  | 			namespace:        "ns", | 
					
						
							|  |  |  | 			group:            "gr", | 
					
						
							|  |  |  | 			resource:         "rs", | 
					
						
							|  |  |  | 			name:             "nm", | 
					
						
							|  |  |  | 			folder:           "folder", | 
					
						
							|  |  |  | 			resource_version: "300", | 
					
						
							|  |  |  | 			value:            "rv-300", | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		readResource := []string{"namespace", "group", "resource", "name", "folder", "resource_version", "value"} | 
					
						
							|  |  |  | 		b.SQLMock.ExpectBegin() | 
					
						
							|  |  |  | 		b.SQLMock.ExpectQuery("SELECT .* FROM resource"). | 
					
						
							|  |  |  | 			WillReturnRows(sqlmock.NewRows(readResource). | 
					
						
							|  |  |  | 				AddRow( | 
					
						
							|  |  |  | 					expectedReadRow.namespace, | 
					
						
							|  |  |  | 					expectedReadRow.group, | 
					
						
							|  |  |  | 					expectedReadRow.resource, | 
					
						
							|  |  |  | 					expectedReadRow.name, | 
					
						
							|  |  |  | 					expectedReadRow.folder, | 
					
						
							|  |  |  | 					expectedReadRow.resource_version, | 
					
						
							|  |  |  | 					expectedReadRow.value, | 
					
						
							|  |  |  | 				)) | 
					
						
							|  |  |  | 		b.SQLMock.ExpectCommit() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		req := &resource.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 := &resource.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{ | 
					
						
							|  |  |  | 			namespace:        "ns", | 
					
						
							|  |  |  | 			group:            "gr", | 
					
						
							|  |  |  | 			resource:         "rs", | 
					
						
							|  |  |  | 			name:             "nm", | 
					
						
							|  |  |  | 			folder:           "folder", | 
					
						
							|  |  |  | 			resource_version: "300", | 
					
						
							|  |  |  | 			value:            "rv-300", | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		readHistoryColumns := []string{"namespace", "group", "resource", "name", "folder", "resource_version", "value"} | 
					
						
							|  |  |  | 		b.SQLMock.ExpectBegin() | 
					
						
							|  |  |  | 		b.SQLMock.ExpectQuery("SELECT .* FROM resource_history"). | 
					
						
							|  |  |  | 			WillReturnRows(sqlmock.NewRows(readHistoryColumns). | 
					
						
							|  |  |  | 				AddRow( | 
					
						
							|  |  |  | 					expectedReadRow.namespace, | 
					
						
							|  |  |  | 					expectedReadRow.group, | 
					
						
							|  |  |  | 					expectedReadRow.resource, | 
					
						
							|  |  |  | 					expectedReadRow.name, | 
					
						
							|  |  |  | 					expectedReadRow.folder, | 
					
						
							|  |  |  | 					expectedReadRow.resource_version, | 
					
						
							|  |  |  | 					expectedReadRow.value, | 
					
						
							|  |  |  | 				)) | 
					
						
							|  |  |  | 		b.SQLMock.ExpectCommit() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		req := &resource.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 := &resource.ReadRequest{ | 
					
						
							|  |  |  | 			Key: resKey, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		rps := b.ReadResource(ctx, req) | 
					
						
							|  |  |  | 		require.NotNil(t, rps.Error) | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | func TestBackend_getHistory(t *testing.T) { | 
					
						
							|  |  |  | 	t.Parallel() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Common setup
 | 
					
						
							|  |  |  | 	key := &resource.ResourceKey{ | 
					
						
							|  |  |  | 		Namespace: "ns", | 
					
						
							|  |  |  | 		Group:     "gr", | 
					
						
							|  |  |  | 		Resource:  "rs", | 
					
						
							|  |  |  | 		Name:      "nm", | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	rv1, rv2, rv3 := int64(100), int64(200), int64(300) | 
					
						
							|  |  |  | 	cols := []string{"resource_version", "namespace", "name", "folder", "value"} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tests := []struct { | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 		name                          string | 
					
						
							|  |  |  | 		source                        resource.ListRequest_Source | 
					
						
							|  |  |  | 		versionMatch                  resource.ResourceVersionMatchV2 | 
					
						
							|  |  |  | 		resourceVersion               int64 | 
					
						
							|  |  |  | 		expectedVersions              []int64 | 
					
						
							|  |  |  | 		expectedListRv                int64 | 
					
						
							|  |  |  | 		expectedRowsCount             int | 
					
						
							|  |  |  | 		expectedErr                   string | 
					
						
							|  |  |  | 		expectedLatestDeletionAsMinRV bool | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 	}{ | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			name:              "with ResourceVersionMatch_NotOlderThan", | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 			source:            resource.ListRequest_HISTORY, | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 			versionMatch:      resource.ResourceVersionMatchV2_NotOlderThan, | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 			resourceVersion:   rv2, | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 			expectedVersions:  []int64{rv2, rv3}, // Should be in ASC order due to NotOlderThan
 | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 			expectedListRv:    rv3, | 
					
						
							|  |  |  | 			expectedRowsCount: 2, | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 		{ | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 			name:                          "with ResourceVersionMatch_NotOlderThan and ResourceVersion=0", | 
					
						
							|  |  |  | 			source:                        resource.ListRequest_HISTORY, | 
					
						
							|  |  |  | 			versionMatch:                  resource.ResourceVersionMatchV2_NotOlderThan, | 
					
						
							|  |  |  | 			resourceVersion:               0, | 
					
						
							|  |  |  | 			expectedVersions:              []int64{rv1, rv2, rv3}, // Should be in ASC order due to NotOlderThan
 | 
					
						
							|  |  |  | 			expectedListRv:                rv3, | 
					
						
							|  |  |  | 			expectedRowsCount:             3, | 
					
						
							|  |  |  | 			expectedLatestDeletionAsMinRV: true, | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 		{ | 
					
						
							|  |  |  | 			name:              "with ResourceVersionMatch_Exact", | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 			source:            resource.ListRequest_HISTORY, | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 			versionMatch:      resource.ResourceVersionMatchV2_Exact, | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 			resourceVersion:   rv2, | 
					
						
							|  |  |  | 			expectedVersions:  []int64{rv2}, | 
					
						
							|  |  |  | 			expectedListRv:    rv3, | 
					
						
							|  |  |  | 			expectedRowsCount: 1, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 			name:                          "with ResourceVersionMatch_Unset (default)", | 
					
						
							|  |  |  | 			source:                        resource.ListRequest_HISTORY, | 
					
						
							|  |  |  | 			expectedVersions:              []int64{rv3, rv2, rv1}, // Should be in DESC order by default
 | 
					
						
							|  |  |  | 			expectedListRv:                rv3, | 
					
						
							|  |  |  | 			expectedRowsCount:             3, | 
					
						
							|  |  |  | 			expectedLatestDeletionAsMinRV: true, | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			name:            "error with ResourceVersionMatch_Exact and ResourceVersion <= 0", | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 			source:          resource.ListRequest_HISTORY, | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 			versionMatch:    resource.ResourceVersionMatchV2_Exact, | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 			resourceVersion: 0, | 
					
						
							|  |  |  | 			expectedErr:     "expecting an explicit resource version query when using Exact matching", | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 		{ | 
					
						
							|  |  |  | 			name:              "with ListRequest_TRASH", | 
					
						
							|  |  |  | 			source:            resource.ListRequest_TRASH, | 
					
						
							|  |  |  | 			expectedVersions:  []int64{rv3, rv2, rv1}, // Should be in DESC order by default
 | 
					
						
							|  |  |  | 			expectedListRv:    rv3, | 
					
						
							|  |  |  | 			expectedRowsCount: 3, | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	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 := &resource.ListRequest{ | 
					
						
							|  |  |  | 				Options:         &resource.ListOptions{Key: key}, | 
					
						
							|  |  |  | 				ResourceVersion: tc.resourceVersion, | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 				VersionMatchV2:  tc.versionMatch, | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 				Source:          tc.source, | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// 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 | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 					var seenVersions []int64 | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 					for iter.Next() { | 
					
						
							|  |  |  | 						count++ | 
					
						
							|  |  |  | 						currentRV := iter.ResourceVersion() | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 						seenVersions = append(seenVersions, currentRV) | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 						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) | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 					// Verify the order matches what we expect
 | 
					
						
							|  |  |  | 					require.Equal(t, tc.expectedVersions, seenVersions, "Resource versions returned in incorrect order") | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 					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) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 				if tc.expectedLatestDeletionAsMinRV { | 
					
						
							|  |  |  | 					latestHistoryRVRows := sqlmock.NewRows([]string{"resource_version"}). | 
					
						
							|  |  |  | 						AddRow(rv1) | 
					
						
							|  |  |  | 					b.SQLMock.ExpectQuery("SELECT .* FROM resource_history"). | 
					
						
							|  |  |  | 						WillReturnRows(latestHistoryRVRows) | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-19 23:16:48 +08:00
										 |  |  | 				// Expect history query
 | 
					
						
							|  |  |  | 				historyRows := sqlmock.NewRows(cols) | 
					
						
							|  |  |  | 				for _, rv := range tc.expectedVersions { | 
					
						
							|  |  |  | 					historyRows.AddRow( | 
					
						
							|  |  |  | 						rv,                               // resource_version
 | 
					
						
							|  |  |  | 						"ns",                             // namespace
 | 
					
						
							|  |  |  | 						"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) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | // 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 := &resource.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 := &resource.ListRequest{ | 
					
						
							|  |  |  | 				Options:         &resource.ListOptions{Key: key}, | 
					
						
							|  |  |  | 				ResourceVersion: initialRV, | 
					
						
							|  |  |  | 				VersionMatchV2:  resource.ResourceVersionMatchV2_NotOlderThan, | 
					
						
							|  |  |  | 				Source:          resource.ListRequest_HISTORY, | 
					
						
							|  |  |  | 				Limit:           4, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if page.token != nil { | 
					
						
							|  |  |  | 				req.NextPageToken = page.token.String() | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 			expectedLatestDeletionAsMinRV := false | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 			items := make([]int64, 0) | 
					
						
							|  |  |  | 			callback := func(iter resource.ListIterator) error { | 
					
						
							|  |  |  | 				for iter.Next() { | 
					
						
							|  |  |  | 					items = append(items, iter.ResourceVersion()) | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				return nil | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			b.SQLMock.ExpectBegin() | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 			historyRows := setupHistoryTest(b, page.versions, rv60, expectedLatestDeletionAsMinRV) | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 			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 := &resource.ListRequest{ | 
					
						
							|  |  |  | 			Options:         &resource.ListOptions{Key: key}, | 
					
						
							|  |  |  | 			ResourceVersion: 0, | 
					
						
							|  |  |  | 			VersionMatchV2:  resource.ResourceVersionMatchV2_NotOlderThan, | 
					
						
							|  |  |  | 			Source:          resource.ListRequest_HISTORY, | 
					
						
							|  |  |  | 			Limit:           4, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 		expectedLatestDeletionAsMinRV := true | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 		// 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() | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 		historyRows := setupHistoryTest(b, expectedVersions, rv60, expectedLatestDeletionAsMinRV) | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 		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
 | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | func setupHistoryTest(b testBackend, resourceVersions []int64, latestRV int64, expectedLatestDeletionAsMinRV4 bool) *sqlmock.Rows { | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 	// 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) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-28 01:34:37 +08:00
										 |  |  | 	if expectedLatestDeletionAsMinRV4 { | 
					
						
							|  |  |  | 		latestHistoryRVRows := sqlmock.NewRows([]string{"resource_version"}). | 
					
						
							|  |  |  | 			AddRow(latestRV) | 
					
						
							|  |  |  | 		b.SQLMock.ExpectQuery("SELECT .* FROM resource_history"). | 
					
						
							|  |  |  | 			WillReturnRows(latestHistoryRVRows) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-22 00:35:32 +08:00
										 |  |  | 	// Create the mock rows for the history items
 | 
					
						
							|  |  |  | 	cols := []string{"resource_version", "namespace", "name", "folder", "value"} | 
					
						
							|  |  |  | 	historyRows := sqlmock.NewRows(cols) | 
					
						
							|  |  |  | 	for _, rv := range resourceVersions { | 
					
						
							|  |  |  | 		historyRows.AddRow( | 
					
						
							|  |  |  | 			rv,                               // resource_version
 | 
					
						
							|  |  |  | 			"ns",                             // namespace
 | 
					
						
							|  |  |  | 			"nm",                             // name
 | 
					
						
							|  |  |  | 			"folder",                         // folder
 | 
					
						
							|  |  |  | 			[]byte(fmt.Sprintf("rv-%d", rv)), // value
 | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return historyRows | 
					
						
							|  |  |  | } |