mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Unified Storage: Testing For Fix Create, Update and Delete wrt Resource Versions (#88568)
Add testing harness
This commit is contained in:
parent
6fcd7d9e03
commit
5fc580b401
@ -11,6 +11,9 @@ func GetUserIDString(user *user.SignedInUser) string {
|
||||
// TODO: should we check IsDisabled?
|
||||
// TODO: could we use the NamespacedID.ID() as prefix instead of manually
|
||||
// setting "anon", "key", etc.?
|
||||
// TODO: the default unauthenticated user is not anonymous and would be
|
||||
// returned as `sys:0:` here. We may want to do something special in that
|
||||
// case
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
|
@ -1,49 +1,92 @@
|
||||
package dbimpl
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetEnginePostgresFromConfig(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
s, err := cfg.Raw.NewSection("entity_api")
|
||||
require.NoError(t, err)
|
||||
s.Key("db_type").SetValue("mysql")
|
||||
s.Key("db_host").SetValue("localhost")
|
||||
s.Key("db_name").SetValue("grafana")
|
||||
s.Key("db_user").SetValue("user")
|
||||
s.Key("db_password").SetValue("password")
|
||||
|
||||
getter := §ionGetter{
|
||||
DynamicSection: cfg.SectionWithEnvOverrides("entity_api"),
|
||||
}
|
||||
engine, err := getEnginePostgres(getter, nil)
|
||||
|
||||
assert.NotNil(t, engine)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.Contains(engine.DataSourceName(), "dbname=grafana"))
|
||||
}
|
||||
|
||||
func TestGetEngineMySQLFromConfig(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
s, err := cfg.Raw.NewSection("entity_api")
|
||||
require.NoError(t, err)
|
||||
s.Key("db_type").SetValue("mysql")
|
||||
s.Key("db_host").SetValue("localhost")
|
||||
s.Key("db_name").SetValue("grafana")
|
||||
s.Key("db_user").SetValue("user")
|
||||
s.Key("db_password").SetValue("password")
|
||||
t.Parallel()
|
||||
|
||||
getter := §ionGetter{
|
||||
DynamicSection: cfg.SectionWithEnvOverrides("entity_api"),
|
||||
}
|
||||
engine, err := getEngineMySQL(getter, nil)
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.NotNil(t, engine)
|
||||
assert.NoError(t, err)
|
||||
getter := newTestSectionGetter(map[string]string{
|
||||
"db_type": "mysql",
|
||||
"db_host": "/var/run/mysql.socket",
|
||||
"db_name": "grafana",
|
||||
"db_user": "user",
|
||||
"db_password": "password",
|
||||
})
|
||||
engine, err := getEngineMySQL(getter, nil)
|
||||
assert.NotNil(t, engine)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid string", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
getter := newTestSectionGetter(map[string]string{
|
||||
"db_type": "mysql",
|
||||
"db_host": "/var/run/mysql.socket",
|
||||
"db_name": string(invalidUTF8ByteSequence),
|
||||
"db_user": "user",
|
||||
"db_password": "password",
|
||||
})
|
||||
engine, err := getEngineMySQL(getter, nil)
|
||||
assert.Nil(t, engine)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrInvalidUTF8Sequence)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetEnginePostgresFromConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
getter := newTestSectionGetter(map[string]string{
|
||||
"db_type": "mysql",
|
||||
"db_host": "localhost",
|
||||
"db_name": "grafana",
|
||||
"db_user": "user",
|
||||
"db_password": "password",
|
||||
})
|
||||
engine, err := getEnginePostgres(getter, nil)
|
||||
|
||||
assert.NotNil(t, engine)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid string", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
getter := newTestSectionGetter(map[string]string{
|
||||
"db_type": "mysql",
|
||||
"db_host": string(invalidUTF8ByteSequence),
|
||||
"db_name": "grafana",
|
||||
"db_user": "user",
|
||||
"db_password": "password",
|
||||
})
|
||||
engine, err := getEnginePostgres(getter, nil)
|
||||
|
||||
assert.Nil(t, engine)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrInvalidUTF8Sequence)
|
||||
})
|
||||
|
||||
t.Run("invalid hostport", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
getter := newTestSectionGetter(map[string]string{
|
||||
"db_type": "mysql",
|
||||
"db_host": "1:1:1",
|
||||
"db_name": "grafana",
|
||||
"db_user": "user",
|
||||
"db_password": "password",
|
||||
})
|
||||
engine, err := getEnginePostgres(getter, nil)
|
||||
|
||||
assert.Nil(t, engine)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package dbimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -29,6 +30,44 @@ func newCtx(t *testing.T) context.Context {
|
||||
return ctx
|
||||
}
|
||||
|
||||
var errTest = errors.New("because of reasons")
|
||||
|
||||
const driverName = "sqlmock"
|
||||
|
||||
func TestDB_BeginTx(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqldb, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
db := NewDB(sqldb, driverName)
|
||||
require.Equal(t, driverName, db.DriverName())
|
||||
|
||||
mock.ExpectBegin()
|
||||
tx, err := db.BeginTx(newCtx(t), nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tx)
|
||||
})
|
||||
|
||||
t.Run("fail begin", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqldb, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
db := NewDB(sqldb, "sqlmock")
|
||||
|
||||
mock.ExpectBegin().WillReturnError(errTest)
|
||||
tx, err := db.BeginTx(newCtx(t), nil)
|
||||
|
||||
require.Nil(t, tx)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDB_WithTx(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@ -51,4 +90,65 @@ func TestDB_WithTx(t *testing.T) {
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("fail begin", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqldb, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
db := NewDB(sqldb, "sqlmock")
|
||||
|
||||
mock.ExpectBegin().WillReturnError(errTest)
|
||||
err = db.WithTx(newCtx(t), nil, newTxFunc(nil))
|
||||
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
})
|
||||
|
||||
t.Run("fail tx", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqldb, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
db := NewDB(sqldb, "sqlmock")
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectRollback()
|
||||
err = db.WithTx(newCtx(t), nil, newTxFunc(errTest))
|
||||
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
})
|
||||
|
||||
t.Run("fail tx; fail rollback", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqldb, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
db := NewDB(sqldb, "sqlmock")
|
||||
errTest2 := errors.New("yet another err")
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectRollback().WillReturnError(errTest)
|
||||
err = db.WithTx(newCtx(t), nil, newTxFunc(errTest2))
|
||||
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
require.ErrorIs(t, err, errTest2)
|
||||
})
|
||||
|
||||
t.Run("fail commit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqldb, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
db := NewDB(sqldb, "sqlmock")
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectCommit().WillReturnError(errTest)
|
||||
err = db.WithTx(newCtx(t), nil, newTxFunc(nil))
|
||||
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
})
|
||||
}
|
||||
|
@ -44,15 +44,18 @@ type EntityDB struct {
|
||||
}
|
||||
|
||||
func (db *EntityDB) Init() error {
|
||||
_, err := db.GetEngine()
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *EntityDB) GetEngine() (*xorm.Engine, error) {
|
||||
db.once.Do(func() {
|
||||
db.onceErr = db.init()
|
||||
})
|
||||
|
||||
return db.onceErr
|
||||
}
|
||||
|
||||
func (db *EntityDB) GetEngine() (*xorm.Engine, error) {
|
||||
if err := db.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.engine, db.onceErr
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
@ -25,9 +26,6 @@ func (g *sectionGetter) Err() error {
|
||||
}
|
||||
|
||||
func (g *sectionGetter) String(key string) string {
|
||||
if g.err != nil {
|
||||
return ""
|
||||
}
|
||||
v := g.DynamicSection.Key(key).MustString("")
|
||||
if !utf8.ValidString(v) {
|
||||
g.err = fmt.Errorf("value for key %q: %w", key, ErrInvalidUTF8Sequence)
|
||||
@ -43,7 +41,10 @@ func (g *sectionGetter) String(key string) string {
|
||||
func MakeDSN(m map[string]string) (string, error) {
|
||||
b := new(strings.Builder)
|
||||
|
||||
for k, v := range m {
|
||||
ks := keys(m)
|
||||
sort.Strings(ks) // provide deterministic behaviour
|
||||
for _, k := range ks {
|
||||
v := m[k]
|
||||
if !utf8.ValidString(v) {
|
||||
return "", fmt.Errorf("value for DSN key %q: %w", k,
|
||||
ErrInvalidUTF8Sequence)
|
||||
@ -63,6 +64,14 @@ func MakeDSN(m map[string]string) (string, error) {
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func keys(m map[string]string) []string {
|
||||
ret := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
ret = append(ret, k)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func writeDSNValue(b *strings.Builder, v string) {
|
||||
numq := strings.Count(v, `'`)
|
||||
numb := strings.Count(v, `\`)
|
||||
@ -83,6 +92,9 @@ func writeDSNValue(b *strings.Builder, v string) {
|
||||
_ = b.WriteByte('\'')
|
||||
}
|
||||
|
||||
// splitHostPortDefault is similar to net.SplitHostPort, but will also accept a
|
||||
// specification with no port and apply the default port instead. It also
|
||||
// applies the given defaults if the results are empty strings.
|
||||
func splitHostPortDefault(hostport, defaultHost, defaultPort string) (string, string, error) {
|
||||
host, port, err := net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
|
108
pkg/services/store/entity/db/dbimpl/util_test.go
Normal file
108
pkg/services/store/entity/db/dbimpl/util_test.go
Normal file
@ -0,0 +1,108 @@
|
||||
package dbimpl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var invalidUTF8ByteSequence = []byte{0xff, 0xfe, 0xfd}
|
||||
|
||||
func setSectionKeyValues(section *setting.DynamicSection, m map[string]string) {
|
||||
for k, v := range m {
|
||||
section.Key(k).SetValue(v)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestSectionGetter(m map[string]string) *sectionGetter {
|
||||
section := setting.NewCfg().SectionWithEnvOverrides("entity_api")
|
||||
setSectionKeyValues(section, m)
|
||||
|
||||
return §ionGetter{
|
||||
DynamicSection: section,
|
||||
}
|
||||
}
|
||||
|
||||
func TestSectionGetter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
key = "the key"
|
||||
val = string(invalidUTF8ByteSequence)
|
||||
)
|
||||
|
||||
g := newTestSectionGetter(map[string]string{
|
||||
key: val,
|
||||
})
|
||||
|
||||
v := g.String("whatever")
|
||||
require.Empty(t, v)
|
||||
require.NoError(t, g.Err())
|
||||
|
||||
v = g.String(key)
|
||||
require.Empty(t, v)
|
||||
require.Error(t, g.Err())
|
||||
require.ErrorIs(t, g.Err(), ErrInvalidUTF8Sequence)
|
||||
}
|
||||
|
||||
func TestMakeDSN(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s, err := MakeDSN(map[string]string{
|
||||
"db_name": string(invalidUTF8ByteSequence),
|
||||
})
|
||||
require.Empty(t, s)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, ErrInvalidUTF8Sequence)
|
||||
|
||||
s, err = MakeDSN(map[string]string{
|
||||
"skip": "",
|
||||
"user": `shou'ld esc\ape`,
|
||||
"pass": "noescape",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `pass=noescape user='shou\'ld esc\\ape'`, s)
|
||||
}
|
||||
|
||||
func TestSplitHostPort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
hostport string
|
||||
defaultHost string
|
||||
defaultPort string
|
||||
fails bool
|
||||
|
||||
host string
|
||||
port string
|
||||
}{
|
||||
{hostport: "192.168.0.140:456", defaultHost: "", defaultPort: "", host: "192.168.0.140", port: "456"},
|
||||
{hostport: "192.168.0.140", defaultHost: "", defaultPort: "123", host: "192.168.0.140", port: "123"},
|
||||
{hostport: "[::1]:456", defaultHost: "", defaultPort: "", host: "::1", port: "456"},
|
||||
{hostport: "[::1]", defaultHost: "", defaultPort: "123", host: "::1", port: "123"},
|
||||
{hostport: ":456", defaultHost: "1.2.3.4", defaultPort: "", host: "1.2.3.4", port: "456"},
|
||||
{hostport: "xyz.rds.amazonaws.com", defaultHost: "", defaultPort: "123", host: "xyz.rds.amazonaws.com", port: "123"},
|
||||
{hostport: "xyz.rds.amazonaws.com:123", defaultHost: "", defaultPort: "", host: "xyz.rds.amazonaws.com", port: "123"},
|
||||
{hostport: "", defaultHost: "localhost", defaultPort: "1433", host: "localhost", port: "1433"},
|
||||
{hostport: "1:1:1", fails: true},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("test index #%d", i), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
host, port, err := splitHostPortDefault(tc.hostport, tc.defaultHost, tc.defaultPort)
|
||||
if tc.fails {
|
||||
require.Error(t, err)
|
||||
require.Empty(t, host)
|
||||
require.Empty(t, port)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.host, host)
|
||||
require.Equal(t, tc.port, port)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
SELECT {{ template "common_entity_select_into" . }}
|
||||
|
||||
FROM {{ .Ident "entity_history" }} AS e
|
||||
|
||||
WHERE 1 = 1
|
||||
|
||||
{{ if gt .Before 0 }}
|
||||
AND {{ .Ident "resource_version" }} < {{ .Arg .Before }}
|
||||
{{ end }}
|
||||
|
||||
{{/* There are two mutually exclusive search modes: by GUID and by Key */}}
|
||||
|
||||
{{ if ne .Query.GUID "" }}
|
||||
AND {{ .Ident "guid" }} = {{ .Arg .Query.GUID }}
|
||||
|
||||
{{ else }}
|
||||
AND {{ .Ident "group" }} = {{ .Arg .Query.Key.Group }}
|
||||
AND {{ .Ident "resource" }} = {{ .Arg .Query.Key.Resource }}
|
||||
AND {{ .Ident "name" }} = {{ .Arg .Query.Key.Name }}
|
||||
|
||||
{{ if ne .Query.Key.Namespace "" }}
|
||||
AND {{ .Ident "namespace" }} = {{ .Arg .Query.Key.Namespace }}
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
|
||||
ORDER BY {{ template "common_order_by" . }}
|
||||
LIMIT {{ .Limit }}
|
||||
OFFSET {{ .Offset }}
|
||||
;
|
@ -1,14 +0,0 @@
|
||||
SELECT {{ template "common_entity_select_into" . }}
|
||||
|
||||
FROM
|
||||
{{ .Ident "entity_ref" }} AS r
|
||||
INNER JOIN
|
||||
{{ .Ident "entity" }} AS e
|
||||
ON r.{{ .Ident "guid" }} = e.{{ .Ident "guid" }}
|
||||
|
||||
WHERE 1 = 1
|
||||
AND r.{{ .Ident "namespace" }} = {{ .Arg .Request.Namespace }}
|
||||
AND r.{{ .Ident "group" }} = {{ .Arg .Request.Group }}
|
||||
AND r.{{ .Ident "resource" }} = {{ .Arg .Request.Resource }}
|
||||
AND r.{{ .Ident "resolved_to" }} = {{ .Arg .Request.Name }}
|
||||
;
|
@ -100,8 +100,8 @@ func (s *sqlEntityServer) updateFolderTree(ctx context.Context, x db.ContextExec
|
||||
itemList = append(itemList, &fi)
|
||||
}
|
||||
|
||||
if err := rows.Close(); err != nil {
|
||||
return fmt.Errorf("close rows after listing folder items in namespace %q: %w", namespace, err)
|
||||
if err := rows.Err(); err != nil {
|
||||
return fmt.Errorf("rows error after listing folder items in namespace %q: %w", namespace, err)
|
||||
}
|
||||
|
||||
root, lost, err := buildFolderTree(itemList)
|
||||
|
@ -40,18 +40,14 @@ func mustTemplate(filename string) *template.Template {
|
||||
|
||||
// Templates.
|
||||
var (
|
||||
sqlEntityDelete = mustTemplate("entity_delete.sql")
|
||||
sqlEntityHistory = mustTemplate("entity_history.sql")
|
||||
//sqlEntityHistoryList = mustTemplate("entity_history_list.sql") // TODO: in upcoming PRs
|
||||
sqlEntityDelete = mustTemplate("entity_delete.sql")
|
||||
sqlEntityInsert = mustTemplate("entity_insert.sql")
|
||||
sqlEntityListFolderElements = mustTemplate("entity_list_folder_elements.sql")
|
||||
sqlEntityUpdate = mustTemplate("entity_update.sql")
|
||||
sqlEntityRead = mustTemplate("entity_read.sql")
|
||||
sqlEntityUpdate = mustTemplate("entity_update.sql")
|
||||
|
||||
sqlEntityFolderInsert = mustTemplate("entity_folder_insert.sql")
|
||||
|
||||
sqlEntityRefFind = mustTemplate("entity_ref_find.sql")
|
||||
|
||||
sqlEntityLabelsDelete = mustTemplate("entity_labels_delete.sql")
|
||||
sqlEntityLabelsInsert = mustTemplate("entity_labels_insert.sql")
|
||||
|
||||
@ -75,12 +71,18 @@ var (
|
||||
// SQLError is an error returned by the database, which includes additionally
|
||||
// debugging information about what was sent to the database.
|
||||
type SQLError struct {
|
||||
Err error
|
||||
CallType string // either Query, QueryRow or Exec
|
||||
Arguments []any
|
||||
ScanDest []any
|
||||
Query string
|
||||
RawQuery string
|
||||
Err error
|
||||
CallType string // either Query, QueryRow or Exec
|
||||
TemplateName string
|
||||
Query string
|
||||
RawQuery string
|
||||
ScanDest []any
|
||||
|
||||
// potentially regulated information is not exported and only directly
|
||||
// available for local testing and local debugging purposes, making sure it
|
||||
// is never marshaled to JSON or any other serialization.
|
||||
|
||||
arguments []any
|
||||
}
|
||||
|
||||
func (e SQLError) Unwrap() error {
|
||||
@ -88,20 +90,9 @@ func (e SQLError) Unwrap() error {
|
||||
}
|
||||
|
||||
func (e SQLError) Error() string {
|
||||
return fmt.Sprintf("calling %s in database: %v", e.CallType, e.Err)
|
||||
}
|
||||
|
||||
func (e SQLError) Debug() string {
|
||||
scanDestStr := "(none)"
|
||||
if len(e.ScanDest) > 0 {
|
||||
format := "[%T" + strings.Repeat(", %T", len(e.ScanDest)-1) + "]"
|
||||
scanDestStr = fmt.Sprintf(format, e.ScanDest...)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("call %s in database: %v\n\tArguments (%d): %#v\n\t"+
|
||||
"Return Value Types (%d): %s\n\tExecuted Query: %s\n\tRaw SQL "+
|
||||
"Template Output: %s", e.CallType, e.Err, len(e.Arguments), e.Arguments,
|
||||
len(e.ScanDest), scanDestStr, e.Query, e.RawQuery)
|
||||
return fmt.Sprintf("%s: %s with %d input arguments and %d output "+
|
||||
"destination arguments: %v", e.TemplateName, e.CallType,
|
||||
len(e.arguments), len(e.ScanDest), e.Err)
|
||||
}
|
||||
|
||||
// entity_folder table requests.
|
||||
@ -111,6 +102,10 @@ type sqlEntityFolderInsertRequest struct {
|
||||
Items []*sqlEntityFolderInsertRequestItem
|
||||
}
|
||||
|
||||
func (r sqlEntityFolderInsertRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
type sqlEntityFolderInsertRequestItem struct {
|
||||
GUID string
|
||||
Namespace string
|
||||
@ -123,14 +118,6 @@ type sqlEntityFolderInsertRequestItem struct {
|
||||
Detached bool
|
||||
}
|
||||
|
||||
// entity_ref table requests.
|
||||
|
||||
type sqlEntityRefFindRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
Request *entity.ReferenceRequest
|
||||
returnsEntitySet
|
||||
}
|
||||
|
||||
// entity_labels table requests.
|
||||
|
||||
type sqlEntityLabelsInsertRequest struct {
|
||||
@ -139,12 +126,20 @@ type sqlEntityLabelsInsertRequest struct {
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
func (r sqlEntityLabelsInsertRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
type sqlEntityLabelsDeleteRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
GUID string
|
||||
KeepLabels []string
|
||||
}
|
||||
|
||||
func (r sqlEntityLabelsDeleteRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
// entity_kind table requests.
|
||||
|
||||
type returnsKindVersion struct {
|
||||
@ -164,6 +159,10 @@ type sqlKindVersionGetRequest struct {
|
||||
*returnsKindVersion
|
||||
}
|
||||
|
||||
func (r sqlKindVersionGetRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
type sqlKindVersionLockRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
Group string
|
||||
@ -171,6 +170,10 @@ type sqlKindVersionLockRequest struct {
|
||||
*returnsKindVersion
|
||||
}
|
||||
|
||||
func (r sqlKindVersionLockRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
type sqlKindVersionIncRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
Group string
|
||||
@ -179,6 +182,10 @@ type sqlKindVersionIncRequest struct {
|
||||
UpdatedAt int64
|
||||
}
|
||||
|
||||
func (r sqlKindVersionIncRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
type sqlKindVersionInsertRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
Group string
|
||||
@ -187,6 +194,10 @@ type sqlKindVersionInsertRequest struct {
|
||||
UpdatedAt int64
|
||||
}
|
||||
|
||||
func (r sqlKindVersionInsertRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
// entity and entity_history tables requests.
|
||||
|
||||
type sqlEntityListFolderElementsRequest struct {
|
||||
@ -197,6 +208,10 @@ type sqlEntityListFolderElementsRequest struct {
|
||||
FolderInfo *folderInfo
|
||||
}
|
||||
|
||||
func (r sqlEntityListFolderElementsRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
// sqlEntityReadRequest can be used to retrieve a row from either the "entity"
|
||||
// or the "entity_history" tables. In particular, don't use this template
|
||||
// directly. Instead, use the readEntity function, which provides all common use
|
||||
@ -209,21 +224,17 @@ type sqlEntityReadRequest struct {
|
||||
returnsEntitySet
|
||||
}
|
||||
|
||||
func (r sqlEntityReadRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
type sqlEntityDeleteRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
Key *entity.Key
|
||||
}
|
||||
|
||||
type sqlEntityHistoryRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
//historyToken // TODO: coming in another PR
|
||||
returnsEntitySet
|
||||
}
|
||||
|
||||
type sqlEntityHistoryListRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
//hitoryListToken // TODO: coming in another PR
|
||||
returnsEntitySet
|
||||
func (r sqlEntityDeleteRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
type sqlEntityInsertRequest struct {
|
||||
@ -235,16 +246,33 @@ type sqlEntityInsertRequest struct {
|
||||
TableEntity bool
|
||||
}
|
||||
|
||||
func (r sqlEntityInsertRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
type sqlEntityUpdateRequest struct {
|
||||
*sqltemplate.SQLTemplate
|
||||
Entity *returnsEntity
|
||||
}
|
||||
|
||||
func (r sqlEntityUpdateRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
// newEmptyEntity allocates a new entity.Entity and all its internal state to be
|
||||
// ready for use.
|
||||
func newEmptyEntity() *entity.Entity {
|
||||
return &entity.Entity{
|
||||
// we need to allocate all internal pointer types so that they
|
||||
// are readily available to be populated in the template
|
||||
Origin: new(entity.EntityOriginInfo),
|
||||
|
||||
// we also set default empty values in slices and maps instead of nil to
|
||||
// provide the most consistent JSON representation fields that will be
|
||||
// serialized this way to the database.
|
||||
Labels: map[string]string{},
|
||||
Fields: map[string]string{},
|
||||
Errors: []*entity.EntityErrorInfo{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,6 +286,36 @@ func cloneEntity(src *entity.Entity) *entity.Entity {
|
||||
// returnsEntitySet can be embedded in a request struct to provide automatic set
|
||||
// returning of []*entity.Entity from the database, deserializing as needed. It
|
||||
// should be embedded as a value type.
|
||||
// Example struct:
|
||||
//
|
||||
// type sqlMyRequest struct {
|
||||
// *sqltemplate.SQLTemplate
|
||||
// returnsEntitySet // embedded value type, not pointer type
|
||||
// GUID string // example argument
|
||||
// MaxResourceVersion int // example argument
|
||||
// }
|
||||
//
|
||||
// Example struct usage::
|
||||
//
|
||||
// req := sqlMyRequest{
|
||||
// SQLTemplate: sqltemplate.New(myDialect),
|
||||
// returnsEntitySet: newReturnsEntitySet(),
|
||||
// GUID: "abc",
|
||||
// MaxResourceVersion: 1,
|
||||
// }
|
||||
// entities, err := query(myTx, myTmpl, req)
|
||||
//
|
||||
// Example usage in SQL template:
|
||||
//
|
||||
// SELECT
|
||||
// {{ .Ident "guid" | .Into .Entity.Guid }},
|
||||
// {{ .Ident "resource_version" | .Into .Entity.ResourceVersion }},
|
||||
// {{ .Ident "body" | .Into .Entity.Body }}
|
||||
// FROM {{ .Ident "entity_history" }}
|
||||
// WHERE 1 = 1
|
||||
// AND {{ .Ident "guid" }} = {{ .Arg .GUID }}
|
||||
// AND {{ .Ident "resource_version" }} <= {{ .Arg .MaxResourceVersion }}
|
||||
// ;
|
||||
type returnsEntitySet struct {
|
||||
Entity *returnsEntity
|
||||
}
|
||||
@ -278,13 +336,46 @@ func (e returnsEntitySet) Results() (*entity.Entity, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return proto.Clone(ent).(*entity.Entity), nil
|
||||
return cloneEntity(ent), nil
|
||||
}
|
||||
|
||||
// returnsEntity is a wrapper that aids with database (de)serialization. It
|
||||
// embeds a *entity.Entity to provide transparent access to all its fields, but
|
||||
// overrides the ones that need database (de)serialization. It should be a named
|
||||
// field in your request struct, with pointer type.
|
||||
// Example struct:
|
||||
//
|
||||
// type sqlMyRequest struct {
|
||||
// *sqltemplate.SQLTemplate
|
||||
// Entity *returnsEntity // named field with pointer type
|
||||
// GUID string // example argument
|
||||
// ResourceVersion int // example argument
|
||||
// }
|
||||
//
|
||||
// Example struct usage:
|
||||
//
|
||||
// req := sqlMyRequest{
|
||||
// SQLTemplate: sqltemplate.New(myDialect),
|
||||
// Entity: newReturnsEntity(),
|
||||
// GUID: "abc",
|
||||
// ResourceVersion: 1,
|
||||
// }
|
||||
// err := queryRow(myTx, myTmpl, req)
|
||||
// // check err here
|
||||
// err = req.Entity.unmarshal()
|
||||
// // check err, and you can now use req.Entity.Entity
|
||||
//
|
||||
// Example usage in SQL template:
|
||||
//
|
||||
// SELECT
|
||||
// {{ .Ident "guid" | .Into .Entity.Guid }},
|
||||
// {{ .Ident "resource_version" | .Into .Entity.ResourceVersion }},
|
||||
// {{ .Ident "body" | .Into .Entity.Body }}
|
||||
// FROM {{ .Ident "entity" }}
|
||||
// WHERE 1 =1
|
||||
// AND {{ .Ident "guid" }} = {{ .Arg .GUID }}
|
||||
// AND {{ .Ident "resource_version" }} = {{ .Arg .ResourceVersion }}
|
||||
// ;
|
||||
type returnsEntity struct {
|
||||
*entity.Entity
|
||||
Labels []byte
|
||||
@ -348,23 +439,42 @@ func (e *returnsEntity) unmarshal() error {
|
||||
if err := json.Unmarshal(e.Labels, &e.Entity.Labels); err != nil {
|
||||
return fmt.Errorf("deserialize entity \"labels\" field: %w", err)
|
||||
}
|
||||
} else {
|
||||
e.Entity.Labels = map[string]string{}
|
||||
}
|
||||
|
||||
if len(e.Fields) > 0 {
|
||||
if err := json.Unmarshal(e.Fields, &e.Entity.Fields); err != nil {
|
||||
return fmt.Errorf("deserialize entity \"fields\" field: %w", err)
|
||||
}
|
||||
} else {
|
||||
e.Entity.Fields = map[string]string{}
|
||||
}
|
||||
|
||||
if len(e.Errors) > 0 {
|
||||
if err := json.Unmarshal(e.Errors, &e.Entity.Errors); err != nil {
|
||||
return fmt.Errorf("deserialize entity \"errors\" field: %w", err)
|
||||
}
|
||||
} else {
|
||||
e.Entity.Errors = []*entity.EntityErrorInfo{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readEntity returns the entity defined by the given key as it existed at
|
||||
// version `asOfVersion`, if that value is greater than zero. The returned
|
||||
// entity will have at most that version. If `asOfVersion` is zero, then the
|
||||
// current version of that entity will be returned. If `optimisticLocking` is
|
||||
// true, then the latest version of the entity will be retrieved and return an
|
||||
// error if its version is not exactly `asOfVersion`. The option
|
||||
// `selectForUpdate` will cause to acquire a row-level exclusive lock upon
|
||||
// selecting it. `optimisticLocking` is ignored if `asOfVersion` is zero.
|
||||
// Common errors to check:
|
||||
// 1. ErrOptimisticLockingFailed: the latest version of the entity does not
|
||||
// match the value of `asOfVersion`.
|
||||
// 2. ErrNotFound: the entity does not currently exist, did not exist at the
|
||||
// version of `asOfVersion` or was deleted.
|
||||
func readEntity(
|
||||
ctx context.Context,
|
||||
x db.ContextExecer,
|
||||
@ -374,12 +484,8 @@ func readEntity(
|
||||
optimisticLocking bool,
|
||||
selectForUpdate bool,
|
||||
) (*returnsEntity, error) {
|
||||
if asOfVersion < 0 {
|
||||
asOfVersion = 0
|
||||
}
|
||||
if asOfVersion == 0 {
|
||||
optimisticLocking = false
|
||||
}
|
||||
asOfVersion = max(asOfVersion, 0)
|
||||
optimisticLocking = optimisticLocking && asOfVersion != 0
|
||||
|
||||
v := asOfVersion
|
||||
if optimisticLocking {
|
||||
@ -437,7 +543,7 @@ func kindVersionAtomicInc(ctx context.Context, x db.ContextExecer, d sqltemplate
|
||||
// a new (Group, Resource) to the cell, which should be very unlikely,
|
||||
// and the workaround is simply retrying. The alternative would be to
|
||||
// use INSERT ... ON CONFLICT DO UPDATE ..., but that creates a
|
||||
// requirement for support in Dialect only for this marginal case, but
|
||||
// requirement for support in Dialect only for this marginal case, and
|
||||
// we would rather keep Dialect as small as possible. Another
|
||||
// alternative is to simply check if the INSERT returns a DUPLICATE KEY
|
||||
// error and then retry the original SELECT, but that also adds some
|
||||
|
821
pkg/services/store/entity/sqlstash/queries_test.go
Normal file
821
pkg/services/store/entity/sqlstash/queries_test.go
Normal file
@ -0,0 +1,821 @@
|
||||
package sqlstash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
sqlmock "github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||
"github.com/grafana/grafana/pkg/services/store/entity/db"
|
||||
"github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
|
||||
// debug is meant to provide greater debugging detail about certain errors. The
|
||||
// returned error will either provide more detailed information or be the same
|
||||
// original error, suitable only for local debugging. The details provided are
|
||||
// not meant to be logged, since they could include PII or otherwise
|
||||
// sensitive/confidential information. These information should only be used for
|
||||
// local debugging with fake or otherwise non-regulated information.
|
||||
func debug(err error) error {
|
||||
var d interface{ Debug() string }
|
||||
if errors.As(err, &d) {
|
||||
return errors.New(d.Debug())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ = debug // silence the `unused` linter
|
||||
|
||||
//go:embed testdata/*
|
||||
var testdataFS embed.FS
|
||||
|
||||
func testdata(t *testing.T, filename string) []byte {
|
||||
t.Helper()
|
||||
b, err := testdataFS.ReadFile(`testdata/` + filename)
|
||||
require.NoError(t, err)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func testdataJSON(t *testing.T, filename string, dest any) {
|
||||
t.Helper()
|
||||
b := testdata(t, filename)
|
||||
err := json.Unmarshal(b, dest)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestQueries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Each template has one or more test cases, each identified with a
|
||||
// descriptive name (e.g. "happy path", "error twiddling the frobb"). Each
|
||||
// of them will test that for the same input data they must produce a result
|
||||
// that will depend on the Dialect. Expected queries should be defined in
|
||||
// separate files in the testdata directory. This improves the testing
|
||||
// experience by separating test data from test code, since mixing both
|
||||
// tends to make it more difficult to reason about what is being done,
|
||||
// especially as we want testing code to scale and make it easy to add
|
||||
// tests.
|
||||
type (
|
||||
// type aliases to make code more semantic and self-documenting
|
||||
resultSQLFilename = string
|
||||
dialects = []sqltemplate.Dialect
|
||||
expected map[resultSQLFilename]dialects
|
||||
|
||||
testCase = struct {
|
||||
Name string
|
||||
|
||||
// Data should be the struct passed to the template.
|
||||
Data sqltemplate.SQLTemplateIface
|
||||
|
||||
// Expected maps the filename containing the expected result query
|
||||
// to the list of dialects that would produce it. For simple
|
||||
// queries, it is possible that more than one dialect produce the
|
||||
// same output. The filename is expected to be in the `testdata`
|
||||
// directory.
|
||||
Expected expected
|
||||
}
|
||||
)
|
||||
|
||||
// Define tests cases. Most templates are trivial and testing that they
|
||||
// generate correct code for a single Dialect is fine, since the one thing
|
||||
// that always changes is how SQL placeholder arguments are passed (most
|
||||
// Dialects use `?` while PostgreSQL uses `$1`, `$2`, etc.), and that is
|
||||
// something that should be tested in the Dialect implementation instead of
|
||||
// here. We will ask to have at least one test per SQL template, and we will
|
||||
// lean to test MySQL. Templates containing branching (conditionals, loops,
|
||||
// etc.) should be exercised at least once in each of their branches.
|
||||
//
|
||||
// NOTE: in the Data field, make sure to have pointers populated to simulate
|
||||
// data is set as it would be in a real request. The data being correctly
|
||||
// populated in each case should be tested in integration tests, where the
|
||||
// data will actually flow to and from a real database. In this tests we
|
||||
// only care about producing the correct SQL.
|
||||
testCases := map[*template.Template][]*testCase{
|
||||
sqlEntityDelete: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlEntityDeleteRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Key: new(entity.Key),
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_delete_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
"entity_delete_postgres.sql": dialects{
|
||||
sqltemplate.PostgreSQL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlEntityInsert: {
|
||||
{
|
||||
Name: "insert into entity",
|
||||
Data: &sqlEntityInsertRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Entity: newReturnsEntity(),
|
||||
TableEntity: true,
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_insert_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "insert into entity_history",
|
||||
Data: &sqlEntityInsertRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Entity: newReturnsEntity(),
|
||||
TableEntity: false,
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_history_insert_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlEntityListFolderElements: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlEntityListFolderElementsRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
FolderInfo: new(folderInfo),
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_list_folder_elements_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlEntityRead: {
|
||||
{
|
||||
Name: "with resource version and select for update",
|
||||
Data: &sqlEntityReadRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Key: new(entity.Key),
|
||||
ResourceVersion: 1,
|
||||
SelectForUpdate: true,
|
||||
returnsEntitySet: returnsEntitySet{
|
||||
Entity: newReturnsEntity(),
|
||||
},
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_history_read_full_mysql.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "without resource version and select for update",
|
||||
Data: &sqlEntityReadRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Key: new(entity.Key),
|
||||
returnsEntitySet: returnsEntitySet{
|
||||
Entity: newReturnsEntity(),
|
||||
},
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_read_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlEntityUpdate: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlEntityUpdateRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Entity: newReturnsEntity(),
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_update_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlEntityFolderInsert: {
|
||||
{
|
||||
Name: "one item",
|
||||
Data: &sqlEntityFolderInsertRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Items: []*sqlEntityFolderInsertRequestItem{{}},
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_folder_insert_1_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "two items",
|
||||
Data: &sqlEntityFolderInsertRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Items: []*sqlEntityFolderInsertRequestItem{{}, {}},
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_folder_insert_2_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlEntityLabelsDelete: {
|
||||
{
|
||||
Name: "one element",
|
||||
Data: &sqlEntityLabelsDeleteRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
KeepLabels: []string{"one"},
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_labels_delete_1_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "two elements",
|
||||
Data: &sqlEntityLabelsDeleteRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
KeepLabels: []string{"one", "two"},
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_labels_delete_2_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlEntityLabelsInsert: {
|
||||
{
|
||||
Name: "one element",
|
||||
Data: &sqlEntityLabelsInsertRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Labels: map[string]string{"lbl1": "val1"},
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_labels_insert_1_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "two elements",
|
||||
Data: &sqlEntityLabelsInsertRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
Labels: map[string]string{"lbl1": "val1", "lbl2": "val2"},
|
||||
},
|
||||
Expected: expected{
|
||||
"entity_labels_insert_2_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlKindVersionGet: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlKindVersionGetRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
returnsKindVersion: new(returnsKindVersion),
|
||||
},
|
||||
Expected: expected{
|
||||
"kind_version_get_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlKindVersionInc: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlKindVersionIncRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
},
|
||||
Expected: expected{
|
||||
"kind_version_inc_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlKindVersionInsert: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlKindVersionInsertRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
},
|
||||
Expected: expected{
|
||||
"kind_version_insert_mysql_sqlite.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlKindVersionLock: {
|
||||
{
|
||||
Name: "single path",
|
||||
Data: &sqlKindVersionLockRequest{
|
||||
SQLTemplate: new(sqltemplate.SQLTemplate),
|
||||
returnsKindVersion: new(returnsKindVersion),
|
||||
},
|
||||
Expected: expected{
|
||||
"kind_version_lock_mysql.sql": dialects{
|
||||
sqltemplate.MySQL,
|
||||
},
|
||||
"kind_version_lock_postgres.sql": dialects{
|
||||
sqltemplate.PostgreSQL,
|
||||
},
|
||||
"kind_version_lock_sqlite.sql": dialects{
|
||||
sqltemplate.SQLite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Execute test cases
|
||||
for tmpl, tcs := range testCases {
|
||||
t.Run(tmpl.Name(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for filename, ds := range tc.Expected {
|
||||
t.Run(filename, func(t *testing.T) {
|
||||
// not parallel because we're sharing tc.Data, not
|
||||
// worth it deep cloning
|
||||
|
||||
rawQuery := string(testdata(t, filename))
|
||||
expectedQuery := sqltemplate.FormatSQL(rawQuery)
|
||||
|
||||
for _, d := range ds {
|
||||
t.Run(d.Name(), func(t *testing.T) {
|
||||
// not parallel for the same reason
|
||||
|
||||
tc.Data.SetDialect(d)
|
||||
err := tc.Data.Validate()
|
||||
require.NoError(t, err)
|
||||
got, err := sqltemplate.Execute(tmpl, tc.Data)
|
||||
require.NoError(t, err)
|
||||
got = sqltemplate.FormatSQL(got)
|
||||
require.Equal(t, expectedQuery, got)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReturnsEntity_marshal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test data for maps
|
||||
someMap := map[string]string{
|
||||
"alpha": "aleph",
|
||||
"beta": "beth",
|
||||
}
|
||||
someMapJSONb, err := json.Marshal(someMap)
|
||||
require.NoError(t, err)
|
||||
someMapJSON := string(someMapJSONb)
|
||||
|
||||
// test data for errors
|
||||
someErrors := []*entity.EntityErrorInfo{
|
||||
{
|
||||
Code: 1,
|
||||
Message: "not cool",
|
||||
DetailsJson: []byte(`"nothing to add"`),
|
||||
},
|
||||
}
|
||||
someErrorsJSONb, err := json.Marshal(someErrors)
|
||||
require.NoError(t, err)
|
||||
someErrorsJSON := string(someErrorsJSONb)
|
||||
|
||||
t.Run("happy path - nothing to marshal", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
d := &returnsEntity{
|
||||
Entity: &entity.Entity{
|
||||
Labels: map[string]string{},
|
||||
Fields: map[string]string{},
|
||||
Errors: []*entity.EntityErrorInfo{},
|
||||
},
|
||||
}
|
||||
err := d.marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.JSONEq(t, `{}`, string(d.Labels))
|
||||
require.JSONEq(t, `{}`, string(d.Fields))
|
||||
require.JSONEq(t, `[]`, string(d.Errors))
|
||||
|
||||
// nil Go Object/Slice map to empty JSON Object/Array for consistency
|
||||
|
||||
d.Entity = new(entity.Entity)
|
||||
err = d.marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.JSONEq(t, `{}`, string(d.Labels))
|
||||
require.JSONEq(t, `{}`, string(d.Fields))
|
||||
require.JSONEq(t, `[]`, string(d.Errors))
|
||||
})
|
||||
|
||||
t.Run("happy path - everything to marshal", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
d := &returnsEntity{
|
||||
Entity: &entity.Entity{
|
||||
Labels: someMap,
|
||||
Fields: someMap,
|
||||
Errors: someErrors,
|
||||
},
|
||||
}
|
||||
err := d.marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.JSONEq(t, someMapJSON, string(d.Labels))
|
||||
require.JSONEq(t, someMapJSON, string(d.Fields))
|
||||
require.JSONEq(t, someErrorsJSON, string(d.Errors))
|
||||
})
|
||||
|
||||
// NOTE: the error path for serialization is apparently unreachable. If you
|
||||
// find a way to simulate a serialization error, consider raising awareness
|
||||
// of such case(s) and add the corresponding tests here
|
||||
}
|
||||
|
||||
func TestReturnsEntity_unmarshal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("happy path - nothing to unmarshal", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := newReturnsEntity()
|
||||
err := e.unmarshal()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, e.Entity.Labels)
|
||||
require.NotNil(t, e.Entity.Fields)
|
||||
require.NotNil(t, e.Entity.Errors)
|
||||
})
|
||||
|
||||
t.Run("happy path - everything to unmarshal", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := newReturnsEntity()
|
||||
e.Labels = []byte(`{}`)
|
||||
e.Fields = []byte(`{}`)
|
||||
e.Errors = []byte(`[]`)
|
||||
err := e.unmarshal()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, e.Entity.Labels)
|
||||
require.NotNil(t, e.Entity.Fields)
|
||||
require.NotNil(t, e.Entity.Errors)
|
||||
})
|
||||
|
||||
t.Run("fail to unmarshal", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var jsonInvalid = []byte(`.`)
|
||||
|
||||
e := newReturnsEntity()
|
||||
e.Labels = jsonInvalid
|
||||
err := e.unmarshal()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "labels")
|
||||
|
||||
e = newReturnsEntity()
|
||||
e.Labels = nil
|
||||
e.Fields = jsonInvalid
|
||||
err = e.unmarshal()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "fields")
|
||||
|
||||
e = newReturnsEntity()
|
||||
e.Fields = nil
|
||||
e.Errors = jsonInvalid
|
||||
err = e.unmarshal()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "errors")
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadEntity(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// readonly, shared data for all subtests
|
||||
expectedEntity := newEmptyEntity()
|
||||
testdataJSON(t, `grpc-res-entity.json`, expectedEntity)
|
||||
key, err := entity.ParseKey(expectedEntity.Key)
|
||||
require.NoErrorf(t, err, "provided key: %#v", expectedEntity)
|
||||
|
||||
t.Run("happy path - entity table, optimistic locking", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
db, mock := newMockDBMatchWords(t)
|
||||
x := expectReadEntity(t, mock, cloneEntity(expectedEntity))
|
||||
x(ctx, db)
|
||||
})
|
||||
|
||||
t.Run("happy path - entity table, no optimistic locking", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
db, mock := newMockDBMatchWords(t)
|
||||
readReq := sqlEntityReadRequest{ // used to generate mock results
|
||||
SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
|
||||
Key: new(entity.Key),
|
||||
returnsEntitySet: newReturnsEntitySet(),
|
||||
}
|
||||
readReq.Entity.Entity = cloneEntity(expectedEntity)
|
||||
results := newMockResults(t, mock, sqlEntityRead, readReq)
|
||||
|
||||
// setup expectations
|
||||
results.AddCurrentData()
|
||||
mock.ExpectQuery(`select from entity where !resource_version update`).
|
||||
WillReturnRows(results.Rows())
|
||||
|
||||
// execute and assert
|
||||
e, err := readEntity(ctx, db, sqltemplate.MySQL, key, 0, false, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedEntity, e.Entity)
|
||||
})
|
||||
|
||||
t.Run("happy path - entity_history table", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
db, mock := newMockDBMatchWords(t)
|
||||
readReq := sqlEntityReadRequest{ // used to generate mock results
|
||||
SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
|
||||
Key: new(entity.Key),
|
||||
returnsEntitySet: newReturnsEntitySet(),
|
||||
}
|
||||
readReq.Entity.Entity = cloneEntity(expectedEntity)
|
||||
results := newMockResults(t, mock, sqlEntityRead, readReq)
|
||||
|
||||
// setup expectations
|
||||
results.AddCurrentData()
|
||||
mock.ExpectQuery(`select from entity_history where resource_version !update`).
|
||||
WillReturnRows(results.Rows())
|
||||
|
||||
// execute and assert
|
||||
e, err := readEntity(ctx, db, sqltemplate.MySQL, key,
|
||||
expectedEntity.ResourceVersion, false, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedEntity, e.Entity)
|
||||
})
|
||||
|
||||
t.Run("entity table, optimistic locking failed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
db, mock := newMockDBMatchWords(t)
|
||||
x := expectReadEntity(t, mock, nil)
|
||||
x(ctx, db)
|
||||
})
|
||||
|
||||
t.Run("entity_history table, entity not found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
db, mock := newMockDBMatchWords(t)
|
||||
readReq := sqlEntityReadRequest{ // used to generate mock results
|
||||
SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
|
||||
Key: new(entity.Key),
|
||||
returnsEntitySet: newReturnsEntitySet(),
|
||||
}
|
||||
results := newMockResults(t, mock, sqlEntityRead, readReq)
|
||||
|
||||
// setup expectations
|
||||
mock.ExpectQuery(`select from entity_history where resource_version !update`).
|
||||
WillReturnRows(results.Rows())
|
||||
|
||||
// execute and assert
|
||||
e, err := readEntity(ctx, db, sqltemplate.MySQL, key,
|
||||
expectedEntity.ResourceVersion, false, false)
|
||||
require.Nil(t, e)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("entity_history table, entity was deleted = not found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
db, mock := newMockDBMatchWords(t)
|
||||
readReq := sqlEntityReadRequest{ // used to generate mock results
|
||||
SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
|
||||
Key: new(entity.Key),
|
||||
returnsEntitySet: newReturnsEntitySet(),
|
||||
}
|
||||
readReq.Entity.Entity = cloneEntity(expectedEntity)
|
||||
readReq.Entity.Entity.Action = entity.Entity_DELETED
|
||||
results := newMockResults(t, mock, sqlEntityRead, readReq)
|
||||
|
||||
// setup expectations
|
||||
results.AddCurrentData()
|
||||
mock.ExpectQuery(`select from entity_history where resource_version !update`).
|
||||
WillReturnRows(results.Rows())
|
||||
|
||||
// execute and assert
|
||||
e, err := readEntity(ctx, db, sqltemplate.MySQL, key,
|
||||
expectedEntity.ResourceVersion, false, false)
|
||||
require.Nil(t, e)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, ErrNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
// expectReadEntity arranges test expectations so that it's easier to reuse
|
||||
// across tests that need to call `readEntity`. If you provide a non-nil
|
||||
// *entity.Entity, that will be returned by `readEntity`. If it's nil, then
|
||||
// `readEntity` will return ErrOptimisticLockingFailed. It returns the function
|
||||
// to execute the actual test and assert the expectations that were set.
|
||||
func expectReadEntity(t *testing.T, mock sqlmock.Sqlmock, e *entity.Entity) func(ctx context.Context, db db.DB) {
|
||||
t.Helper()
|
||||
|
||||
// test declarations
|
||||
readReq := sqlEntityReadRequest{ // used to generate mock results
|
||||
SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
|
||||
Key: new(entity.Key),
|
||||
returnsEntitySet: newReturnsEntitySet(),
|
||||
}
|
||||
results := newMockResults(t, mock, sqlEntityRead, readReq)
|
||||
if e != nil {
|
||||
readReq.Entity.Entity = cloneEntity(e)
|
||||
}
|
||||
|
||||
// setup expectations
|
||||
results.AddCurrentData()
|
||||
mock.ExpectQuery(`select from entity where !resource_version update`).
|
||||
WillReturnRows(results.Rows())
|
||||
|
||||
// execute and assert
|
||||
if e != nil {
|
||||
return func(ctx context.Context, db db.DB) {
|
||||
ent, err := readEntity(ctx, db, sqltemplate.MySQL, readReq.Key,
|
||||
e.ResourceVersion, true, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, e, ent.Entity)
|
||||
}
|
||||
}
|
||||
|
||||
return func(ctx context.Context, db db.DB) {
|
||||
ent, err := readEntity(ctx, db, sqltemplate.MySQL, readReq.Key, 1, true,
|
||||
true)
|
||||
require.Nil(t, ent)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, ErrOptimisticLockingFailed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKindVersionAtomicInc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("happy path - row locked", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test declarations
|
||||
const curVersion int64 = 1
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
db, mock := newMockDBMatchWords(t)
|
||||
|
||||
// setup expectations
|
||||
mock.ExpectQuery(`select resource_version from kind_version where group resource update`).
|
||||
WillReturnRows(mock.NewRows([]string{"resource_version"}).AddRow(curVersion))
|
||||
mock.ExpectExec("update kind_version set resource_version updated_at where group resource").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
// execute and assert
|
||||
gotVersion, err := kindVersionAtomicInc(ctx, db, sqltemplate.MySQL, "groupname", "resname")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, curVersion+1, gotVersion)
|
||||
})
|
||||
|
||||
t.Run("happy path - row created", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
db, mock := newMockDBMatchWords(t)
|
||||
x := expectKindVersionAtomicInc(t, mock, false)
|
||||
x(ctx, db)
|
||||
})
|
||||
|
||||
t.Run("fail to create row", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
db, mock := newMockDBMatchWords(t)
|
||||
x := expectKindVersionAtomicInc(t, mock, true)
|
||||
x(ctx, db)
|
||||
})
|
||||
}
|
||||
|
||||
// expectKindVersionAtomicInc arranges test expectations so that it's easier to
|
||||
// reuse across tests that need to call `kindVersionAtomicInc`. If you the test
|
||||
// shuld fail, it will do so with `errTest`, and it will return resource version
|
||||
// 1 otherwise. It returns the function to execute the actual test and assert
|
||||
// the expectations that were set.
|
||||
func expectKindVersionAtomicInc(t *testing.T, mock sqlmock.Sqlmock, shouldFail bool) func(ctx context.Context, db db.DB) {
|
||||
t.Helper()
|
||||
|
||||
// setup expectations
|
||||
mock.ExpectQuery(`select resource_version from kind_version where group resource update`).
|
||||
WillReturnRows(mock.NewRows([]string{"resource_version"}))
|
||||
call := mock.ExpectExec("insert kind_version resource_version")
|
||||
|
||||
// execute and assert
|
||||
if shouldFail {
|
||||
call.WillReturnError(errTest)
|
||||
|
||||
return func(ctx context.Context, db db.DB) {
|
||||
gotVersion, err := kindVersionAtomicInc(ctx, db, sqltemplate.MySQL, "groupname", "resname")
|
||||
require.Zero(t, gotVersion)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errTest)
|
||||
}
|
||||
}
|
||||
call.WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
return func(ctx context.Context, db db.DB) {
|
||||
gotVersion, err := kindVersionAtomicInc(ctx, db, sqltemplate.MySQL, "groupname", "resname")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), gotVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.Panics(t, func() {
|
||||
mustTemplate("non existent file")
|
||||
})
|
||||
}
|
||||
|
||||
// Debug provides greater detail about the SQL error. It is defined on the same
|
||||
// struct but on a test file so that the intention that its results should not
|
||||
// be used in runtime code is very clear. The results could include PII or
|
||||
// otherwise regulated information, hence this method is only available in
|
||||
// tests, so that it can be used in local debugging only. Note that the error
|
||||
// information may still be available through other means, like using the
|
||||
// "reflect" package, so care must be taken not to ever expose these information
|
||||
// in production.
|
||||
func (e SQLError) Debug() string {
|
||||
scanDestStr := "(none)"
|
||||
if len(e.ScanDest) > 0 {
|
||||
format := "[%T" + strings.Repeat(", %T", len(e.ScanDest)-1) + "]"
|
||||
scanDestStr = fmt.Sprintf(format, e.ScanDest...)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s: %s: %v\n\tArguments (%d): %#v\n\tReturn Value "+
|
||||
"Types (%d): %s\n\tExecuted Query: %s\n\tRaw SQL Template Output: %s",
|
||||
e.TemplateName, e.CallType, e.Err, len(e.arguments), e.arguments,
|
||||
len(e.ScanDest), scanDestStr, e.Query, e.RawQuery)
|
||||
}
|
@ -535,19 +535,6 @@ type SortBy struct {
|
||||
Direction Direction
|
||||
}
|
||||
|
||||
func parseAllSortBy(s []string) ([]SortBy, error) {
|
||||
ret := make([]SortBy, len(s))
|
||||
for i, v := range s {
|
||||
ss, err := ParseSortBy(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse #%d-eth sort item: %w", i, err)
|
||||
}
|
||||
ret[i] = ss
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func ParseSortBy(sort string) (SortBy, error) {
|
||||
sortBy := SortBy{
|
||||
Field: "guid",
|
||||
|
@ -1,65 +1,28 @@
|
||||
package sqlstash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
oldDB "github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||
"github.com/grafana/grafana/pkg/services/store/entity/db/dbimpl"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
func TestIsHealthy(t *testing.T) {
|
||||
s := setUpTestServer(t)
|
||||
t.Parallel()
|
||||
|
||||
_, err := s.IsHealthy(context.Background(), &entity.HealthCheckRequest{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func setUpTestServer(t *testing.T) entity.EntityStoreServer {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
// test declarations
|
||||
ctx := testutil.NewDefaultTestContext(t)
|
||||
db, mock := newMockDBNopSQL(t)
|
||||
s := &sqlEntityServer{
|
||||
sqlDB: db,
|
||||
}
|
||||
sqlStore, cfg := oldDB.InitTestDBWithCfg(t)
|
||||
|
||||
entityDB, err := dbimpl.ProvideEntityDB(
|
||||
sqlStore,
|
||||
cfg,
|
||||
featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorage),
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
// setup expectations
|
||||
mock.ExpectPing()
|
||||
|
||||
traceConfig, err := tracing.ParseTracingConfig(cfg)
|
||||
// execute and assert
|
||||
_, err := s.IsHealthy(ctx, new(entity.HealthCheckRequest))
|
||||
require.NoError(t, err)
|
||||
tracer, err := tracing.ProvideService(traceConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
s, err := ProvideSQLEntityServer(entityDB, tracer)
|
||||
require.NoError(t, err)
|
||||
return s
|
||||
}
|
||||
|
||||
// TODO: remove all the following once the Proposal 1 for Consistent Resource
|
||||
// Version is finished.
|
||||
var (
|
||||
_ = parseAllSortBy
|
||||
_ = countTrue
|
||||
_ = query[any]
|
||||
_ = sqlEntityHistory
|
||||
_ = sqlEntityRefFind
|
||||
_ = sqlKindVersionGet
|
||||
_ = sqlEntityRefFindRequest{}
|
||||
_ = sqlKindVersionGetRequest{}
|
||||
_ = sqlEntityHistoryRequest{}
|
||||
_ = sqlEntityHistoryListRequest{}
|
||||
)
|
||||
|
@ -16,7 +16,7 @@ var (
|
||||
// embedding or with a named struct field if its Arg method would clash with
|
||||
// another struct field.
|
||||
type Args struct {
|
||||
d Dialect
|
||||
d interface{ ArgPlaceholder(argNum int) string }
|
||||
values []any
|
||||
}
|
||||
|
||||
@ -69,6 +69,10 @@ func (a *Args) ArgList(slice reflect.Value) (string, error) {
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func (a *Args) Reset() {
|
||||
a.values = nil
|
||||
}
|
||||
|
||||
func (a *Args) GetArgs() []any {
|
||||
return a.values
|
||||
}
|
||||
@ -77,4 +81,5 @@ type ArgsIface interface {
|
||||
Arg(x any) string
|
||||
ArgList(slice reflect.Value) (string, error)
|
||||
GetArgs() []any
|
||||
Reset()
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
package sqltemplate
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArgs_Arg(t *testing.T) {
|
||||
t.Parallel()
|
||||
@ -30,3 +34,68 @@ func TestArgs_Arg(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArg_ArgList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
input reflect.Value
|
||||
added []any
|
||||
output string
|
||||
err error
|
||||
}{
|
||||
{err: ErrInvalidArgList},
|
||||
{input: reflect.ValueOf(1), err: ErrInvalidArgList},
|
||||
{input: reflect.ValueOf(nil), err: ErrInvalidArgList},
|
||||
{input: reflect.ValueOf(any(nil)), err: ErrInvalidArgList},
|
||||
{input: reflect.ValueOf("asd"), err: ErrInvalidArgList},
|
||||
{input: reflect.ValueOf([]any{})},
|
||||
|
||||
{
|
||||
input: reflect.ValueOf([]any{true}),
|
||||
added: []any{true},
|
||||
output: "?",
|
||||
},
|
||||
|
||||
{
|
||||
input: reflect.ValueOf([]any{1, true}),
|
||||
added: []any{1, true},
|
||||
output: "?, ?",
|
||||
},
|
||||
|
||||
{
|
||||
input: reflect.ValueOf([]any{1, "asd", true}),
|
||||
added: []any{1, "asd", true},
|
||||
output: "?, ?, ?",
|
||||
},
|
||||
}
|
||||
|
||||
var a Args
|
||||
a.d = argFmtSQL92
|
||||
for i, tc := range testCases {
|
||||
a.Reset()
|
||||
|
||||
gotOutput, gotErr := a.ArgList(tc.input)
|
||||
if !errors.Is(gotErr, tc.err) {
|
||||
t.Fatalf("[test #%d] Unexpected error. Expected: %v, actual: %v",
|
||||
i, gotErr, tc.err)
|
||||
}
|
||||
|
||||
if tc.output != gotOutput {
|
||||
t.Fatalf("[test #%d] Unexpected output. Expected: %v, actual: %v",
|
||||
i, gotOutput, tc.output)
|
||||
}
|
||||
|
||||
if len(tc.added) != len(a.values) {
|
||||
t.Fatalf("[test #%d] Unexpected added items.\n\tExpected: %#v\n\t"+
|
||||
"Actual: %#v", i, tc.added, a.values)
|
||||
}
|
||||
|
||||
for j := range tc.added {
|
||||
if !reflect.DeepEqual(tc.added[j], a.values[j]) {
|
||||
t.Fatalf("[test #%d] Unexpected %d-eth item.\n\tExpected:"+
|
||||
" %#v\n\tActual: %#v", i, j, tc.added[j], a.values[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,15 +17,21 @@ var (
|
||||
// embedded for ease of use, or with a named struct field if any of its methods
|
||||
// would clash with other struct field names.
|
||||
type Dialect interface {
|
||||
// Name identifies the Dialect. Note that a Dialect may be common to more
|
||||
// than one DBMS (e.g. "postgres" is common to PostgreSQL and to
|
||||
// CockroachDB), while we can maintain different Dialects for the same DBMS
|
||||
// but different versions (e.g. "mysql5" and "mysql8").
|
||||
Name() string
|
||||
|
||||
// Ident returns the given string quoted in a way that is suitable to be
|
||||
// used as an identifier. Database names, schema names, table names, column
|
||||
// names are all examples of identifiers.
|
||||
Ident(string) (string, error)
|
||||
|
||||
// ArgPlaceholder returns a safe argument suitable to be used in a SQL
|
||||
// prepared statement for the argNum-eth argument passed in execution. The
|
||||
// SQL92 Standard specifies the question mark ('?') should be used in all
|
||||
// cases, but some implementations differ.
|
||||
// prepared statement for the argNum-eth argument passed in execution
|
||||
// (starting at 1). The SQL92 Standard specifies the question mark ('?')
|
||||
// should be used in all cases, but some implementations differ.
|
||||
ArgPlaceholder(argNum int) string
|
||||
|
||||
// SelectFor parses and returns the given row-locking clause for a SELECT
|
||||
@ -75,12 +81,9 @@ const (
|
||||
SelectForUpdateSkipLocked RowLockingClause = "UPDATE SKIP LOCKED"
|
||||
)
|
||||
|
||||
// rowLockingClauseAll aids implementations that either support all the
|
||||
// row-locking clause options or none. If it's true, it returns the clause,
|
||||
// otherwise it returns an empty string.
|
||||
type rowLockingClauseAll bool
|
||||
type rowLockingClauseMap map[RowLockingClause]RowLockingClause
|
||||
|
||||
func (rlc rowLockingClauseAll) SelectFor(s ...string) (string, error) {
|
||||
func (rlc rowLockingClauseMap) SelectFor(s ...string) (string, error) {
|
||||
// all implementations should err on invalid input, otherwise we would just
|
||||
// be hiding the error until we change the dialect
|
||||
o, err := ParseRowLockingClause(s...)
|
||||
@ -88,11 +91,21 @@ func (rlc rowLockingClauseAll) SelectFor(s ...string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !rlc {
|
||||
return "", nil
|
||||
var ret string
|
||||
if len(rlc) > 0 {
|
||||
ret = "FOR " + string(rlc[o])
|
||||
}
|
||||
|
||||
return "FOR " + string(o), nil
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
var rowLockingClauseAll = rowLockingClauseMap{
|
||||
SelectForShare: SelectForShare,
|
||||
SelectForShareNoWait: SelectForShareNoWait,
|
||||
SelectForShareSkipLocked: SelectForShareSkipLocked,
|
||||
SelectForUpdate: SelectForUpdate,
|
||||
SelectForUpdateNoWait: SelectForUpdateNoWait,
|
||||
SelectForUpdateSkipLocked: SelectForUpdateSkipLocked,
|
||||
}
|
||||
|
||||
// standardIdent provides standard SQL escaping of identifiers.
|
||||
@ -119,3 +132,9 @@ var (
|
||||
return "$" + strconv.Itoa(argNum)
|
||||
})
|
||||
)
|
||||
|
||||
type name string
|
||||
|
||||
func (n name) Name() string {
|
||||
return string(n)
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
package sqltemplate
|
||||
|
||||
// MySQL is an implementation of Dialect for the MySQL DMBS. It relies on having
|
||||
// ANSI_QUOTES SQL Mode enabled. For more information about ANSI_QUOTES and SQL
|
||||
// Modes see:
|
||||
// MySQL is the default implementation of Dialect for the MySQL DMBS, currently
|
||||
// supporting MySQL-8.x. It relies on having ANSI_QUOTES SQL Mode enabled. For
|
||||
// more information about ANSI_QUOTES and SQL Modes see:
|
||||
//
|
||||
// https://dev.mysql.com/doc/refman/8.4/en/sql-mode.html#sqlmode_ansi_quotes
|
||||
var MySQL = mysql{
|
||||
rowLockingClauseAll: true,
|
||||
rowLockingClauseMap: rowLockingClauseAll,
|
||||
argPlaceholderFunc: argFmtSQL92,
|
||||
name: "mysql",
|
||||
}
|
||||
|
||||
var _ Dialect = MySQL
|
||||
|
||||
type mysql struct {
|
||||
standardIdent
|
||||
rowLockingClauseAll
|
||||
rowLockingClauseMap
|
||||
argPlaceholderFunc
|
||||
name
|
||||
}
|
||||
|
@ -7,8 +7,9 @@ import (
|
||||
|
||||
// PostgreSQL is an implementation of Dialect for the PostgreSQL DMBS.
|
||||
var PostgreSQL = postgresql{
|
||||
rowLockingClauseAll: true,
|
||||
rowLockingClauseMap: rowLockingClauseAll,
|
||||
argPlaceholderFunc: argFmtPositional,
|
||||
name: "postgres",
|
||||
}
|
||||
|
||||
var _ Dialect = PostgreSQL
|
||||
@ -20,8 +21,9 @@ var (
|
||||
|
||||
type postgresql struct {
|
||||
standardIdent
|
||||
rowLockingClauseAll
|
||||
rowLockingClauseMap
|
||||
argPlaceholderFunc
|
||||
name
|
||||
}
|
||||
|
||||
func (p postgresql) Ident(s string) (string, error) {
|
||||
|
@ -2,8 +2,8 @@ package sqltemplate
|
||||
|
||||
// SQLite is an implementation of Dialect for the SQLite DMBS.
|
||||
var SQLite = sqlite{
|
||||
rowLockingClauseAll: false,
|
||||
argPlaceholderFunc: argFmtSQL92,
|
||||
argPlaceholderFunc: argFmtSQL92,
|
||||
name: "sqlite",
|
||||
}
|
||||
|
||||
var _ Dialect = SQLite
|
||||
@ -12,6 +12,7 @@ type sqlite struct {
|
||||
// See:
|
||||
// https://www.sqlite.org/lang_keywords.html
|
||||
standardIdent
|
||||
rowLockingClauseAll
|
||||
rowLockingClauseMap
|
||||
argPlaceholderFunc
|
||||
name
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ func TestParseRowLockingClause(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowLockingClauseAll_SelectFor(t *testing.T) {
|
||||
func TestRowLockingClauseMap_SelectFor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
splitSpace := func(s string) []string {
|
||||
@ -95,21 +95,22 @@ func TestRowLockingClauseAll_SelectFor(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
var nilRLC rowLockingClauseMap
|
||||
for i, tc := range testCases {
|
||||
gotOutput, gotErr := rowLockingClauseAll(true).SelectFor(tc.input...)
|
||||
gotOutput, gotErr := nilRLC.SelectFor(tc.input...)
|
||||
if !errors.Is(gotErr, tc.err) {
|
||||
t.Fatalf("[true] unexpected error %v in test case %d", gotErr, i)
|
||||
}
|
||||
if gotOutput != string(tc.output) {
|
||||
t.Fatalf("[true] unexpected error %v in test case %d", gotErr, i)
|
||||
}
|
||||
|
||||
gotOutput, gotErr = rowLockingClauseAll(false).SelectFor(tc.input...)
|
||||
if !errors.Is(gotErr, tc.err) {
|
||||
t.Fatalf("[false] unexpected error %v in test case %d", gotErr, i)
|
||||
t.Fatalf("[nil] unexpected error %v in test case %d", gotErr, i)
|
||||
}
|
||||
if gotOutput != "" {
|
||||
t.Fatalf("[false] unexpected error %v in test case %d", gotErr, i)
|
||||
t.Fatalf("[nil] unexpected output %v in test case %d", gotOutput, i)
|
||||
}
|
||||
|
||||
gotOutput, gotErr = rowLockingClauseAll.SelectFor(tc.input...)
|
||||
if !errors.Is(gotErr, tc.err) {
|
||||
t.Fatalf("[all] unexpected error %v in test case %d", gotErr, i)
|
||||
}
|
||||
if gotOutput != string(tc.output) {
|
||||
t.Fatalf("[all] unexpected output %v in test case %d", gotOutput, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,3 +142,43 @@ func TestStandardIdent_Ident(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgPlaceholderFunc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
input int
|
||||
valuePositional string
|
||||
}{
|
||||
{
|
||||
input: 1,
|
||||
valuePositional: "$1",
|
||||
},
|
||||
{
|
||||
input: 16,
|
||||
valuePositional: "$16",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
got := argFmtSQL92(tc.input)
|
||||
if got != "?" {
|
||||
t.Fatalf("[argFmtSQL92] unexpected value %q in test case %d", got, i)
|
||||
}
|
||||
|
||||
got = argFmtPositional(tc.input)
|
||||
if got != tc.valuePositional {
|
||||
t.Fatalf("[argFmtPositional] unexpected value %q in test case %d", got, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestName_Name(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const v = "some dialect name"
|
||||
n := name(v)
|
||||
if n.Name() != v {
|
||||
t.Fatalf("unexpected dialect name %q", n.Name())
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,8 @@ import (
|
||||
)
|
||||
|
||||
type ScanDest struct {
|
||||
values []any
|
||||
values []any
|
||||
colNames []string
|
||||
}
|
||||
|
||||
func (i *ScanDest) Into(v reflect.Value, colName string) (string, error) {
|
||||
@ -15,15 +16,26 @@ func (i *ScanDest) Into(v reflect.Value, colName string) (string, error) {
|
||||
}
|
||||
|
||||
i.values = append(i.values, v.Addr().Interface())
|
||||
i.colNames = append(i.colNames, colName)
|
||||
|
||||
return colName, nil
|
||||
}
|
||||
|
||||
func (i *ScanDest) Reset() {
|
||||
i.values = nil
|
||||
}
|
||||
|
||||
func (i *ScanDest) GetScanDest() []any {
|
||||
return i.values
|
||||
}
|
||||
|
||||
func (i *ScanDest) GetColNames() []string {
|
||||
return i.colNames
|
||||
}
|
||||
|
||||
type ScanDestIface interface {
|
||||
Into(v reflect.Value, colName string) (string, error)
|
||||
GetScanDest() []any
|
||||
GetColNames() []string
|
||||
Reset()
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package sqltemplate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -22,17 +23,29 @@ func TestScanDest_Into(t *testing.T) {
|
||||
}{}
|
||||
dataVal := reflect.ValueOf(&data).Elem()
|
||||
|
||||
colName, err = d.Into(dataVal.FieldByName("X"), "some int")
|
||||
expectedColNames := []string{"some int", "and a byte"}
|
||||
|
||||
colName, err = d.Into(dataVal.FieldByName("X"), expectedColNames[0])
|
||||
v := d.GetScanDest()
|
||||
if err != nil || colName != "some int" || len(v) != 1 || v[0] != &data.X {
|
||||
if err != nil || colName != expectedColNames[0] || len(v) != 1 || v[0] != &data.X {
|
||||
t.Fatalf("unexpected outcome, got colname %q, err: %v, scan dest: %#v",
|
||||
colName, err, d)
|
||||
}
|
||||
|
||||
colName, err = d.Into(dataVal.FieldByName("Y"), "some byte")
|
||||
colName, err = d.Into(dataVal.FieldByName("Y"), expectedColNames[1])
|
||||
v = d.GetScanDest()
|
||||
if err != nil || colName != "some byte" || len(v) != 2 || v[1] != &data.Y {
|
||||
if err != nil || colName != expectedColNames[1] || len(v) != 2 || v[1] != &data.Y {
|
||||
t.Fatalf("unexpected outcome, got colname %q, err: %v, scan dest: %#v",
|
||||
colName, err, d)
|
||||
}
|
||||
|
||||
if gotColNames := d.GetColNames(); !slices.Equal(expectedColNames, gotColNames) {
|
||||
t.Fatalf("unexpected column names: %v", gotColNames)
|
||||
}
|
||||
|
||||
d.Reset()
|
||||
v = d.GetScanDest()
|
||||
if len(v) != 0 {
|
||||
t.Fatalf("unexpected values after reset: %v", v)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,664 @@
|
||||
// Code generated by mockery v2.43.1. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
sqltemplate "github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
|
||||
)
|
||||
|
||||
// SQLTemplateIface is an autogenerated mock type for the SQLTemplateIface type
|
||||
type SQLTemplateIface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type SQLTemplateIface_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *SQLTemplateIface) EXPECT() *SQLTemplateIface_Expecter {
|
||||
return &SQLTemplateIface_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Arg provides a mock function with given fields: x
|
||||
func (_m *SQLTemplateIface) Arg(x interface{}) string {
|
||||
ret := _m.Called(x)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Arg")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(interface{}) string); ok {
|
||||
r0 = rf(x)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_Arg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Arg'
|
||||
type SQLTemplateIface_Arg_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Arg is a helper method to define mock.On call
|
||||
// - x interface{}
|
||||
func (_e *SQLTemplateIface_Expecter) Arg(x interface{}) *SQLTemplateIface_Arg_Call {
|
||||
return &SQLTemplateIface_Arg_Call{Call: _e.mock.On("Arg", x)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Arg_Call) Run(run func(x interface{})) *SQLTemplateIface_Arg_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(interface{}))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Arg_Call) Return(_a0 string) *SQLTemplateIface_Arg_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Arg_Call) RunAndReturn(run func(interface{}) string) *SQLTemplateIface_Arg_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ArgList provides a mock function with given fields: slice
|
||||
func (_m *SQLTemplateIface) ArgList(slice reflect.Value) (string, error) {
|
||||
ret := _m.Called(slice)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ArgList")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value) (string, error)); ok {
|
||||
return rf(slice)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value) string); ok {
|
||||
r0 = rf(slice)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(reflect.Value) error); ok {
|
||||
r1 = rf(slice)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SQLTemplateIface_ArgList_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ArgList'
|
||||
type SQLTemplateIface_ArgList_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ArgList is a helper method to define mock.On call
|
||||
// - slice reflect.Value
|
||||
func (_e *SQLTemplateIface_Expecter) ArgList(slice interface{}) *SQLTemplateIface_ArgList_Call {
|
||||
return &SQLTemplateIface_ArgList_Call{Call: _e.mock.On("ArgList", slice)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_ArgList_Call) Run(run func(slice reflect.Value)) *SQLTemplateIface_ArgList_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(reflect.Value))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_ArgList_Call) Return(_a0 string, _a1 error) *SQLTemplateIface_ArgList_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_ArgList_Call) RunAndReturn(run func(reflect.Value) (string, error)) *SQLTemplateIface_ArgList_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ArgPlaceholder provides a mock function with given fields: argNum
|
||||
func (_m *SQLTemplateIface) ArgPlaceholder(argNum int) string {
|
||||
ret := _m.Called(argNum)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ArgPlaceholder")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(int) string); ok {
|
||||
r0 = rf(argNum)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_ArgPlaceholder_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ArgPlaceholder'
|
||||
type SQLTemplateIface_ArgPlaceholder_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ArgPlaceholder is a helper method to define mock.On call
|
||||
// - argNum int
|
||||
func (_e *SQLTemplateIface_Expecter) ArgPlaceholder(argNum interface{}) *SQLTemplateIface_ArgPlaceholder_Call {
|
||||
return &SQLTemplateIface_ArgPlaceholder_Call{Call: _e.mock.On("ArgPlaceholder", argNum)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_ArgPlaceholder_Call) Run(run func(argNum int)) *SQLTemplateIface_ArgPlaceholder_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(int))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_ArgPlaceholder_Call) Return(_a0 string) *SQLTemplateIface_ArgPlaceholder_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_ArgPlaceholder_Call) RunAndReturn(run func(int) string) *SQLTemplateIface_ArgPlaceholder_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetArgs provides a mock function with given fields:
|
||||
func (_m *SQLTemplateIface) GetArgs() []interface{} {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetArgs")
|
||||
}
|
||||
|
||||
var r0 []interface{}
|
||||
if rf, ok := ret.Get(0).(func() []interface{}); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_GetArgs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetArgs'
|
||||
type SQLTemplateIface_GetArgs_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetArgs is a helper method to define mock.On call
|
||||
func (_e *SQLTemplateIface_Expecter) GetArgs() *SQLTemplateIface_GetArgs_Call {
|
||||
return &SQLTemplateIface_GetArgs_Call{Call: _e.mock.On("GetArgs")}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetArgs_Call) Run(run func()) *SQLTemplateIface_GetArgs_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetArgs_Call) Return(_a0 []interface{}) *SQLTemplateIface_GetArgs_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetArgs_Call) RunAndReturn(run func() []interface{}) *SQLTemplateIface_GetArgs_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetColNames provides a mock function with given fields:
|
||||
func (_m *SQLTemplateIface) GetColNames() []string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetColNames")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_GetColNames_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetColNames'
|
||||
type SQLTemplateIface_GetColNames_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetColNames is a helper method to define mock.On call
|
||||
func (_e *SQLTemplateIface_Expecter) GetColNames() *SQLTemplateIface_GetColNames_Call {
|
||||
return &SQLTemplateIface_GetColNames_Call{Call: _e.mock.On("GetColNames")}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetColNames_Call) Run(run func()) *SQLTemplateIface_GetColNames_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetColNames_Call) Return(_a0 []string) *SQLTemplateIface_GetColNames_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetColNames_Call) RunAndReturn(run func() []string) *SQLTemplateIface_GetColNames_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetScanDest provides a mock function with given fields:
|
||||
func (_m *SQLTemplateIface) GetScanDest() []interface{} {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetScanDest")
|
||||
}
|
||||
|
||||
var r0 []interface{}
|
||||
if rf, ok := ret.Get(0).(func() []interface{}); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_GetScanDest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetScanDest'
|
||||
type SQLTemplateIface_GetScanDest_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetScanDest is a helper method to define mock.On call
|
||||
func (_e *SQLTemplateIface_Expecter) GetScanDest() *SQLTemplateIface_GetScanDest_Call {
|
||||
return &SQLTemplateIface_GetScanDest_Call{Call: _e.mock.On("GetScanDest")}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetScanDest_Call) Run(run func()) *SQLTemplateIface_GetScanDest_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetScanDest_Call) Return(_a0 []interface{}) *SQLTemplateIface_GetScanDest_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_GetScanDest_Call) RunAndReturn(run func() []interface{}) *SQLTemplateIface_GetScanDest_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Ident provides a mock function with given fields: _a0
|
||||
func (_m *SQLTemplateIface) Ident(_a0 string) (string, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Ident")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
|
||||
return rf(_a0)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) string); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SQLTemplateIface_Ident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ident'
|
||||
type SQLTemplateIface_Ident_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Ident is a helper method to define mock.On call
|
||||
// - _a0 string
|
||||
func (_e *SQLTemplateIface_Expecter) Ident(_a0 interface{}) *SQLTemplateIface_Ident_Call {
|
||||
return &SQLTemplateIface_Ident_Call{Call: _e.mock.On("Ident", _a0)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Ident_Call) Run(run func(_a0 string)) *SQLTemplateIface_Ident_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Ident_Call) Return(_a0 string, _a1 error) *SQLTemplateIface_Ident_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Ident_Call) RunAndReturn(run func(string) (string, error)) *SQLTemplateIface_Ident_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Into provides a mock function with given fields: v, colName
|
||||
func (_m *SQLTemplateIface) Into(v reflect.Value, colName string) (string, error) {
|
||||
ret := _m.Called(v, colName)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Into")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value, string) (string, error)); ok {
|
||||
return rf(v, colName)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value, string) string); ok {
|
||||
r0 = rf(v, colName)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(reflect.Value, string) error); ok {
|
||||
r1 = rf(v, colName)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SQLTemplateIface_Into_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Into'
|
||||
type SQLTemplateIface_Into_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Into is a helper method to define mock.On call
|
||||
// - v reflect.Value
|
||||
// - colName string
|
||||
func (_e *SQLTemplateIface_Expecter) Into(v interface{}, colName interface{}) *SQLTemplateIface_Into_Call {
|
||||
return &SQLTemplateIface_Into_Call{Call: _e.mock.On("Into", v, colName)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Into_Call) Run(run func(v reflect.Value, colName string)) *SQLTemplateIface_Into_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(reflect.Value), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Into_Call) Return(_a0 string, _a1 error) *SQLTemplateIface_Into_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Into_Call) RunAndReturn(run func(reflect.Value, string) (string, error)) *SQLTemplateIface_Into_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Name provides a mock function with given fields:
|
||||
func (_m *SQLTemplateIface) Name() string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Name")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
|
||||
type SQLTemplateIface_Name_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Name is a helper method to define mock.On call
|
||||
func (_e *SQLTemplateIface_Expecter) Name() *SQLTemplateIface_Name_Call {
|
||||
return &SQLTemplateIface_Name_Call{Call: _e.mock.On("Name")}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Name_Call) Run(run func()) *SQLTemplateIface_Name_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Name_Call) Return(_a0 string) *SQLTemplateIface_Name_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Name_Call) RunAndReturn(run func() string) *SQLTemplateIface_Name_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Reset provides a mock function with given fields:
|
||||
func (_m *SQLTemplateIface) Reset() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// SQLTemplateIface_Reset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Reset'
|
||||
type SQLTemplateIface_Reset_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Reset is a helper method to define mock.On call
|
||||
func (_e *SQLTemplateIface_Expecter) Reset() *SQLTemplateIface_Reset_Call {
|
||||
return &SQLTemplateIface_Reset_Call{Call: _e.mock.On("Reset")}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Reset_Call) Run(run func()) *SQLTemplateIface_Reset_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Reset_Call) Return() *SQLTemplateIface_Reset_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Reset_Call) RunAndReturn(run func()) *SQLTemplateIface_Reset_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SelectFor provides a mock function with given fields: _a0
|
||||
func (_m *SQLTemplateIface) SelectFor(_a0 ...string) (string, error) {
|
||||
_va := make([]interface{}, len(_a0))
|
||||
for _i := range _a0 {
|
||||
_va[_i] = _a0[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SelectFor")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(...string) (string, error)); ok {
|
||||
return rf(_a0...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(...string) string); ok {
|
||||
r0 = rf(_a0...)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(...string) error); ok {
|
||||
r1 = rf(_a0...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SQLTemplateIface_SelectFor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectFor'
|
||||
type SQLTemplateIface_SelectFor_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SelectFor is a helper method to define mock.On call
|
||||
// - _a0 ...string
|
||||
func (_e *SQLTemplateIface_Expecter) SelectFor(_a0 ...interface{}) *SQLTemplateIface_SelectFor_Call {
|
||||
return &SQLTemplateIface_SelectFor_Call{Call: _e.mock.On("SelectFor",
|
||||
append([]interface{}{}, _a0...)...)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_SelectFor_Call) Run(run func(_a0 ...string)) *SQLTemplateIface_SelectFor_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]string, len(args)-0)
|
||||
for i, a := range args[0:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(string)
|
||||
}
|
||||
}
|
||||
run(variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_SelectFor_Call) Return(_a0 string, _a1 error) *SQLTemplateIface_SelectFor_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_SelectFor_Call) RunAndReturn(run func(...string) (string, error)) *SQLTemplateIface_SelectFor_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetDialect provides a mock function with given fields: _a0
|
||||
func (_m *SQLTemplateIface) SetDialect(_a0 sqltemplate.Dialect) {
|
||||
_m.Called(_a0)
|
||||
}
|
||||
|
||||
// SQLTemplateIface_SetDialect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDialect'
|
||||
type SQLTemplateIface_SetDialect_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetDialect is a helper method to define mock.On call
|
||||
// - _a0 sqltemplate.Dialect
|
||||
func (_e *SQLTemplateIface_Expecter) SetDialect(_a0 interface{}) *SQLTemplateIface_SetDialect_Call {
|
||||
return &SQLTemplateIface_SetDialect_Call{Call: _e.mock.On("SetDialect", _a0)}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_SetDialect_Call) Run(run func(_a0 sqltemplate.Dialect)) *SQLTemplateIface_SetDialect_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(sqltemplate.Dialect))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_SetDialect_Call) Return() *SQLTemplateIface_SetDialect_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_SetDialect_Call) RunAndReturn(run func(sqltemplate.Dialect)) *SQLTemplateIface_SetDialect_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Validate provides a mock function with given fields:
|
||||
func (_m *SQLTemplateIface) Validate() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Validate")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SQLTemplateIface_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate'
|
||||
type SQLTemplateIface_Validate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Validate is a helper method to define mock.On call
|
||||
func (_e *SQLTemplateIface_Expecter) Validate() *SQLTemplateIface_Validate_Call {
|
||||
return &SQLTemplateIface_Validate_Call{Call: _e.mock.On("Validate")}
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Validate_Call) Run(run func()) *SQLTemplateIface_Validate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Validate_Call) Return(_a0 error) *SQLTemplateIface_Validate_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SQLTemplateIface_Validate_Call) RunAndReturn(run func() error) *SQLTemplateIface_Validate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewSQLTemplateIface creates a new instance of SQLTemplateIface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewSQLTemplateIface(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *SQLTemplateIface {
|
||||
mock := &SQLTemplateIface{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
@ -0,0 +1,719 @@
|
||||
// Code generated by mockery v2.43.1. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
sqltemplate "github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
|
||||
)
|
||||
|
||||
// WithResults is an autogenerated mock type for the WithResults type
|
||||
type WithResults[T interface{}] struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type WithResults_Expecter[T interface{}] struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *WithResults[T]) EXPECT() *WithResults_Expecter[T] {
|
||||
return &WithResults_Expecter[T]{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Arg provides a mock function with given fields: x
|
||||
func (_m *WithResults[T]) Arg(x interface{}) string {
|
||||
ret := _m.Called(x)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Arg")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(interface{}) string); ok {
|
||||
r0 = rf(x)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_Arg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Arg'
|
||||
type WithResults_Arg_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Arg is a helper method to define mock.On call
|
||||
// - x interface{}
|
||||
func (_e *WithResults_Expecter[T]) Arg(x interface{}) *WithResults_Arg_Call[T] {
|
||||
return &WithResults_Arg_Call[T]{Call: _e.mock.On("Arg", x)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Arg_Call[T]) Run(run func(x interface{})) *WithResults_Arg_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(interface{}))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Arg_Call[T]) Return(_a0 string) *WithResults_Arg_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Arg_Call[T]) RunAndReturn(run func(interface{}) string) *WithResults_Arg_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ArgList provides a mock function with given fields: slice
|
||||
func (_m *WithResults[T]) ArgList(slice reflect.Value) (string, error) {
|
||||
ret := _m.Called(slice)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ArgList")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value) (string, error)); ok {
|
||||
return rf(slice)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value) string); ok {
|
||||
r0 = rf(slice)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(reflect.Value) error); ok {
|
||||
r1 = rf(slice)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WithResults_ArgList_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ArgList'
|
||||
type WithResults_ArgList_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ArgList is a helper method to define mock.On call
|
||||
// - slice reflect.Value
|
||||
func (_e *WithResults_Expecter[T]) ArgList(slice interface{}) *WithResults_ArgList_Call[T] {
|
||||
return &WithResults_ArgList_Call[T]{Call: _e.mock.On("ArgList", slice)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_ArgList_Call[T]) Run(run func(slice reflect.Value)) *WithResults_ArgList_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(reflect.Value))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_ArgList_Call[T]) Return(_a0 string, _a1 error) *WithResults_ArgList_Call[T] {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_ArgList_Call[T]) RunAndReturn(run func(reflect.Value) (string, error)) *WithResults_ArgList_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ArgPlaceholder provides a mock function with given fields: argNum
|
||||
func (_m *WithResults[T]) ArgPlaceholder(argNum int) string {
|
||||
ret := _m.Called(argNum)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ArgPlaceholder")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(int) string); ok {
|
||||
r0 = rf(argNum)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_ArgPlaceholder_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ArgPlaceholder'
|
||||
type WithResults_ArgPlaceholder_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ArgPlaceholder is a helper method to define mock.On call
|
||||
// - argNum int
|
||||
func (_e *WithResults_Expecter[T]) ArgPlaceholder(argNum interface{}) *WithResults_ArgPlaceholder_Call[T] {
|
||||
return &WithResults_ArgPlaceholder_Call[T]{Call: _e.mock.On("ArgPlaceholder", argNum)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_ArgPlaceholder_Call[T]) Run(run func(argNum int)) *WithResults_ArgPlaceholder_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(int))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_ArgPlaceholder_Call[T]) Return(_a0 string) *WithResults_ArgPlaceholder_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_ArgPlaceholder_Call[T]) RunAndReturn(run func(int) string) *WithResults_ArgPlaceholder_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetArgs provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) GetArgs() []interface{} {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetArgs")
|
||||
}
|
||||
|
||||
var r0 []interface{}
|
||||
if rf, ok := ret.Get(0).(func() []interface{}); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_GetArgs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetArgs'
|
||||
type WithResults_GetArgs_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetArgs is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) GetArgs() *WithResults_GetArgs_Call[T] {
|
||||
return &WithResults_GetArgs_Call[T]{Call: _e.mock.On("GetArgs")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetArgs_Call[T]) Run(run func()) *WithResults_GetArgs_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetArgs_Call[T]) Return(_a0 []interface{}) *WithResults_GetArgs_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetArgs_Call[T]) RunAndReturn(run func() []interface{}) *WithResults_GetArgs_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetColNames provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) GetColNames() []string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetColNames")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_GetColNames_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetColNames'
|
||||
type WithResults_GetColNames_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetColNames is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) GetColNames() *WithResults_GetColNames_Call[T] {
|
||||
return &WithResults_GetColNames_Call[T]{Call: _e.mock.On("GetColNames")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetColNames_Call[T]) Run(run func()) *WithResults_GetColNames_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetColNames_Call[T]) Return(_a0 []string) *WithResults_GetColNames_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetColNames_Call[T]) RunAndReturn(run func() []string) *WithResults_GetColNames_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetScanDest provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) GetScanDest() []interface{} {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetScanDest")
|
||||
}
|
||||
|
||||
var r0 []interface{}
|
||||
if rf, ok := ret.Get(0).(func() []interface{}); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_GetScanDest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetScanDest'
|
||||
type WithResults_GetScanDest_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetScanDest is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) GetScanDest() *WithResults_GetScanDest_Call[T] {
|
||||
return &WithResults_GetScanDest_Call[T]{Call: _e.mock.On("GetScanDest")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetScanDest_Call[T]) Run(run func()) *WithResults_GetScanDest_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetScanDest_Call[T]) Return(_a0 []interface{}) *WithResults_GetScanDest_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_GetScanDest_Call[T]) RunAndReturn(run func() []interface{}) *WithResults_GetScanDest_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Ident provides a mock function with given fields: _a0
|
||||
func (_m *WithResults[T]) Ident(_a0 string) (string, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Ident")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
|
||||
return rf(_a0)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) string); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WithResults_Ident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ident'
|
||||
type WithResults_Ident_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Ident is a helper method to define mock.On call
|
||||
// - _a0 string
|
||||
func (_e *WithResults_Expecter[T]) Ident(_a0 interface{}) *WithResults_Ident_Call[T] {
|
||||
return &WithResults_Ident_Call[T]{Call: _e.mock.On("Ident", _a0)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Ident_Call[T]) Run(run func(_a0 string)) *WithResults_Ident_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Ident_Call[T]) Return(_a0 string, _a1 error) *WithResults_Ident_Call[T] {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Ident_Call[T]) RunAndReturn(run func(string) (string, error)) *WithResults_Ident_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Into provides a mock function with given fields: v, colName
|
||||
func (_m *WithResults[T]) Into(v reflect.Value, colName string) (string, error) {
|
||||
ret := _m.Called(v, colName)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Into")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value, string) (string, error)); ok {
|
||||
return rf(v, colName)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(reflect.Value, string) string); ok {
|
||||
r0 = rf(v, colName)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(reflect.Value, string) error); ok {
|
||||
r1 = rf(v, colName)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WithResults_Into_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Into'
|
||||
type WithResults_Into_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Into is a helper method to define mock.On call
|
||||
// - v reflect.Value
|
||||
// - colName string
|
||||
func (_e *WithResults_Expecter[T]) Into(v interface{}, colName interface{}) *WithResults_Into_Call[T] {
|
||||
return &WithResults_Into_Call[T]{Call: _e.mock.On("Into", v, colName)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Into_Call[T]) Run(run func(v reflect.Value, colName string)) *WithResults_Into_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(reflect.Value), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Into_Call[T]) Return(_a0 string, _a1 error) *WithResults_Into_Call[T] {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Into_Call[T]) RunAndReturn(run func(reflect.Value, string) (string, error)) *WithResults_Into_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Name provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) Name() string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Name")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
|
||||
type WithResults_Name_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Name is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) Name() *WithResults_Name_Call[T] {
|
||||
return &WithResults_Name_Call[T]{Call: _e.mock.On("Name")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Name_Call[T]) Run(run func()) *WithResults_Name_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Name_Call[T]) Return(_a0 string) *WithResults_Name_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Name_Call[T]) RunAndReturn(run func() string) *WithResults_Name_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Reset provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) Reset() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// WithResults_Reset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Reset'
|
||||
type WithResults_Reset_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Reset is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) Reset() *WithResults_Reset_Call[T] {
|
||||
return &WithResults_Reset_Call[T]{Call: _e.mock.On("Reset")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Reset_Call[T]) Run(run func()) *WithResults_Reset_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Reset_Call[T]) Return() *WithResults_Reset_Call[T] {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Reset_Call[T]) RunAndReturn(run func()) *WithResults_Reset_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Results provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) Results() (T, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Results")
|
||||
}
|
||||
|
||||
var r0 T
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (T, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() T); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(T)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WithResults_Results_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Results'
|
||||
type WithResults_Results_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Results is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) Results() *WithResults_Results_Call[T] {
|
||||
return &WithResults_Results_Call[T]{Call: _e.mock.On("Results")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Results_Call[T]) Run(run func()) *WithResults_Results_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Results_Call[T]) Return(_a0 T, _a1 error) *WithResults_Results_Call[T] {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Results_Call[T]) RunAndReturn(run func() (T, error)) *WithResults_Results_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SelectFor provides a mock function with given fields: _a0
|
||||
func (_m *WithResults[T]) SelectFor(_a0 ...string) (string, error) {
|
||||
_va := make([]interface{}, len(_a0))
|
||||
for _i := range _a0 {
|
||||
_va[_i] = _a0[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SelectFor")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(...string) (string, error)); ok {
|
||||
return rf(_a0...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(...string) string); ok {
|
||||
r0 = rf(_a0...)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(...string) error); ok {
|
||||
r1 = rf(_a0...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WithResults_SelectFor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectFor'
|
||||
type WithResults_SelectFor_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SelectFor is a helper method to define mock.On call
|
||||
// - _a0 ...string
|
||||
func (_e *WithResults_Expecter[T]) SelectFor(_a0 ...interface{}) *WithResults_SelectFor_Call[T] {
|
||||
return &WithResults_SelectFor_Call[T]{Call: _e.mock.On("SelectFor",
|
||||
append([]interface{}{}, _a0...)...)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_SelectFor_Call[T]) Run(run func(_a0 ...string)) *WithResults_SelectFor_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]string, len(args)-0)
|
||||
for i, a := range args[0:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(string)
|
||||
}
|
||||
}
|
||||
run(variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_SelectFor_Call[T]) Return(_a0 string, _a1 error) *WithResults_SelectFor_Call[T] {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_SelectFor_Call[T]) RunAndReturn(run func(...string) (string, error)) *WithResults_SelectFor_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetDialect provides a mock function with given fields: _a0
|
||||
func (_m *WithResults[T]) SetDialect(_a0 sqltemplate.Dialect) {
|
||||
_m.Called(_a0)
|
||||
}
|
||||
|
||||
// WithResults_SetDialect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDialect'
|
||||
type WithResults_SetDialect_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetDialect is a helper method to define mock.On call
|
||||
// - _a0 sqltemplate.Dialect
|
||||
func (_e *WithResults_Expecter[T]) SetDialect(_a0 interface{}) *WithResults_SetDialect_Call[T] {
|
||||
return &WithResults_SetDialect_Call[T]{Call: _e.mock.On("SetDialect", _a0)}
|
||||
}
|
||||
|
||||
func (_c *WithResults_SetDialect_Call[T]) Run(run func(_a0 sqltemplate.Dialect)) *WithResults_SetDialect_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(sqltemplate.Dialect))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_SetDialect_Call[T]) Return() *WithResults_SetDialect_Call[T] {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_SetDialect_Call[T]) RunAndReturn(run func(sqltemplate.Dialect)) *WithResults_SetDialect_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Validate provides a mock function with given fields:
|
||||
func (_m *WithResults[T]) Validate() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Validate")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WithResults_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate'
|
||||
type WithResults_Validate_Call[T interface{}] struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Validate is a helper method to define mock.On call
|
||||
func (_e *WithResults_Expecter[T]) Validate() *WithResults_Validate_Call[T] {
|
||||
return &WithResults_Validate_Call[T]{Call: _e.mock.On("Validate")}
|
||||
}
|
||||
|
||||
func (_c *WithResults_Validate_Call[T]) Run(run func()) *WithResults_Validate_Call[T] {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Validate_Call[T]) Return(_a0 error) *WithResults_Validate_Call[T] {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WithResults_Validate_Call[T]) RunAndReturn(run func() error) *WithResults_Validate_Call[T] {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewWithResults creates a new instance of WithResults. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewWithResults[T interface{}](t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *WithResults[T] {
|
||||
mock := &WithResults[T]{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
@ -1,11 +1,18 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// Package-level errors.
|
||||
var (
|
||||
ErrValidationNotImplemented = errors.New("validation not implemented")
|
||||
ErrSQLTemplateNoSerialize = errors.New("SQLTemplate should not be serialized")
|
||||
)
|
||||
|
||||
// SQLTemplate provides comprehensive support for SQL templating, handling
|
||||
// dialect traits, execution arguments and scanning arguments.
|
||||
type SQLTemplate struct {
|
||||
@ -16,22 +23,55 @@ type SQLTemplate struct {
|
||||
|
||||
// New returns a nee *SQLTemplate that will use the given dialect.
|
||||
func New(d Dialect) *SQLTemplate {
|
||||
return &SQLTemplate{
|
||||
Args: Args{
|
||||
d: d,
|
||||
},
|
||||
Dialect: d,
|
||||
}
|
||||
ret := new(SQLTemplate)
|
||||
ret.SetDialect(d)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t *SQLTemplate) Reset() {
|
||||
t.Args.Reset()
|
||||
t.ScanDest.Reset()
|
||||
}
|
||||
|
||||
func (t *SQLTemplate) SetDialect(d Dialect) {
|
||||
t.Reset()
|
||||
t.Dialect = d
|
||||
t.Args.d = d
|
||||
}
|
||||
|
||||
func (t *SQLTemplate) Validate() error {
|
||||
return ErrValidationNotImplemented
|
||||
}
|
||||
|
||||
func (t *SQLTemplate) MarshalJSON() ([]byte, error) {
|
||||
return nil, ErrSQLTemplateNoSerialize
|
||||
}
|
||||
|
||||
func (t *SQLTemplate) UnmarshalJSON([]byte) error {
|
||||
return ErrSQLTemplateNoSerialize
|
||||
}
|
||||
|
||||
//go:generate mockery --with-expecter --name SQLTemplateIface
|
||||
|
||||
// SQLTemplateIface can be used as argument in general purpose utilities
|
||||
// expecting a struct embedding *SQLTemplate.
|
||||
type SQLTemplateIface interface {
|
||||
Dialect
|
||||
ArgsIface
|
||||
ScanDestIface
|
||||
// Reset calls the Reset method of ArgsIface and ScanDestIface.
|
||||
Reset()
|
||||
// SetDialect allows reusing the template components. It should first call
|
||||
// Reset.
|
||||
SetDialect(Dialect)
|
||||
// Validate should be implemented to validate a request before executing the
|
||||
// template.
|
||||
Validate() error
|
||||
}
|
||||
|
||||
//go:generate mockery --with-expecter --name WithResults
|
||||
|
||||
// WithResults has an additional method suited for structs embedding
|
||||
// *SQLTemplate and returning a set of rows.
|
||||
type WithResults[T any] interface {
|
||||
@ -55,11 +95,10 @@ func Execute(t *template.Template, data any) (string, error) {
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// FormatSQL is an opinionated formatter for SQL template output that returns
|
||||
// the code as a oneliner. It can be used to reduce the final code length, for
|
||||
// debugging, and testing. It is not a propoer and full-fledged SQL parser, so
|
||||
// it makes the following assumptions, which are also good practices for writing
|
||||
// your SQL templates:
|
||||
// FormatSQL is an opinionated formatter for SQL template output. It can be used
|
||||
// to reduce the final code length, for debugging, and testing. It is not a
|
||||
// propoer and full-fledged SQL parser, so it makes the following assumptions,
|
||||
// which are also good practices for writing your SQL templates:
|
||||
// 1. There are no SQL comments. Consider adding your comments as template
|
||||
// comments instead (i.e. "{{/* this is a template comment */}}").
|
||||
// 2. There are no multiline strings, and strings do not contain consecutive
|
||||
@ -72,6 +111,7 @@ func FormatSQL(q string) string {
|
||||
for _, f := range formatREs {
|
||||
q = f.re.ReplaceAllString(q, f.replacement)
|
||||
}
|
||||
q = strings.TrimSpace(q)
|
||||
|
||||
return q
|
||||
}
|
||||
@ -88,4 +128,14 @@ var formatREs = []reFormatting{
|
||||
{re: regexp.MustCompile(` ([)\]}])`), replacement: "$1"},
|
||||
{re: regexp.MustCompile(` ?, ?`), replacement: ", "},
|
||||
{re: regexp.MustCompile(` ?([;.:]) ?`), replacement: "$1"},
|
||||
|
||||
// Add newlines and a bit of visual aid
|
||||
{
|
||||
re: regexp.MustCompile(`((UNION|INTERSECT|EXCEPT)( (ALL|DISTINCT))? )?SELECT `),
|
||||
replacement: "\n${1}SELECT ",
|
||||
},
|
||||
{
|
||||
re: regexp.MustCompile(` (FROM|WHERE|GROUP BY|HAVING|WINDOW|ORDER BY|LIMIT|OFFSET) `),
|
||||
replacement: "\n $1 ",
|
||||
},
|
||||
}
|
||||
|
@ -1,10 +1,50 @@
|
||||
package sqltemplate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func TestSQLTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
field := reflect.ValueOf(new(struct {
|
||||
X int
|
||||
})).Elem().FieldByName("X")
|
||||
|
||||
tmpl := New(MySQL)
|
||||
tmpl.Arg(1)
|
||||
_, err := tmpl.Into(field, "colname")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tmpl.SetDialect(SQLite)
|
||||
a := tmpl.GetArgs()
|
||||
d := tmpl.GetScanDest()
|
||||
if len(a) != 0 || len(d) != 0 {
|
||||
t.Fatalf("unexpected values after SetDialect(). Args: %v, ScanDest: %v",
|
||||
a, d)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(tmpl)
|
||||
if b != nil || !errors.Is(err, ErrSQLTemplateNoSerialize) {
|
||||
t.Fatalf("should fail serialization with ErrSQLTemplateNoSerialize")
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(`{}`), &tmpl)
|
||||
if !errors.Is(err, ErrSQLTemplateNoSerialize) {
|
||||
t.Fatalf("should fail deserialization with ErrSQLTemplateNoSerialize")
|
||||
}
|
||||
|
||||
err = tmpl.Validate()
|
||||
if !errors.Is(err, ErrValidationNotImplemented) {
|
||||
t.Fatalf("should fail with ErrValidationNotImplemented")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@ -26,3 +66,26 @@ func TestExecute(t *testing.T) {
|
||||
t.Fatalf("unexpected result, txt: %q, err: %v", txt, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSQL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// TODO: improve testing
|
||||
|
||||
const (
|
||||
input = `
|
||||
SELECT *
|
||||
FROM "mytab" AS t
|
||||
WHERE "id">= 3 AND "str" = ? ;
|
||||
`
|
||||
expected = `SELECT *
|
||||
FROM "mytab" AS t
|
||||
WHERE "id" >= 3 AND "str" = ?;`
|
||||
)
|
||||
|
||||
got := FormatSQL(input)
|
||||
if expected != got {
|
||||
t.Fatalf("Unexpected output.\n\tExpected: %s\n\tActual: %s", expected,
|
||||
got)
|
||||
}
|
||||
}
|
||||
|
1
pkg/services/store/entity/sqlstash/testdata/entity_delete_mysql_sqlite.sql
vendored
Normal file
1
pkg/services/store/entity/sqlstash/testdata/entity_delete_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1 @@
|
||||
DELETE FROM "entity" WHERE 1 = 1 AND "namespace" = ? AND "group" = ? AND "resource" = ? AND "name" = ?;
|
1
pkg/services/store/entity/sqlstash/testdata/entity_delete_postgres.sql
vendored
Normal file
1
pkg/services/store/entity/sqlstash/testdata/entity_delete_postgres.sql
vendored
Normal file
@ -0,0 +1 @@
|
||||
DELETE FROM "entity" WHERE 1 = 1 AND "namespace" = $1 AND "group" = $2 AND "resource" = $3 AND "name" = $4;
|
3
pkg/services/store/entity/sqlstash/testdata/entity_folder_insert_1_mysql_sqlite.sql
vendored
Normal file
3
pkg/services/store/entity/sqlstash/testdata/entity_folder_insert_1_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
INSERT INTO "entity_folder"
|
||||
("guid", "namespace", "name", "slug_path", "tree", "depth", "lft", "rgt", "detached")
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
5
pkg/services/store/entity/sqlstash/testdata/entity_folder_insert_2_mysql_sqlite.sql
vendored
Normal file
5
pkg/services/store/entity/sqlstash/testdata/entity_folder_insert_2_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
INSERT INTO "entity_folder"
|
||||
("guid", "namespace", "name", "slug_path", "tree", "depth", "lft", "rgt", "detached")
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?),
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?);
|
3
pkg/services/store/entity/sqlstash/testdata/entity_history_insert_mysql_sqlite.sql
vendored
Normal file
3
pkg/services/store/entity/sqlstash/testdata/entity_history_insert_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
INSERT INTO "entity_history"
|
||||
("guid", "resource_version", "key", "group", "group_version", "resource", "namespace", "name", "folder", "meta", "body", "status", "size", "etag", "created_at", "created_by", "updated_at", "updated_by", "origin", "origin_key", "origin_ts", "title", "slug", "description", "message", "labels", "fields", "errors", "action")
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
5
pkg/services/store/entity/sqlstash/testdata/entity_history_read_full_mysql.sql
vendored
Normal file
5
pkg/services/store/entity/sqlstash/testdata/entity_history_read_full_mysql.sql
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
SELECT e."guid", e."resource_version", e."key", e."group", e."group_version", e."resource", e."namespace", e."name", e."folder", e."meta", e."body", e."status", e."size", e."etag", e."created_at", e."created_by", e."updated_at", e."updated_by", e."origin", e."origin_key", e."origin_ts", e."title", e."slug", e."description", e."message", e."labels", e."fields", e."errors", e."action"
|
||||
FROM "entity_history" AS e
|
||||
WHERE 1 = 1 AND "namespace" = ? AND "group" = ? AND "resource" = ? AND "name" = ? AND "resource_version" <= ?
|
||||
ORDER BY "resource_version" DESC
|
||||
LIMIT 1 FOR UPDATE NOWAIT;
|
3
pkg/services/store/entity/sqlstash/testdata/entity_insert_mysql_sqlite.sql
vendored
Normal file
3
pkg/services/store/entity/sqlstash/testdata/entity_insert_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
INSERT INTO "entity"
|
||||
("guid", "resource_version", "key", "group", "group_version", "resource", "namespace", "name", "folder", "meta", "body", "status", "size", "etag", "created_at", "created_by", "updated_at", "updated_by", "origin", "origin_key", "origin_ts", "title", "slug", "description", "message", "labels", "fields", "errors", "action")
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
1
pkg/services/store/entity/sqlstash/testdata/entity_labels_delete_1_mysql_sqlite.sql
vendored
Normal file
1
pkg/services/store/entity/sqlstash/testdata/entity_labels_delete_1_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1 @@
|
||||
DELETE FROM "entity_labels" WHERE 1 = 1 AND "guid" = ? AND "label" NOT IN (?);
|
1
pkg/services/store/entity/sqlstash/testdata/entity_labels_delete_2_mysql_sqlite.sql
vendored
Normal file
1
pkg/services/store/entity/sqlstash/testdata/entity_labels_delete_2_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1 @@
|
||||
DELETE FROM "entity_labels" WHERE 1 = 1 AND "guid" = ? AND "label" NOT IN (?, ?);
|
2
pkg/services/store/entity/sqlstash/testdata/entity_labels_insert_1_mysql_sqlite.sql
vendored
Normal file
2
pkg/services/store/entity/sqlstash/testdata/entity_labels_insert_1_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
INSERT INTO "entity_labels" ("guid", "label", "value")
|
||||
VALUES (?, ?, ?);
|
3
pkg/services/store/entity/sqlstash/testdata/entity_labels_insert_2_mysql_sqlite.sql
vendored
Normal file
3
pkg/services/store/entity/sqlstash/testdata/entity_labels_insert_2_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
INSERT INTO "entity_labels" ("guid", "label", "value") VALUES
|
||||
(?, ?, ?),
|
||||
(?, ?, ?);
|
3
pkg/services/store/entity/sqlstash/testdata/entity_list_folder_elements_mysql_sqlite.sql
vendored
Normal file
3
pkg/services/store/entity/sqlstash/testdata/entity_list_folder_elements_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
SELECT "guid", "name", "folder", "name", "slug"
|
||||
FROM "entity"
|
||||
WHERE 1 = 1 AND "group" = ? AND "resource" = ? AND "namespace" = ?;
|
3
pkg/services/store/entity/sqlstash/testdata/entity_read_mysql_sqlite.sql
vendored
Normal file
3
pkg/services/store/entity/sqlstash/testdata/entity_read_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
SELECT e."guid", e."resource_version", e."key", e."group", e."group_version", e."resource", e."namespace", e."name", e."folder", e."meta", e."body", e."status", e."size", e."etag", e."created_at", e."created_by", e."updated_at", e."updated_by", e."origin", e."origin_key", e."origin_ts", e."title", e."slug", e."description", e."message", e."labels", e."fields", e."errors", e."action"
|
||||
FROM "entity" AS e
|
||||
WHERE 1 = 1 AND "namespace" = ? AND "group" = ? AND "resource" = ? AND "name" = ?;
|
2
pkg/services/store/entity/sqlstash/testdata/entity_update_mysql_sqlite.sql
vendored
Normal file
2
pkg/services/store/entity/sqlstash/testdata/entity_update_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
UPDATE "entity" SET "resource_version" = ?, "group_version" = ?, "folder" = ?, "meta" = ?, "body" = ?, "status" = ?, "size" = ?, "etag" = ?, "updated_at" = ?, "updated_by" = ?, "origin" = ?, "origin_key" = ?, "origin_ts" = ?, "title" = ?, "slug" = ?, "description" = ?, "message" = ?, "labels" = ?, "fields" = ?, "errors" = ?, "action" = ?
|
||||
WHERE "guid" = ?;
|
21
pkg/services/store/entity/sqlstash/testdata/grpc-req-create.json
vendored
Normal file
21
pkg/services/store/entity/sqlstash/testdata/grpc-req-create.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"entity": {
|
||||
"guid": "b0199c60-5f3a-41be-9ba6-7a52f1de83ff",
|
||||
"group": "playlist.grafana.app",
|
||||
"resource": "playlists",
|
||||
"namespace": "default",
|
||||
"name": "adnj1llchbbi8a",
|
||||
"group_version": "v0alpha1",
|
||||
"key": "/playlist.grafana.app/playlists/namespaces/default/adnj1llchbbi8a",
|
||||
"meta": "eyJtZXRhZGF0YSI6eyJuYW1lIjoiYWRuajFsbGNoYmJpOGEiLCJuYW1lc3BhY2UiOiJkZWZhdWx0IiwidWlkIjoiYjAxOTljNjAtNWYzYS00MWJlLTliYTYtN2E1MmYxZGU4M2ZmIiwiY3JlYXRpb25UaW1lc3RhbXAiOiIyMDI0LTA2LTAyVDAzOjI4OjE3WiIsImFubm90YXRpb25zIjp7ImdyYWZhbmEuYXBwL29yaWdpbktleSI6IjIiLCJncmFmYW5hLmFwcC9vcmlnaW5OYW1lIjoiU1FMIiwiZ3JhZmFuYS5hcHAvb3JpZ2luVGltZXN0YW1wIjoiMjAyNC0wNi0wMlQwMzoyODoxN1oiLCJncmFmYW5hLmFwcC91cGRhdGVkVGltZXN0YW1wIjoiMjAyNC0wNi0wMlQwMzoyODoxN1oifX19",
|
||||
"body": "eyJraW5kIjoiUGxheWxpc3QiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJtZXRhZGF0YSI6eyJuYW1lIjoiYWRuajFsbGNoYmJpOGEiLCJuYW1lc3BhY2UiOiJkZWZhdWx0IiwidWlkIjoiYjAxOTljNjAtNWYzYS00MWJlLTliYTYtN2E1MmYxZGU4M2ZmIiwiY3JlYXRpb25UaW1lc3RhbXAiOiIyMDI0LTA2LTAyVDAzOjI4OjE3WiIsImFubm90YXRpb25zIjp7ImdyYWZhbmEuYXBwL29yaWdpbktleSI6IjIiLCJncmFmYW5hLmFwcC9vcmlnaW5OYW1lIjoiU1FMIiwiZ3JhZmFuYS5hcHAvb3JpZ2luVGltZXN0YW1wIjoiMjAyNC0wNi0wMlQwMzoyODoxN1oiLCJncmFmYW5hLmFwcC91cGRhdGVkVGltZXN0YW1wIjoiMjAyNC0wNi0wMlQwMzoyODoxN1oifX0sInNwZWMiOnsidGl0bGUiOiJ0ZXN0IHBsYXlsaXN0IiwiaW50ZXJ2YWwiOiI1bSIsIml0ZW1zIjpbeyJ0eXBlIjoiZGFzaGJvYXJkX2J5X3VpZCIsInZhbHVlIjoiY2RuaXY1M2dtZDR3MGUifV19fQo=",
|
||||
"title": "test playlist",
|
||||
"created_at": 1717298897750,
|
||||
"updated_at": 1717298897000,
|
||||
"origin": {
|
||||
"source": "SQL",
|
||||
"key": "2",
|
||||
"time": 1717298897000
|
||||
}
|
||||
}
|
||||
}
|
3
pkg/services/store/entity/sqlstash/testdata/grpc-req-delete.json
vendored
Normal file
3
pkg/services/store/entity/sqlstash/testdata/grpc-req-delete.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"key": "/playlist.grafana.app/playlists/namespaces/default/sdfsdfsdf"
|
||||
}
|
18
pkg/services/store/entity/sqlstash/testdata/grpc-req-update.json
vendored
Normal file
18
pkg/services/store/entity/sqlstash/testdata/grpc-req-update.json
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"entity": {
|
||||
"guid": "3c769b2e-aaa7-46f6-ab83-e038050a6a75",
|
||||
"resource_version": 1,
|
||||
"group": "playlist.grafana.app",
|
||||
"resource": "playlists",
|
||||
"namespace": "default",
|
||||
"name": "sdfsdfsdf",
|
||||
"group_version": "v0alpha1",
|
||||
"key": "/playlist.grafana.app/playlists/namespaces/default/sdfsdfsdf",
|
||||
"meta": "eyJtZXRhZGF0YSI6eyJuYW1lIjoic2Rmc2Rmc2RmIiwibmFtZXNwYWNlIjoiZGVmYXVsdCIsInVpZCI6IjNjNzY5YjJlLWFhYTctNDZmNi1hYjgzLWUwMzgwNTBhNmE3NSIsInJlc291cmNlVmVyc2lvbiI6IjEiLCJjcmVhdGlvblRpbWVzdGFtcCI6IjIwMjQtMDYtMDJUMDM6NDk6MjlaIiwibWFuYWdlZEZpZWxkcyI6W3sibWFuYWdlciI6Ik1vemlsbGEiLCJvcGVyYXRpb24iOiJVcGRhdGUiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJ0aW1lIjoiMjAyNC0wNi0wMlQwMzo1Mzo1NVoiLCJmaWVsZHNUeXBlIjoiRmllbGRzVjEiLCJmaWVsZHNWMSI6eyJmOnNwZWMiOnsiZjppbnRlcnZhbCI6e30sImY6aXRlbXMiOnt9LCJmOnRpdGxlIjp7fX19fV19fQ==",
|
||||
"body": "eyJraW5kIjoiUGxheWxpc3QiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJtZXRhZGF0YSI6eyJuYW1lIjoic2Rmc2Rmc2RmIiwibmFtZXNwYWNlIjoiZGVmYXVsdCIsInVpZCI6IjNjNzY5YjJlLWFhYTctNDZmNi1hYjgzLWUwMzgwNTBhNmE3NSIsInJlc291cmNlVmVyc2lvbiI6IjEiLCJjcmVhdGlvblRpbWVzdGFtcCI6IjIwMjQtMDYtMDJUMDM6NDk6MjlaIiwibWFuYWdlZEZpZWxkcyI6W3sibWFuYWdlciI6Ik1vemlsbGEiLCJvcGVyYXRpb24iOiJVcGRhdGUiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJ0aW1lIjoiMjAyNC0wNi0wMlQwMzo1Mzo1NVoiLCJmaWVsZHNUeXBlIjoiRmllbGRzVjEiLCJmaWVsZHNWMSI6eyJmOnNwZWMiOnsiZjppbnRlcnZhbCI6e30sImY6aXRlbXMiOnt9LCJmOnRpdGxlIjp7fX19fV19LCJzcGVjIjp7InRpdGxlIjoieHpjdnp4Y3Zxd2Vxd2UiLCJpbnRlcnZhbCI6IjVtIiwiaXRlbXMiOlt7InR5cGUiOiJkYXNoYm9hcmRfYnlfdWlkIiwidmFsdWUiOiJjZG5pdjUzZ21kNHcwZSJ9XX19Cg==",
|
||||
"title": "xzcvzxcvqweqwe",
|
||||
"created_at": 1717300169240,
|
||||
"origin": {}
|
||||
},
|
||||
"previous_version": 1
|
||||
}
|
21
pkg/services/store/entity/sqlstash/testdata/grpc-res-entity.json
vendored
Normal file
21
pkg/services/store/entity/sqlstash/testdata/grpc-res-entity.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"guid": "5842f146-07b9-405b-af76-b4c4b2612518",
|
||||
"resource_version": 6,
|
||||
"group": "playlist.grafana.app",
|
||||
"resource": "playlists",
|
||||
"namespace": "default",
|
||||
"name": "sdfsdfsdf",
|
||||
"group_version": "v0alpha1",
|
||||
"key": "/playlist.grafana.app/playlists/namespaces/default/sdfsdfsdf",
|
||||
"meta": "eyJtZXRhZGF0YSI6eyJuYW1lIjoic2Rmc2Rmc2RmIiwibmFtZXNwYWNlIjoiZGVmYXVsdCIsInVpZCI6IjAyZmVhOGVlLTk2ZDYtNGIzMy04ZGI5LTU5MmI0NzU4NTM4NSIsImNyZWF0aW9uVGltZXN0YW1wIjoiMjAyNC0wNi0wNFQxNToxODozNFoiLCJtYW5hZ2VkRmllbGRzIjpbeyJtYW5hZ2VyIjoiTW96aWxsYSIsIm9wZXJhdGlvbiI6IlVwZGF0ZSIsImFwaVZlcnNpb24iOiJwbGF5bGlzdC5ncmFmYW5hLmFwcC92MGFscGhhMSIsInRpbWUiOiIyMDI0LTA2LTA0VDE1OjE4OjM0WiIsImZpZWxkc1R5cGUiOiJGaWVsZHNWMSIsImZpZWxkc1YxIjp7ImY6c3BlYyI6eyJmOmludGVydmFsIjp7fSwiZjppdGVtcyI6e30sImY6dGl0bGUiOnt9fX19XX19",
|
||||
"body": "eyJraW5kIjoiUGxheWxpc3QiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJtZXRhZGF0YSI6eyJuYW1lIjoic2Rmc2Rmc2RmIiwibmFtZXNwYWNlIjoiZGVmYXVsdCIsInVpZCI6IjAyZmVhOGVlLTk2ZDYtNGIzMy04ZGI5LTU5MmI0NzU4NTM4NSIsImNyZWF0aW9uVGltZXN0YW1wIjoiMjAyNC0wNi0wNFQxNToxODozNFoiLCJtYW5hZ2VkRmllbGRzIjpbeyJtYW5hZ2VyIjoiTW96aWxsYSIsIm9wZXJhdGlvbiI6IlVwZGF0ZSIsImFwaVZlcnNpb24iOiJwbGF5bGlzdC5ncmFmYW5hLmFwcC92MGFscGhhMSIsInRpbWUiOiIyMDI0LTA2LTA0VDE1OjE4OjM0WiIsImZpZWxkc1R5cGUiOiJGaWVsZHNWMSIsImZpZWxkc1YxIjp7ImY6c3BlYyI6eyJmOmludGVydmFsIjp7fSwiZjppdGVtcyI6e30sImY6dGl0bGUiOnt9fX19XX0sInNwZWMiOnsidGl0bGUiOiJ4emN2enhjdiIsImludGVydmFsIjoiNW0iLCJpdGVtcyI6W3sidHlwZSI6ImRhc2hib2FyZF9ieV91aWQiLCJ2YWx1ZSI6ImNkbml2NTNnbWQ0dzBlIn1dfX0K",
|
||||
"title": "xzcvzxcv",
|
||||
"size": 540,
|
||||
"ETag": "3225612903101e3b94f458cfc79baf89",
|
||||
"created_at": 1717514314565,
|
||||
"created_by": "user:1:admin",
|
||||
"updated_at": 1717514314565,
|
||||
"updated_by": "user:1:admin",
|
||||
"origin": {},
|
||||
"action": 1
|
||||
}
|
3
pkg/services/store/entity/sqlstash/testdata/kind_version_get_mysql_sqlite.sql
vendored
Normal file
3
pkg/services/store/entity/sqlstash/testdata/kind_version_get_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
SELECT "resource_version", "created_at", "updated_at"
|
||||
FROM "kind_version"
|
||||
WHERE 1 = 1 AND "group" = ? AND "resource" = ?;
|
4
pkg/services/store/entity/sqlstash/testdata/kind_version_inc_mysql_sqlite.sql
vendored
Normal file
4
pkg/services/store/entity/sqlstash/testdata/kind_version_inc_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
UPDATE "kind_version"
|
||||
SET "resource_version" = ? + 1,
|
||||
"updated_at" = ?
|
||||
WHERE 1 = 1 AND "group" = ? AND "resource" = ? AND "resource_version" = ?;
|
3
pkg/services/store/entity/sqlstash/testdata/kind_version_insert_mysql_sqlite.sql
vendored
Normal file
3
pkg/services/store/entity/sqlstash/testdata/kind_version_insert_mysql_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
INSERT INTO "kind_version"
|
||||
("group", "resource", "resource_version", "created_at", "updated_at")
|
||||
VALUES (?, ?, 1, ?, ?);
|
3
pkg/services/store/entity/sqlstash/testdata/kind_version_lock_mysql.sql
vendored
Normal file
3
pkg/services/store/entity/sqlstash/testdata/kind_version_lock_mysql.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
SELECT "resource_version"
|
||||
FROM "kind_version"
|
||||
WHERE 1 = 1 AND "group" = ? AND "resource" = ? FOR UPDATE;
|
3
pkg/services/store/entity/sqlstash/testdata/kind_version_lock_postgres.sql
vendored
Normal file
3
pkg/services/store/entity/sqlstash/testdata/kind_version_lock_postgres.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
SELECT "resource_version"
|
||||
FROM "kind_version"
|
||||
WHERE 1 = 1 AND "group" = $1 AND "resource" = $2 FOR UPDATE;
|
3
pkg/services/store/entity/sqlstash/testdata/kind_version_lock_sqlite.sql
vendored
Normal file
3
pkg/services/store/entity/sqlstash/testdata/kind_version_lock_sqlite.sql
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
SELECT "resource_version"
|
||||
FROM "kind_version"
|
||||
WHERE 1 = 1 AND "group" = ? AND "resource" = ?;
|
@ -15,11 +15,13 @@ import (
|
||||
)
|
||||
|
||||
func createETag(body []byte, meta []byte, status []byte) string {
|
||||
// TODO: can we change this to something more modern like sha256?
|
||||
h := md5.New()
|
||||
_, _ = h.Write(meta)
|
||||
_, _ = h.Write(body)
|
||||
_, _ = h.Write(status)
|
||||
hash := h.Sum(nil)
|
||||
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
@ -34,8 +36,7 @@ func getCurrentUser(ctx context.Context) (string, error) {
|
||||
return store.GetUserIDString(user), nil
|
||||
}
|
||||
|
||||
// ptrOr returns the first non-nil pointer in the least or a new non-nil
|
||||
// pointer.
|
||||
// ptrOr returns the first non-nil pointer in the list or a new non-nil pointer.
|
||||
func ptrOr[P ~*E, E any](ps ...P) P {
|
||||
for _, p := range ps {
|
||||
if p != nil {
|
||||
@ -46,8 +47,8 @@ func ptrOr[P ~*E, E any](ps ...P) P {
|
||||
return P(new(E))
|
||||
}
|
||||
|
||||
// sliceOr returns the first slice that has at least one element, or a non-nil
|
||||
// empty slice.
|
||||
// sliceOr returns the first slice that has at least one element, or a new empty
|
||||
// slice.
|
||||
func sliceOr[S ~[]E, E comparable](vals ...S) S {
|
||||
for _, s := range vals {
|
||||
if len(s) > 0 {
|
||||
@ -58,7 +59,7 @@ func sliceOr[S ~[]E, E comparable](vals ...S) S {
|
||||
return S{}
|
||||
}
|
||||
|
||||
// mapOr returns the first map that has at least one element, or a non-nil empty
|
||||
// mapOr returns the first map that has at least one element, or a new empty
|
||||
// map.
|
||||
func mapOr[M ~map[K]V, K comparable, V any](vals ...M) M {
|
||||
for _, m := range vals {
|
||||
@ -70,61 +71,16 @@ func mapOr[M ~map[K]V, K comparable, V any](vals ...M) M {
|
||||
return M{}
|
||||
}
|
||||
|
||||
// countTrue returns the number of true values in its arguments.
|
||||
func countTrue(bools ...bool) uint64 {
|
||||
var ret uint64
|
||||
for _, b := range bools {
|
||||
if b {
|
||||
ret++
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// query uses `req` as input and output for a zero or more row-returning query
|
||||
// generated with `tmpl`, and executed in `x`.
|
||||
func query[T any](ctx context.Context, x db.ContextExecer, tmpl *template.Template, req sqltemplate.WithResults[T]) ([]T, error) {
|
||||
rawQuery, err := sqltemplate.Execute(tmpl, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("execute template: %w", err)
|
||||
}
|
||||
query := sqltemplate.FormatSQL(rawQuery)
|
||||
|
||||
rows, err := x.QueryContext(ctx, query, req.GetArgs()...)
|
||||
if err != nil {
|
||||
return nil, SQLError{
|
||||
Err: err,
|
||||
CallType: "Query",
|
||||
Arguments: req.GetArgs(),
|
||||
ScanDest: req.GetScanDest(),
|
||||
Query: query,
|
||||
RawQuery: rawQuery,
|
||||
}
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var ret []T
|
||||
for rows.Next() {
|
||||
res, err := scanRow(rows, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret = append(ret, res)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows err: %w", err)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// queryRow uses `req` as input and output for a single-row returning query
|
||||
// generated with `tmpl`, and executed in `x`.
|
||||
func queryRow[T any](ctx context.Context, x db.ContextExecer, tmpl *template.Template, req sqltemplate.WithResults[T]) (T, error) {
|
||||
var zero T
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
return zero, fmt.Errorf("query: invalid request for template %q: %w",
|
||||
tmpl.Name(), err)
|
||||
}
|
||||
|
||||
rawQuery, err := sqltemplate.Execute(tmpl, req)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("execute template: %w", err)
|
||||
@ -134,12 +90,13 @@ func queryRow[T any](ctx context.Context, x db.ContextExecer, tmpl *template.Tem
|
||||
row := x.QueryRowContext(ctx, query, req.GetArgs()...)
|
||||
if err := row.Err(); err != nil {
|
||||
return zero, SQLError{
|
||||
Err: err,
|
||||
CallType: "QueryRow",
|
||||
Arguments: req.GetArgs(),
|
||||
ScanDest: req.GetScanDest(),
|
||||
Query: query,
|
||||
RawQuery: rawQuery,
|
||||
Err: err,
|
||||
CallType: "QueryRow",
|
||||
TemplateName: tmpl.Name(),
|
||||
arguments: req.GetArgs(),
|
||||
ScanDest: req.GetScanDest(),
|
||||
Query: query,
|
||||
RawQuery: rawQuery,
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,6 +125,11 @@ func scanRow[T any](sc scanner, req sqltemplate.WithResults[T]) (zero T, err err
|
||||
// exec uses `req` as input for a non-data returning query generated with
|
||||
// `tmpl`, and executed in `x`.
|
||||
func exec(ctx context.Context, x db.ContextExecer, tmpl *template.Template, req sqltemplate.SQLTemplateIface) (sql.Result, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("exec: invalid request for template %q: %w",
|
||||
tmpl.Name(), err)
|
||||
}
|
||||
|
||||
rawQuery, err := sqltemplate.Execute(tmpl, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("execute template: %w", err)
|
||||
@ -177,11 +139,12 @@ func exec(ctx context.Context, x db.ContextExecer, tmpl *template.Template, req
|
||||
res, err := x.ExecContext(ctx, query, req.GetArgs()...)
|
||||
if err != nil {
|
||||
return nil, SQLError{
|
||||
Err: err,
|
||||
CallType: "Exec",
|
||||
Arguments: req.GetArgs(),
|
||||
Query: query,
|
||||
RawQuery: rawQuery,
|
||||
Err: err,
|
||||
CallType: "Exec",
|
||||
TemplateName: tmpl.Name(),
|
||||
arguments: req.GetArgs(),
|
||||
Query: query,
|
||||
RawQuery: rawQuery,
|
||||
}
|
||||
}
|
||||
|
||||
|
525
pkg/services/store/entity/sqlstash/utils_test.go
Normal file
525
pkg/services/store/entity/sqlstash/utils_test.go
Normal file
@ -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))
|
||||
})
|
||||
}
|
22
pkg/services/store/entity/sqlstash/validation.go
Normal file
22
pkg/services/store/entity/sqlstash/validation.go
Normal file
@ -0,0 +1,22 @@
|
||||
package sqlstash
|
||||
|
||||
import "github.com/grafana/grafana/pkg/services/store/entity"
|
||||
|
||||
// validateEntity validates a fully loaded *entity.Entity model, and should be
|
||||
// used before storing an entity to the database and before returning it to the
|
||||
// user.
|
||||
func validateEntity(*entity.Entity) error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
// validateLabels validates the given map of label names to their values.
|
||||
func validateLabels(map[string]string) error {
|
||||
// this should be called by validateEntity
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
// validateFields validates the given map of fields names to their values.
|
||||
func validateFields(map[string]string) error {
|
||||
// this should be called by validateEntity
|
||||
return nil // TODO
|
||||
}
|
35
pkg/services/store/entity/sqlstash/validation_test.go
Normal file
35
pkg/services/store/entity/sqlstash/validation_test.go
Normal file
@ -0,0 +1,35 @@
|
||||
package sqlstash
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateEntity(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := validateEntity(newEmptyEntity())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidateLabels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := validateLabels(map[string]string{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidateFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := validateFields(map[string]string{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Silence the `unused` linter until we implement and use these validations.
|
||||
var (
|
||||
_ = validateEntity
|
||||
_ = validateLabels
|
||||
_ = validateFields
|
||||
)
|
113
pkg/util/testutil/context.go
Normal file
113
pkg/util/testutil/context.go
Normal file
@ -0,0 +1,113 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/appcontext"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
const DefaultContextTimeout = time.Second
|
||||
|
||||
// TestContext is a context.Context that can be canceled with or without a
|
||||
// cause. This is only relevant for testing purposes.
|
||||
type TestContext interface {
|
||||
context.Context
|
||||
|
||||
// Cancel cancels the context. The `Err` method and the `context.Cause`
|
||||
// function will return context.Canceled.
|
||||
Cancel()
|
||||
|
||||
// CancelCause cancels the current context with the given cause. The `Err`
|
||||
// method will return context.Canceled and the `context.Cause` function will
|
||||
// return the given error.
|
||||
CancelCause(err error)
|
||||
|
||||
// WithUser returns a derived user with the given user associated. To derive
|
||||
// a context without an associated user, pass a nil value.
|
||||
WithUser(*user.SignedInUser) TestContext
|
||||
}
|
||||
|
||||
// NewDefaultTestContext calls NewTestContext with the provided `t` and a
|
||||
// timeout of DefaultContextTimeout. This should work fine for most unit tests.
|
||||
func NewDefaultTestContext(t T) TestContext {
|
||||
return NewTestContext(t, time.Now().Add(DefaultContextTimeout))
|
||||
}
|
||||
|
||||
// NewTestCtx returns a new TestContext with the following features:
|
||||
// 1. Provides a `deadline` argument which is especially useful in integration
|
||||
// tests.
|
||||
// 2. It honours the `-timeout` flag of `go test` if it is given, so it will
|
||||
// actually timeout at the earliest deadline (either the one resulting from
|
||||
// the command line or the one resulting from the `deadline` argument).
|
||||
// 3. By default it has an empty user (i.e. the user at the UI login), so most
|
||||
// of the code paths to be tested will not need any special setup, unless
|
||||
// you need to test permissions. In that case, you can use any of the test
|
||||
// users from the SignedInUser struct (all of them come from real payloads).
|
||||
func NewTestContext(t T, deadline time.Time) TestContext {
|
||||
t.Helper()
|
||||
|
||||
// if the test has a deadline and it happens before our previously
|
||||
// calculated deadline, then use it instead
|
||||
if td, ok := t.Deadline(); ok && td.Before(deadline) {
|
||||
deadline = td
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithDeadline(context.Background(), deadline)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
ctx, cancelCause := context.WithCancelCause(ctx)
|
||||
|
||||
tctx := testContextFunc(func() (context.Context, context.CancelFunc, context.CancelCauseFunc) {
|
||||
return ctx, cancel, cancelCause
|
||||
})
|
||||
|
||||
user, err := SignedInUser{}.NewEmpty()
|
||||
require.NoError(t, err)
|
||||
|
||||
// TODO: improve by adding a better anonymous user struct
|
||||
return tctx.WithUser(user)
|
||||
}
|
||||
|
||||
type testContextFunc func() (context.Context, context.CancelFunc, context.CancelCauseFunc)
|
||||
|
||||
func (f testContextFunc) Deadline() (deadline time.Time, ok bool) {
|
||||
ctx, _, _ := f()
|
||||
return ctx.Deadline()
|
||||
}
|
||||
|
||||
func (f testContextFunc) Done() <-chan struct{} {
|
||||
ctx, _, _ := f()
|
||||
return ctx.Done()
|
||||
}
|
||||
|
||||
func (f testContextFunc) Err() error {
|
||||
ctx, _, _ := f()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (f testContextFunc) Value(key any) any {
|
||||
ctx, _, _ := f()
|
||||
return ctx.Value(key)
|
||||
}
|
||||
|
||||
func (f testContextFunc) Cancel() {
|
||||
_, c, _ := f()
|
||||
c()
|
||||
}
|
||||
|
||||
func (f testContextFunc) CancelCause(err error) {
|
||||
_, _, cc := f()
|
||||
cc(err)
|
||||
}
|
||||
|
||||
func (f testContextFunc) WithUser(usr *user.SignedInUser) TestContext {
|
||||
ctx := appcontext.WithUser(f, usr)
|
||||
|
||||
return testContextFunc(func() (context.Context, context.CancelFunc, context.CancelCauseFunc) {
|
||||
return ctx, f.Cancel, f.CancelCause
|
||||
})
|
||||
}
|
90
pkg/util/testutil/context_test.go
Normal file
90
pkg/util/testutil/context_test.go
Normal file
@ -0,0 +1,90 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util/testutil/mocks"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// make sure we don't leak goroutines after tests in this package have
|
||||
// finished, which means we haven't leaked contexts either
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func TestTestContextFunc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const tolerance = 100 * time.Millisecond
|
||||
|
||||
t.Run("no explicit deadline - no test deadline", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := mocks.NewT(t)
|
||||
tt.EXPECT().Helper()
|
||||
tt.EXPECT().Deadline().Return(time.Time{}, false).Once()
|
||||
tt.EXPECT().Cleanup(mock.Anything).Once()
|
||||
|
||||
ctx := NewDefaultTestContext(tt)
|
||||
d, ok := ctx.Deadline()
|
||||
require.True(t, ok)
|
||||
require.False(t, d.IsZero())
|
||||
diff := time.Now().Add(DefaultContextTimeout).Sub(d)
|
||||
require.GreaterOrEqual(t, diff, time.Duration(0))
|
||||
require.Less(t, diff, tolerance)
|
||||
|
||||
ctx.Cancel()
|
||||
require.ErrorIs(t, ctx.Err(), context.Canceled)
|
||||
|
||||
// already canceled, we shouldn't be able to set a cause now
|
||||
ctx.CancelCause(context.DeadlineExceeded)
|
||||
require.ErrorIs(t, context.Cause(ctx), context.Canceled)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
default:
|
||||
t.Fatalf("done channel not closed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("explicit deadline - earlier test deadline", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// make sure the context will be deadlined already at creation
|
||||
now := time.Now().Add(-time.Second)
|
||||
|
||||
tt := mocks.NewT(t)
|
||||
tt.EXPECT().Helper()
|
||||
tt.EXPECT().Deadline().Return(now, true).Once()
|
||||
tt.EXPECT().Cleanup(mock.Anything).Once()
|
||||
|
||||
ctx := NewTestContext(tt, now.Add(time.Second))
|
||||
d, ok := ctx.Deadline()
|
||||
require.True(t, ok)
|
||||
require.Equal(t, now, d)
|
||||
require.ErrorIs(t, ctx.Err(), context.DeadlineExceeded)
|
||||
})
|
||||
|
||||
t.Run("explicit deadline - later test deadline", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now().Add(-time.Second)
|
||||
|
||||
tt := mocks.NewT(t)
|
||||
tt.EXPECT().Helper()
|
||||
tt.EXPECT().Deadline().Return(now.Add(time.Hour), true).Once()
|
||||
tt.EXPECT().Cleanup(mock.Anything).Once()
|
||||
|
||||
ctx := NewTestContext(tt, now)
|
||||
d, ok := ctx.Deadline()
|
||||
require.True(t, ok)
|
||||
require.Equal(t, now, d)
|
||||
require.ErrorIs(t, ctx.Err(), context.DeadlineExceeded)
|
||||
})
|
||||
}
|
22
pkg/util/testutil/data/user-anonymous.json
Normal file
22
pkg/util/testutil/data/user-anonymous.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"UserID": 0,
|
||||
"UserUID": "",
|
||||
"OrgID": 1,
|
||||
"OrgName": "Main Org.",
|
||||
"OrgRole": "Viewer",
|
||||
"Login": "",
|
||||
"Name": "",
|
||||
"Email": "",
|
||||
"EmailVerified": false,
|
||||
"AuthID": "",
|
||||
"AuthenticatedBy": "",
|
||||
"ApiKeyID": 0,
|
||||
"IsServiceAccount": false,
|
||||
"IsGrafanaAdmin": false,
|
||||
"IsAnonymous": true,
|
||||
"IsDisabled": false,
|
||||
"HelpFlags1": 0,
|
||||
"LastSeenAt": "0001-01-01T00:00:00Z",
|
||||
"Teams": null,
|
||||
"NamespacedID": {}
|
||||
}
|
22
pkg/util/testutil/data/user-editor.json
Normal file
22
pkg/util/testutil/data/user-editor.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"UserID": 2,
|
||||
"UserUID": "ednity0wr3d34d",
|
||||
"OrgID": 1,
|
||||
"OrgName": "Main Org.",
|
||||
"OrgRole": "Editor",
|
||||
"Login": "editor",
|
||||
"Name": "editor",
|
||||
"Email": "editor",
|
||||
"EmailVerified": false,
|
||||
"AuthID": "",
|
||||
"AuthenticatedBy": "",
|
||||
"ApiKeyID": 0,
|
||||
"IsServiceAccount": false,
|
||||
"IsGrafanaAdmin": false,
|
||||
"IsAnonymous": false,
|
||||
"IsDisabled": false,
|
||||
"HelpFlags1": 0,
|
||||
"LastSeenAt": "2024-06-01T23:03:26-03:00",
|
||||
"Teams": [],
|
||||
"NamespacedID": {}
|
||||
}
|
22
pkg/util/testutil/data/user-empty.json
Normal file
22
pkg/util/testutil/data/user-empty.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"UserID": 0,
|
||||
"UserUID": "",
|
||||
"OrgID": 0,
|
||||
"OrgName": "",
|
||||
"OrgRole": "",
|
||||
"Login": "",
|
||||
"Name": "",
|
||||
"Email": "",
|
||||
"EmailVerified": false,
|
||||
"AuthID": "",
|
||||
"AuthenticatedBy": "",
|
||||
"ApiKeyID": 0,
|
||||
"IsServiceAccount": false,
|
||||
"IsGrafanaAdmin": false,
|
||||
"IsAnonymous": false,
|
||||
"IsDisabled": false,
|
||||
"HelpFlags1": 0,
|
||||
"LastSeenAt": "0001-01-01T00:00:00Z",
|
||||
"Teams": null,
|
||||
"NamespacedID": {}
|
||||
}
|
22
pkg/util/testutil/data/user-grafana-admin.json
Normal file
22
pkg/util/testutil/data/user-grafana-admin.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"UserID": 1,
|
||||
"UserUID": "",
|
||||
"OrgID": 1,
|
||||
"OrgName": "Main Org.",
|
||||
"OrgRole": "Admin",
|
||||
"Login": "admin",
|
||||
"Name": "",
|
||||
"Email": "admin@localhost",
|
||||
"EmailVerified": false,
|
||||
"AuthID": "",
|
||||
"AuthenticatedBy": "",
|
||||
"ApiKeyID": 0,
|
||||
"IsServiceAccount": false,
|
||||
"IsGrafanaAdmin": true,
|
||||
"IsAnonymous": false,
|
||||
"IsDisabled": false,
|
||||
"HelpFlags1": 0,
|
||||
"LastSeenAt": "2024-06-01T23:01:11-03:00",
|
||||
"Teams": [],
|
||||
"NamespacedID": {}
|
||||
}
|
22
pkg/util/testutil/data/user-service-account-viewer.json
Normal file
22
pkg/util/testutil/data/user-service-account-viewer.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"UserID": 4,
|
||||
"UserUID": "fdnivb2e7c4cga",
|
||||
"OrgID": 1,
|
||||
"OrgName": "Main Org.",
|
||||
"OrgRole": "Viewer",
|
||||
"Login": "sa-1-something",
|
||||
"Name": "something",
|
||||
"Email": "sa-1-something",
|
||||
"EmailVerified": false,
|
||||
"AuthID": "",
|
||||
"AuthenticatedBy": "apikey",
|
||||
"ApiKeyID": 0,
|
||||
"IsServiceAccount": true,
|
||||
"IsGrafanaAdmin": false,
|
||||
"IsAnonymous": false,
|
||||
"IsDisabled": false,
|
||||
"HelpFlags1": 0,
|
||||
"LastSeenAt": "2024-06-01T23:20:42-03:00",
|
||||
"Teams": [],
|
||||
"NamespacedID": {}
|
||||
}
|
22
pkg/util/testutil/data/user-viewer.json
Normal file
22
pkg/util/testutil/data/user-viewer.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"UserID": 3,
|
||||
"UserUID": "fdniuj44r0zr4d",
|
||||
"OrgID": 1,
|
||||
"OrgName": "Main Org.",
|
||||
"OrgRole": "Viewer",
|
||||
"Login": "viewer",
|
||||
"Name": "viewer",
|
||||
"Email": "viewer",
|
||||
"EmailVerified": false,
|
||||
"AuthID": "",
|
||||
"AuthenticatedBy": "",
|
||||
"ApiKeyID": 0,
|
||||
"IsServiceAccount": false,
|
||||
"IsGrafanaAdmin": false,
|
||||
"IsAnonymous": false,
|
||||
"IsDisabled": false,
|
||||
"HelpFlags1": 0,
|
||||
"LastSeenAt": "2024-06-01T23:09:25-03:00",
|
||||
"Teams": [],
|
||||
"NamespacedID": {}
|
||||
}
|
232
pkg/util/testutil/mocks/T.go
Normal file
232
pkg/util/testutil/mocks/T.go
Normal file
@ -0,0 +1,232 @@
|
||||
// Code generated by mockery v2.43.1. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
time "time"
|
||||
)
|
||||
|
||||
// T is an autogenerated mock type for the T type
|
||||
type T struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type T_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *T) EXPECT() *T_Expecter {
|
||||
return &T_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Cleanup provides a mock function with given fields: _a0
|
||||
func (_m *T) Cleanup(_a0 func()) {
|
||||
_m.Called(_a0)
|
||||
}
|
||||
|
||||
// T_Cleanup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Cleanup'
|
||||
type T_Cleanup_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Cleanup is a helper method to define mock.On call
|
||||
// - _a0 func()
|
||||
func (_e *T_Expecter) Cleanup(_a0 interface{}) *T_Cleanup_Call {
|
||||
return &T_Cleanup_Call{Call: _e.mock.On("Cleanup", _a0)}
|
||||
}
|
||||
|
||||
func (_c *T_Cleanup_Call) Run(run func(_a0 func())) *T_Cleanup_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(func()))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *T_Cleanup_Call) Return() *T_Cleanup_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *T_Cleanup_Call) RunAndReturn(run func(func())) *T_Cleanup_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Deadline provides a mock function with given fields:
|
||||
func (_m *T) Deadline() (time.Time, bool) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Deadline")
|
||||
}
|
||||
|
||||
var r0 time.Time
|
||||
var r1 bool
|
||||
if rf, ok := ret.Get(0).(func() (time.Time, bool)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() time.Time); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(time.Time)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() bool); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Get(1).(bool)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// T_Deadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Deadline'
|
||||
type T_Deadline_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Deadline is a helper method to define mock.On call
|
||||
func (_e *T_Expecter) Deadline() *T_Deadline_Call {
|
||||
return &T_Deadline_Call{Call: _e.mock.On("Deadline")}
|
||||
}
|
||||
|
||||
func (_c *T_Deadline_Call) Run(run func()) *T_Deadline_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *T_Deadline_Call) Return(_a0 time.Time, _a1 bool) *T_Deadline_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *T_Deadline_Call) RunAndReturn(run func() (time.Time, bool)) *T_Deadline_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Errorf provides a mock function with given fields: format, args
|
||||
func (_m *T) Errorf(format string, args ...interface{}) {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, format)
|
||||
_ca = append(_ca, args...)
|
||||
_m.Called(_ca...)
|
||||
}
|
||||
|
||||
// T_Errorf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Errorf'
|
||||
type T_Errorf_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Errorf is a helper method to define mock.On call
|
||||
// - format string
|
||||
// - args ...interface{}
|
||||
func (_e *T_Expecter) Errorf(format interface{}, args ...interface{}) *T_Errorf_Call {
|
||||
return &T_Errorf_Call{Call: _e.mock.On("Errorf",
|
||||
append([]interface{}{format}, args...)...)}
|
||||
}
|
||||
|
||||
func (_c *T_Errorf_Call) Run(run func(format string, args ...interface{})) *T_Errorf_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]interface{}, len(args)-1)
|
||||
for i, a := range args[1:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(interface{})
|
||||
}
|
||||
}
|
||||
run(args[0].(string), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *T_Errorf_Call) Return() *T_Errorf_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *T_Errorf_Call) RunAndReturn(run func(string, ...interface{})) *T_Errorf_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// FailNow provides a mock function with given fields:
|
||||
func (_m *T) FailNow() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// T_FailNow_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailNow'
|
||||
type T_FailNow_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// FailNow is a helper method to define mock.On call
|
||||
func (_e *T_Expecter) FailNow() *T_FailNow_Call {
|
||||
return &T_FailNow_Call{Call: _e.mock.On("FailNow")}
|
||||
}
|
||||
|
||||
func (_c *T_FailNow_Call) Run(run func()) *T_FailNow_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *T_FailNow_Call) Return() *T_FailNow_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *T_FailNow_Call) RunAndReturn(run func()) *T_FailNow_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Helper provides a mock function with given fields:
|
||||
func (_m *T) Helper() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// T_Helper_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Helper'
|
||||
type T_Helper_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Helper is a helper method to define mock.On call
|
||||
func (_e *T_Expecter) Helper() *T_Helper_Call {
|
||||
return &T_Helper_Call{Call: _e.mock.On("Helper")}
|
||||
}
|
||||
|
||||
func (_c *T_Helper_Call) Run(run func()) *T_Helper_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *T_Helper_Call) Return() *T_Helper_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *T_Helper_Call) RunAndReturn(run func()) *T_Helper_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewT creates a new instance of T. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewT(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *T {
|
||||
mock := &T{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
30
pkg/util/testutil/testutil.go
Normal file
30
pkg/util/testutil/testutil.go
Normal file
@ -0,0 +1,30 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed data/*
|
||||
var dataFS embed.FS
|
||||
|
||||
//go:generate mockery --with-expecter --name T
|
||||
|
||||
// T provides a clean way to test the utilities of this package.
|
||||
type T interface {
|
||||
Helper()
|
||||
Cleanup(func())
|
||||
Deadline() (time.Time, bool)
|
||||
Errorf(format string, args ...any)
|
||||
FailNow()
|
||||
}
|
||||
|
||||
func init() {
|
||||
// At the moment of this writing, there is already testing code imported in
|
||||
// server runtime code. Please, consider refactoring your code to keep
|
||||
// runtime dependencies clean.
|
||||
if !testing.Testing() {
|
||||
panic("importing testing libraries in runtime code is not allowed")
|
||||
}
|
||||
}
|
43
pkg/util/testutil/user.go
Normal file
43
pkg/util/testutil/user.go
Normal file
@ -0,0 +1,43 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
type SignedInUser struct{}
|
||||
|
||||
func (SignedInUser) NewAnonymous() (*user.SignedInUser, error) {
|
||||
return readUser(`user-anonymous.json`)
|
||||
}
|
||||
|
||||
func (SignedInUser) NewEditor() (*user.SignedInUser, error) {
|
||||
return readUser(`user-editor.json`)
|
||||
}
|
||||
|
||||
func (SignedInUser) NewGrafanaAdmin() (*user.SignedInUser, error) {
|
||||
return readUser(`user-grafana-admin.json`)
|
||||
}
|
||||
|
||||
func (SignedInUser) NewEmpty() (*user.SignedInUser, error) {
|
||||
return readUser(`user-empty.json`)
|
||||
}
|
||||
|
||||
func (SignedInUser) NewServiceAccount() (*user.SignedInUser, error) {
|
||||
return readUser(`user-service-account-viewer.json`)
|
||||
}
|
||||
|
||||
func (SignedInUser) NewViewer() (*user.SignedInUser, error) {
|
||||
return readUser(`user-viewer.json`)
|
||||
}
|
||||
|
||||
func readUser(filename string) (*user.SignedInUser, error) {
|
||||
file, err := dataFS.Open(`data/` + filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := new(user.SignedInUser)
|
||||
|
||||
return ret, json.NewDecoder(file).Decode(ret)
|
||||
}
|
29
pkg/util/testutil/user_test.go
Normal file
29
pkg/util/testutil/user_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
func TestSignedInUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := func(user *user.SignedInUser, err error) {
|
||||
require.NotNil(t, user)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
r(SignedInUser{}.NewAnonymous())
|
||||
r(SignedInUser{}.NewEditor())
|
||||
r(SignedInUser{}.NewGrafanaAdmin())
|
||||
r(SignedInUser{}.NewEmpty())
|
||||
r(SignedInUser{}.NewServiceAccount())
|
||||
r(SignedInUser{}.NewViewer())
|
||||
|
||||
user, err := readUser(`non existent!!!`)
|
||||
require.Nil(t, user)
|
||||
require.Error(t, err)
|
||||
}
|
Loading…
Reference in New Issue
Block a user