Resource server improvements and fixes (#90715)
* cleanup dependencies and improve list method * Improve Resource Server API, remove unnecessary dependencies * Reduce the API footprint of ResourceDBInterface and its implementation * Improve LifecycleHooks to use context * Improve testing * reduce API size and improve code * sqltemplate: add DialectForDriver func and improve naming * improve lifecycle API * many small fixes after adding more tests
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
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.Rows.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().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().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().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
|
||||
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().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().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().Validate().Return(nil).Once()
|
||||
req.EXPECT().GetArgs().Return(nil).Once()
|
||||
rows.Rows.AddRow() // we don't expect GetScanDest or Results here
|
||||
rows.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().Validate().Return(nil).Once()
|
||||
req.EXPECT().GetArgs().Return(nil).Once()
|
||||
rows1.Add(1, nil)
|
||||
rows2.Rows.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().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().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().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().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().Validate().Return(nil).Once()
|
||||
req.EXPECT().GetArgs().Return(nil).Once()
|
||||
rows1.Add(1, nil)
|
||||
rows2.Rows.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.NewSQLTemplateIface(t)
|
||||
rdb := test.NewDBProviderNopSQL(t)
|
||||
|
||||
// setup expectations
|
||||
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.NewSQLTemplateIface(t)
|
||||
rdb := test.NewDBProviderNopSQL(t)
|
||||
|
||||
// setup expectations
|
||||
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.NewSQLTemplateIface(t)
|
||||
rdb := test.NewDBProviderNopSQL(t)
|
||||
|
||||
// setup expectations
|
||||
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.NewSQLTemplateIface(t)
|
||||
rdb := test.NewDBProviderNopSQL(t)
|
||||
|
||||
// setup expectations
|
||||
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))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user