Unified Storage: Testing For Fix Create, Update and Delete wrt Resource Versions (#88568)

Add testing harness
This commit is contained in:
Diego Augusto Molina
2024-06-05 15:18:33 -03:00
committed by GitHub
parent 6fcd7d9e03
commit 5fc580b401
66 changed files with 4416 additions and 322 deletions
+113
View 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
View 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)
})
}
@@ -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
View 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
View 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": {}
}
@@ -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": {}
}
@@ -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
View 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
View 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
View 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
View 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
View 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)
}