mirror of https://github.com/grafana/grafana
prometheushacktoberfestmetricsmonitoringalertinggrafanagoinfluxdbmysqlpostgresanalyticsdata-visualizationdashboardbusiness-intelligenceelasticsearch
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.
504 lines
13 KiB
504 lines
13 KiB
|
2 years ago
|
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 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))
|
||
|
|
})
|
||
|
|
}
|