mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 09:42:07 +08:00
548 lines
15 KiB
Go
548 lines
15 KiB
Go
package dbutil
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"testing"
|
|
"text/template"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
sqltemplateMocks "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks"
|
|
"github.com/grafana/grafana/pkg/storage/unified/sql/test"
|
|
"github.com/grafana/grafana/pkg/util/testutil"
|
|
)
|
|
|
|
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")
|
|
)
|
|
|
|
func TestSQLError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const hiddenMessage = "obey, consume"
|
|
|
|
var err error = SQLError{
|
|
Err: errTest,
|
|
CallType: "Exec",
|
|
TemplateName: "some.sql",
|
|
Query: "SELECT name FROM movies WHERE quote LIKE ?",
|
|
RawQuery: "SELECT name FROM movies WHERE quote LIKE ?",
|
|
ScanDest: []any{new(string)},
|
|
arguments: []any{hiddenMessage},
|
|
}
|
|
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, errTest)
|
|
require.NotContains(t, err.Error(), hiddenMessage)
|
|
|
|
err = Debug(err)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), hiddenMessage)
|
|
|
|
err = Debug(errTest)
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, errTest)
|
|
}
|
|
|
|
// expectRows is a testing helper to keep mocks in sync when adding rows to a
|
|
// mocked SQL result.
|
|
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.AddRow()
|
|
r.ExpectedResults = append(r.ExpectedResults, value)
|
|
|
|
return r
|
|
}
|
|
|
|
func TestQuery(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("happy path - no rows returned", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// test declarations
|
|
ctx := testutil.NewDefaultTestContext(t)
|
|
req := sqltemplateMocks.NewWithResults[int64](t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil)
|
|
req.EXPECT().GetScanDest().Return(nil).Maybe()
|
|
rdb.SQLMock.ExpectQuery("").WillReturnRows(rdb.SQLMock.NewRows(nil))
|
|
|
|
// execute and assert
|
|
res, err := Query(ctx, rdb.DB, validTestTmpl, req)
|
|
require.NoError(t, err)
|
|
require.Zero(t, res)
|
|
})
|
|
|
|
t.Run("happy path - multiple rows returned", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// test declarations
|
|
ctx := testutil.NewDefaultTestContext(t)
|
|
req := sqltemplateMocks.NewWithResults[int64](t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
rows := newReturnsRow(rdb.SQLMock, req)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil).Once()
|
|
rows.Add(1, nil)
|
|
rows.Add(2, nil)
|
|
rows.Add(3, nil)
|
|
rdb.SQLMock.ExpectQuery("").WillReturnRows(rows.Rows)
|
|
|
|
// execute and assert
|
|
res, err := Query(ctx, rdb.DB, validTestTmpl, req)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, res)
|
|
require.Equal(t, rows.ExpectedResults, res)
|
|
})
|
|
|
|
t.Run("invalid request", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// test declarations
|
|
ctx := testutil.NewDefaultTestContext(t)
|
|
req := sqltemplateMocks.NewWithResults[int64](t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(errTest).Once()
|
|
|
|
// execute and assert
|
|
res, err := Query(ctx, rdb.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)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
|
|
// setup expectations
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
|
|
// execute and assert
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
res, err := Query(ctx, rdb.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)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil)
|
|
req.EXPECT().GetScanDest().Return(nil).Maybe()
|
|
rdb.SQLMock.ExpectQuery("").WillReturnError(errTest)
|
|
|
|
// execute and assert
|
|
res, err := Query(ctx, rdb.DB, validTestTmpl, req)
|
|
require.Zero(t, res)
|
|
require.Error(t, err)
|
|
require.ErrorAs(t, err, new(SQLError))
|
|
})
|
|
|
|
t.Run("error decoding row", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// test declarations
|
|
ctx := testutil.NewDefaultTestContext(t)
|
|
req := sqltemplateMocks.NewWithResults[int64](t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
rows := newReturnsRow(rdb.SQLMock, req)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil).Once()
|
|
rows.Add(0, errTest)
|
|
rdb.SQLMock.ExpectQuery("").WillReturnRows(rows.Rows)
|
|
|
|
// execute and assert
|
|
res, err := Query(ctx, rdb.DB, validTestTmpl, req)
|
|
require.Zero(t, res)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "scan value")
|
|
})
|
|
|
|
t.Run("error iterating rows", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// test declarations
|
|
ctx := testutil.NewDefaultTestContext(t)
|
|
req := sqltemplateMocks.NewWithResults[int64](t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
rows := newReturnsRow(rdb.SQLMock, req)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil).Once()
|
|
rows.AddRow() // we don't expect GetScanDest or Results here
|
|
rows.RowError(0, errTest)
|
|
rdb.SQLMock.ExpectQuery("").WillReturnRows(rows.Rows)
|
|
|
|
// execute and assert
|
|
res, err := Query(ctx, rdb.DB, validTestTmpl, req)
|
|
require.Zero(t, res)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "closing rows")
|
|
})
|
|
|
|
t.Run("too many result sets", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// test declarations
|
|
ctx := testutil.NewDefaultTestContext(t)
|
|
req := sqltemplateMocks.NewWithResults[int64](t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
rows1 := newReturnsRow(rdb.SQLMock, req)
|
|
rows2 := newReturnsRow(rdb.SQLMock, req)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil).Once()
|
|
rows1.Add(1, nil)
|
|
rows2.AddRow() // we don't expect GetScanDest or Results here
|
|
rdb.SQLMock.ExpectQuery("").WillReturnRows(rows1.Rows, rows2.Rows)
|
|
|
|
// execute and assert
|
|
res, err := Query(ctx, rdb.DB, validTestTmpl, req)
|
|
require.Zero(t, res)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "too many result sets")
|
|
})
|
|
}
|
|
|
|
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)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
rows := newReturnsRow(rdb.SQLMock, req)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil).Once()
|
|
rows.Add(1, nil)
|
|
rdb.SQLMock.ExpectQuery("").WillReturnRows(rows.Rows)
|
|
|
|
// execute and assert
|
|
res, err := QueryRow(ctx, rdb.DB, validTestTmpl, req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, rows.ExpectedResults[0], res)
|
|
})
|
|
|
|
t.Run("no rows returned", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// test declarations
|
|
ctx := testutil.NewDefaultTestContext(t)
|
|
req := sqltemplateMocks.NewWithResults[int64](t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil).Once()
|
|
rdb.SQLMock.ExpectQuery("").WillReturnRows(rdb.SQLMock.NewRows(nil))
|
|
|
|
// execute and assert
|
|
res, err := QueryRow(ctx, rdb.DB, validTestTmpl, req)
|
|
require.Zero(t, res)
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
})
|
|
|
|
t.Run("error executing query", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// test declarations
|
|
ctx := testutil.NewDefaultTestContext(t)
|
|
req := sqltemplateMocks.NewWithResults[int64](t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil)
|
|
req.EXPECT().GetScanDest().Return(nil).Maybe()
|
|
rdb.SQLMock.ExpectQuery("").WillReturnError(errTest)
|
|
|
|
// execute and assert
|
|
res, err := QueryRow(ctx, rdb.DB, validTestTmpl, req)
|
|
require.Zero(t, res)
|
|
require.Error(t, err)
|
|
require.ErrorAs(t, err, new(SQLError))
|
|
})
|
|
|
|
t.Run("too many rows returned", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// test declarations
|
|
ctx := testutil.NewDefaultTestContext(t)
|
|
req := sqltemplateMocks.NewWithResults[int64](t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
rows := newReturnsRow(rdb.SQLMock, req)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil).Once()
|
|
rows.Add(1, nil)
|
|
rows.Add(2, nil)
|
|
rdb.SQLMock.ExpectQuery("").WillReturnRows(rows.Rows)
|
|
|
|
// execute and assert
|
|
res, err := QueryRow(ctx, rdb.DB, validTestTmpl, req)
|
|
require.Zero(t, res)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "expecting a single row")
|
|
})
|
|
|
|
t.Run("too many result sets", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// test declarations
|
|
ctx := testutil.NewDefaultTestContext(t)
|
|
req := sqltemplateMocks.NewWithResults[int64](t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
rows1 := newReturnsRow(rdb.SQLMock, req)
|
|
rows2 := newReturnsRow(rdb.SQLMock, req)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil).Once()
|
|
rows1.Add(1, nil)
|
|
rows2.AddRow() // we don't expect GetScanDest or Results here
|
|
rdb.SQLMock.ExpectQuery("").WillReturnRows(rows1.Rows, rows2.Rows)
|
|
|
|
// execute and assert
|
|
res, err := QueryRow(ctx, rdb.DB, validTestTmpl, req)
|
|
require.Zero(t, res)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "too many result sets")
|
|
})
|
|
}
|
|
|
|
// 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.NewSQLTemplate(t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil).Once()
|
|
rdb.SQLMock.ExpectExec("").WillReturnResult(sqlmock.NewResult(0, 0))
|
|
|
|
// execute and assert
|
|
res, err := Exec(ctx, rdb.DB, validTestTmpl, req)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, res)
|
|
})
|
|
|
|
t.Run("invalid request", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// test declarations
|
|
ctx := testutil.NewDefaultTestContext(t)
|
|
req := sqltemplateMocks.NewSQLTemplate(t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(errTest).Once()
|
|
|
|
// execute and assert
|
|
res, err := Exec(ctx, rdb.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.NewSQLTemplate(t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
|
|
// execute and assert
|
|
res, err := Exec(ctx, rdb.DB, invalidTestTmpl, req)
|
|
require.Zero(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.NewSQLTemplate(t)
|
|
rdb := test.NewDBProviderNopSQL(t)
|
|
|
|
// setup expectations
|
|
req.EXPECT().DialectName().Return("test").Maybe()
|
|
req.EXPECT().GetColNames().Return(nil).Maybe()
|
|
req.EXPECT().Validate().Return(nil).Once()
|
|
req.EXPECT().GetArgs().Return(nil)
|
|
rdb.SQLMock.ExpectExec("").WillReturnError(errTest)
|
|
|
|
// execute and assert
|
|
res, err := Exec(ctx, rdb.DB, validTestTmpl, req)
|
|
require.Zero(t, res)
|
|
require.Error(t, err)
|
|
require.ErrorAs(t, err, new(SQLError))
|
|
})
|
|
}
|