Unified Storage: Testing For Fix Create, Update and Delete wrt Resource Versions (#88568)
Add testing harness
This commit is contained in:
@@ -0,0 +1,525 @@
|
||||
package sqlstash
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
sqlmock "github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/store/entity/db"
|
||||
"github.com/grafana/grafana/pkg/services/store/entity/db/dbimpl"
|
||||
"github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
|
||||
sqltemplateMocks "github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate/mocks"
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
|
||||
// newMockDBNopSQL returns a db.DB and a sqlmock.Sqlmock that doesn't validates
|
||||
// SQL. This is only meant to be used to test wrapping utilities exec, query and
|
||||
// queryRow, where the actual SQL is not relevant to the unit tests, but rather
|
||||
// how the possible derived error conditions handled.
|
||||
func newMockDBNopSQL(t *testing.T) (db.DB, sqlmock.Sqlmock) {
|
||||
t.Helper()
|
||||
|
||||
db, mock, err := sqlmock.New(
|
||||
sqlmock.MonitorPingsOption(true),
|
||||
sqlmock.QueryMatcherOption(sqlmock.QueryMatcherFunc(
|
||||
func(expectedSQL, actualSQL string) error {
|
||||
return nil
|
||||
},
|
||||
)),
|
||||
)
|
||||
|
||||
return newUnitTestDB(t, db, mock, err)
|
||||
}
|
||||
|
||||
// newMockDBMatchWords returns a db.DB and a sqlmock.Sqlmock that will match SQL
|
||||
// by splitting the expected SQL string into words, and then try to find all of
|
||||
// them in the actual SQL, in the given order, case insensitively. Prepend a
|
||||
// word with a `!` to say that word should not be found.
|
||||
func newMockDBMatchWords(t *testing.T) (db.DB, sqlmock.Sqlmock) {
|
||||
t.Helper()
|
||||
|
||||
db, mock, err := sqlmock.New(
|
||||
sqlmock.MonitorPingsOption(true),
|
||||
sqlmock.QueryMatcherOption(
|
||||
sqlmock.QueryMatcherFunc(func(expectedSQL, actualSQL string) error {
|
||||
actualSQL = strings.ToLower(sqltemplate.FormatSQL(actualSQL))
|
||||
expectedSQL = strings.ToLower(expectedSQL)
|
||||
|
||||
var offset int
|
||||
for _, vv := range mockDBMatchWordsRE.FindAllStringSubmatch(expectedSQL, -1) {
|
||||
v := vv[1]
|
||||
|
||||
var shouldNotMatch bool
|
||||
if v != "" && v[0] == '!' {
|
||||
v = v[1:]
|
||||
shouldNotMatch = true
|
||||
}
|
||||
if v == "" {
|
||||
return fmt.Errorf("invalid expected word %q in %q", v,
|
||||
expectedSQL)
|
||||
}
|
||||
|
||||
reWord, err := regexp.Compile(`\b` + regexp.QuoteMeta(v) + `\b`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compile word %q from expected SQL: %s", v,
|
||||
expectedSQL)
|
||||
}
|
||||
|
||||
if shouldNotMatch {
|
||||
if reWord.MatchString(actualSQL[offset:]) {
|
||||
return fmt.Errorf("actual SQL fragent should not cont"+
|
||||
"ain %q but it does\n\tFragment: %s\n\tFull SQL: %s",
|
||||
v, actualSQL[offset:], actualSQL)
|
||||
}
|
||||
} else {
|
||||
loc := reWord.FindStringIndex(actualSQL[offset:])
|
||||
if len(loc) == 0 {
|
||||
return fmt.Errorf("actual SQL fragment should contain "+
|
||||
"%q but it doesn't\n\tFragment: %s\n\tFull SQL: %s",
|
||||
v, actualSQL[offset:], actualSQL)
|
||||
}
|
||||
offset = loc[1] // advance the offset
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return newUnitTestDB(t, db, mock, err)
|
||||
}
|
||||
|
||||
var mockDBMatchWordsRE = regexp.MustCompile(`(?:\W|\A)(!?\w+)\b`)
|
||||
|
||||
func newUnitTestDB(t *testing.T, db *sql.DB, mock sqlmock.Sqlmock, err error) (db.DB, sqlmock.Sqlmock) {
|
||||
t.Helper()
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
return dbimpl.NewDB(db, "sqlmock"), mock
|
||||
}
|
||||
|
||||
// mockResults aids in testing code paths with queries returning large number of
|
||||
// values, like those returning *entity.Entity. This is because we want to
|
||||
// emulate returning the same row columns and row values the same as a real
|
||||
// database would do. This utility the same template SQL that is expected to be
|
||||
// used to help populate all the expected fields.
|
||||
// fileds
|
||||
type mockResults[T any] struct {
|
||||
t *testing.T
|
||||
tmpl *template.Template
|
||||
data sqltemplate.WithResults[T]
|
||||
rows *sqlmock.Rows
|
||||
}
|
||||
|
||||
// newMockResults returns a new *mockResults. If you want to emulate a call
|
||||
// returning zero rows, then immediately call the Row method afterward.
|
||||
func newMockResults[T any](t *testing.T, mock sqlmock.Sqlmock, tmpl *template.Template, data sqltemplate.WithResults[T]) *mockResults[T] {
|
||||
t.Helper()
|
||||
|
||||
data.Reset()
|
||||
err := tmpl.Execute(io.Discard, data)
|
||||
require.NoError(t, err)
|
||||
rows := mock.NewRows(data.GetColNames())
|
||||
|
||||
return &mockResults[T]{
|
||||
t: t,
|
||||
tmpl: tmpl,
|
||||
data: data,
|
||||
rows: rows,
|
||||
}
|
||||
}
|
||||
|
||||
// AddCurrentData uses the values contained in the `data` argument used during
|
||||
// creation to populate a new expected row. It will access `data` with pointers,
|
||||
// so you should replace the internal values of `data` with freshly allocated
|
||||
// results to return different rows.
|
||||
func (r *mockResults[T]) AddCurrentData() *mockResults[T] {
|
||||
r.t.Helper()
|
||||
|
||||
r.data.Reset()
|
||||
err := r.tmpl.Execute(io.Discard, r.data)
|
||||
require.NoError(r.t, err)
|
||||
|
||||
d := r.data.GetScanDest()
|
||||
dv := make([]driver.Value, len(d))
|
||||
for i, v := range d {
|
||||
dv[i] = v
|
||||
}
|
||||
r.rows.AddRow(dv...)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Rows returns the *sqlmock.Rows object built.
|
||||
func (r *mockResults[T]) Rows() *sqlmock.Rows {
|
||||
return r.rows
|
||||
}
|
||||
|
||||
func TestCreateETag(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := createETag(nil, nil, nil)
|
||||
require.Equal(t, "d41d8cd98f00b204e9800998ecf8427e", v)
|
||||
}
|
||||
|
||||
func TestGetCurrentUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
username, err := getCurrentUser(ctx)
|
||||
require.NotEmpty(t, username)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx = ctx.WithUser(nil)
|
||||
username, err = getCurrentUser(ctx)
|
||||
require.Empty(t, username)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, ErrUserNotFoundInContext)
|
||||
}
|
||||
|
||||
func TestPtrOr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := ptrOr[*int]()
|
||||
require.NotNil(t, p)
|
||||
require.Zero(t, *p)
|
||||
|
||||
p = ptrOr[*int](nil, nil, nil, nil, nil, nil)
|
||||
require.NotNil(t, p)
|
||||
require.Zero(t, *p)
|
||||
|
||||
v := 42
|
||||
v2 := 5
|
||||
p = ptrOr(nil, nil, nil, &v, nil, &v2, nil, nil)
|
||||
require.NotNil(t, p)
|
||||
require.Equal(t, v, *p)
|
||||
|
||||
p = ptrOr(nil, nil, nil, &v)
|
||||
require.NotNil(t, p)
|
||||
require.Equal(t, v, *p)
|
||||
}
|
||||
|
||||
func TestSliceOr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := sliceOr[[]int]()
|
||||
require.NotNil(t, p)
|
||||
require.Len(t, p, 0)
|
||||
|
||||
p = sliceOr[[]int](nil, nil, nil, nil)
|
||||
require.NotNil(t, p)
|
||||
require.Len(t, p, 0)
|
||||
|
||||
p = sliceOr([]int{}, []int{}, []int{}, []int{})
|
||||
require.NotNil(t, p)
|
||||
require.Len(t, p, 0)
|
||||
|
||||
v := []int{1, 2}
|
||||
p = sliceOr([]int{}, nil, []int{}, v, nil, []int{}, []int{10}, nil)
|
||||
require.NotNil(t, p)
|
||||
require.Equal(t, v, p)
|
||||
|
||||
p = sliceOr([]int{}, nil, []int{}, v)
|
||||
require.NotNil(t, p)
|
||||
require.Equal(t, v, p)
|
||||
}
|
||||
|
||||
func TestMapOr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := mapOr[map[string]int]()
|
||||
require.NotNil(t, p)
|
||||
require.Len(t, p, 0)
|
||||
|
||||
p = mapOr(nil, map[string]int(nil), nil, map[string]int{}, nil)
|
||||
require.NotNil(t, p)
|
||||
require.Len(t, p, 0)
|
||||
|
||||
v := map[string]int{"a": 0, "b": 1}
|
||||
v2 := map[string]int{"c": 2, "d": 3}
|
||||
|
||||
p = mapOr(nil, map[string]int(nil), v, v2, nil, map[string]int{}, nil)
|
||||
require.NotNil(t, p)
|
||||
require.Equal(t, v, p)
|
||||
|
||||
p = mapOr(nil, map[string]int(nil), v)
|
||||
require.NotNil(t, p)
|
||||
require.Equal(t, v, p)
|
||||
}
|
||||
|
||||
var (
|
||||
validTestTmpl = template.Must(template.New("test").Parse("nothing special"))
|
||||
invalidTestTmpl = template.New("no definition should fail to exec")
|
||||
errTest = errors.New("because of reasons")
|
||||
)
|
||||
|
||||
// expectRows is a testing helper to keep mocks in sync when adding rows to a
|
||||
// mocked SQL result. This is a helper to test `query` and `queryRow`.
|
||||
type expectRows[T any] struct {
|
||||
*sqlmock.Rows
|
||||
ExpectedResults []T
|
||||
|
||||
req *sqltemplateMocks.WithResults[T]
|
||||
}
|
||||
|
||||
func newReturnsRow[T any](dbmock sqlmock.Sqlmock, req *sqltemplateMocks.WithResults[T]) *expectRows[T] {
|
||||
return &expectRows[T]{
|
||||
Rows: dbmock.NewRows(nil),
|
||||
req: req,
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a new value that should be returned by the `query` or `queryRow`
|
||||
// operation.
|
||||
func (r *expectRows[T]) Add(value T, err error) *expectRows[T] {
|
||||
r.req.EXPECT().GetScanDest().Return(nil).Once()
|
||||
r.req.EXPECT().Results().Return(value, err).Once()
|
||||
r.Rows.AddRow()
|
||||
r.ExpectedResults = append(r.ExpectedResults, value)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func TestQueryRow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
req := sqltemplateMocks.NewWithResults[int64](t)
|
||||
db, dbmock := newMockDBNopSQL(t)
|
||||
rows := newReturnsRow(dbmock, req)
|
||||
|
||||
// setup expectations
|
||||
req.EXPECT().Validate().Return(nil).Once()
|
||||
req.EXPECT().GetArgs().Return(nil).Once()
|
||||
rows.Add(1, nil)
|
||||
dbmock.ExpectQuery("").WillReturnRows(rows.Rows)
|
||||
|
||||
// execute and assert
|
||||
res, err := queryRow(ctx, db, validTestTmpl, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, rows.ExpectedResults[0], res)
|
||||
})
|
||||
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
req := sqltemplateMocks.NewWithResults[int64](t)
|
||||
db, _ := newMockDBNopSQL(t)
|
||||
|
||||
// setup expectations
|
||||
req.EXPECT().Validate().Return(errTest).Once()
|
||||
|
||||
// execute and assert
|
||||
res, err := queryRow(ctx, db, invalidTestTmpl, req)
|
||||
require.Zero(t, res)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "invalid request")
|
||||
})
|
||||
|
||||
t.Run("error executing template", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
req := sqltemplateMocks.NewWithResults[int64](t)
|
||||
db, _ := newMockDBNopSQL(t)
|
||||
|
||||
// setup expectations
|
||||
req.EXPECT().Validate().Return(nil).Once()
|
||||
|
||||
// execute and assert
|
||||
res, err := queryRow(ctx, db, invalidTestTmpl, req)
|
||||
require.Zero(t, res)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "execute template")
|
||||
})
|
||||
|
||||
t.Run("error executing query", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
req := sqltemplateMocks.NewWithResults[int64](t)
|
||||
db, dbmock := newMockDBNopSQL(t)
|
||||
|
||||
// setup expectations
|
||||
req.EXPECT().Validate().Return(nil).Once()
|
||||
req.EXPECT().GetArgs().Return(nil)
|
||||
req.EXPECT().GetScanDest().Return(nil).Maybe()
|
||||
dbmock.ExpectQuery("").WillReturnError(errTest)
|
||||
|
||||
// execute and assert
|
||||
res, err := queryRow(ctx, db, validTestTmpl, req)
|
||||
require.Zero(t, res)
|
||||
require.Error(t, err)
|
||||
require.ErrorAs(t, err, new(SQLError))
|
||||
})
|
||||
}
|
||||
|
||||
// scannerFunc is an adapter for the `scanner` interface.
|
||||
type scannerFunc func(dest ...any) error
|
||||
|
||||
func (f scannerFunc) Scan(dest ...any) error {
|
||||
return f(dest...)
|
||||
}
|
||||
|
||||
func TestScanRow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const value int64 = 1
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
req := sqltemplateMocks.NewWithResults[int64](t)
|
||||
sc := scannerFunc(func(dest ...any) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
// setup expectations
|
||||
req.EXPECT().GetScanDest().Return(nil).Once()
|
||||
req.EXPECT().Results().Return(value, nil).Once()
|
||||
|
||||
// execute and assert
|
||||
res, err := scanRow(sc, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, value, res)
|
||||
})
|
||||
|
||||
t.Run("scan error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
req := sqltemplateMocks.NewWithResults[int64](t)
|
||||
sc := scannerFunc(func(dest ...any) error {
|
||||
return errTest
|
||||
})
|
||||
|
||||
// setup expectations
|
||||
req.EXPECT().GetScanDest().Return(nil).Once()
|
||||
|
||||
// execute and assert
|
||||
res, err := scanRow(sc, req)
|
||||
require.Zero(t, res)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
})
|
||||
|
||||
t.Run("results error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
req := sqltemplateMocks.NewWithResults[int64](t)
|
||||
sc := scannerFunc(func(dest ...any) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
// setup expectations
|
||||
req.EXPECT().GetScanDest().Return(nil).Once()
|
||||
req.EXPECT().Results().Return(0, errTest).Once()
|
||||
|
||||
// execute and assert
|
||||
res, err := scanRow(sc, req)
|
||||
require.Zero(t, res)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExec(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
req := sqltemplateMocks.NewSQLTemplateIface(t)
|
||||
db, dbmock := newMockDBNopSQL(t)
|
||||
|
||||
// setup expectations
|
||||
req.EXPECT().Validate().Return(nil).Once()
|
||||
req.EXPECT().GetArgs().Return(nil).Once()
|
||||
dbmock.ExpectExec("").WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
|
||||
// execute and assert
|
||||
res, err := exec(ctx, db, validTestTmpl, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, res)
|
||||
})
|
||||
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
req := sqltemplateMocks.NewSQLTemplateIface(t)
|
||||
db, _ := newMockDBNopSQL(t)
|
||||
|
||||
// setup expectations
|
||||
req.EXPECT().Validate().Return(errTest).Once()
|
||||
|
||||
// execute and assert
|
||||
res, err := exec(ctx, db, invalidTestTmpl, req)
|
||||
require.Nil(t, res)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "invalid request")
|
||||
})
|
||||
|
||||
t.Run("error executing template", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
req := sqltemplateMocks.NewSQLTemplateIface(t)
|
||||
db, _ := newMockDBNopSQL(t)
|
||||
|
||||
// setup expectations
|
||||
req.EXPECT().Validate().Return(nil).Once()
|
||||
|
||||
// execute and assert
|
||||
res, err := exec(ctx, db, invalidTestTmpl, req)
|
||||
require.Nil(t, res)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "execute template")
|
||||
})
|
||||
|
||||
t.Run("error executing SQL", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
req := sqltemplateMocks.NewSQLTemplateIface(t)
|
||||
db, dbmock := newMockDBNopSQL(t)
|
||||
|
||||
// setup expectations
|
||||
req.EXPECT().Validate().Return(nil).Once()
|
||||
req.EXPECT().GetArgs().Return(nil)
|
||||
dbmock.ExpectExec("").WillReturnError(errTest)
|
||||
|
||||
// execute and assert
|
||||
res, err := exec(ctx, db, validTestTmpl, req)
|
||||
require.Nil(t, res)
|
||||
require.Error(t, err)
|
||||
require.ErrorAs(t, err, new(SQLError))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user