The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/store/entity/sqlstash/utils_test.go

525 lines
13 KiB

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/storage/unified/sql/sqltemplate"
sqltemplateMocks "github.com/grafana/grafana/pkg/storage/unified/sql/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))
})
}