Preferences: Use inmemory structure for testing (#50491)

This commit is contained in:
Emil Tullstedt
2022-06-14 10:54:19 +02:00
committed by GitHub
parent 264c2a9d1e
commit 929fcd5910
2 changed files with 376 additions and 332 deletions

View File

@@ -0,0 +1,122 @@
package prefimpl
import (
"context"
"fmt"
"sort"
pref "github.com/grafana/grafana/pkg/services/preference"
)
// preferenceKey represents the primarily used index to access
// preferences.
//
// Preferences are typically either addressed by just OrgID;
// OrgID and UserID; or OrgID and UserID. Since all these IDs are one
// or more, using zero to represent "not relevant" replicates how the
// equivalent database index is structured.
type preferenceKey struct {
OrgID int64
TeamID int64
UserID int64
}
// inmemStore implements an implementation of store appropriate for
// testing without needing a full SQL database.
type inmemStore struct {
preference map[preferenceKey]pref.Preference
idMap map[int64]preferenceKey
nextID int64
}
func (s *inmemStore) Get(ctx context.Context, preference *pref.Preference) (*pref.Preference, error) {
res, ok := s.preference[preferenceKey{
OrgID: preference.OrgID,
TeamID: preference.TeamID,
UserID: preference.UserID,
}]
if !ok {
return nil, pref.ErrPrefNotFound
}
return &res, nil
}
// List returns, in order, preferences relevant to the Organization,
// Org+Teams, and Org+User from the preference argument. The order is
// important, since later elements will override earlier when used
// in GetWithDefaults.
//
// Global preferences are not stored in the storage, but rather in the
// settings.Cfg structure, and are not returned by List.
func (s *inmemStore) List(ctx context.Context, preference *pref.Preference) ([]*pref.Preference, error) {
res := []*pref.Preference{}
// Org
p, ok := s.preference[preferenceKey{
OrgID: preference.OrgID,
}]
if ok {
res = append(res, &p)
}
// Org + Teams (teams are numerically ordered)
sort.Slice(preference.Teams, func(i, j int) bool {
return preference.Teams[i] < preference.Teams[j]
})
for _, t := range preference.Teams {
p, ok := s.preference[preferenceKey{
OrgID: preference.OrgID,
TeamID: t,
}]
if !ok {
continue
}
res = append(res, &p)
}
// Org + UserID
if preference.UserID != 0 { // avoid adding the org preferences twice
p, ok := s.preference[preferenceKey{
OrgID: preference.OrgID,
UserID: preference.UserID,
}]
if ok {
res = append(res, &p)
}
}
return res, nil
}
func (s *inmemStore) Insert(ctx context.Context, preference *pref.Preference) (int64, error) {
key := preferenceKey{
OrgID: preference.OrgID,
TeamID: preference.TeamID,
UserID: preference.UserID,
}
var p = *preference
p.ID = s.nextID
s.nextID++
if _, exists := s.preference[key]; exists {
return 0, fmt.Errorf("conflict in fake, preference for [orgid=%d, userid=%d, teamid=%d] already exists", preference.OrgID, preference.UserID, preference.TeamID)
}
s.preference[key] = p
s.idMap[p.ID] = key
return p.ID, nil
}
func (s *inmemStore) Update(ctx context.Context, preference *pref.Preference) error {
key, ok := s.idMap[preference.ID]
if !ok {
return pref.ErrPrefNotFound
}
s.preference[key] = *preference
return nil
}

View File

@@ -5,18 +5,124 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func TestPreferencesService(t *testing.T) {
prefStoreFake := newPreferenceStoreFake()
func TestGet_empty(t *testing.T) {
prefService := &Service{
store: prefStoreFake,
store: newFake(),
cfg: setting.NewCfg(),
}
preference, err := prefService.Get(context.Background(), &pref.GetPreferenceQuery{})
require.NoError(t, err)
expected := &pref.Preference{}
if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
}
emptyNavbarPreferences := pref.NavbarPreference{}
func TestGetDefaults(t *testing.T) {
prefService := &Service{
store: newFake(),
cfg: setting.NewCfg(),
}
prefService.cfg.DefaultTheme = "light"
prefService.cfg.DateFormats.DefaultTimezone = "UTC"
t.Run("GetDefaults", func(t *testing.T) {
preference := prefService.GetDefaults()
expected := &pref.Preference{
Theme: "light",
Timezone: "UTC",
HomeDashboardID: 0,
JSONData: &pref.PreferenceJSONData{},
}
if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
t.Run("GetWithDefaults", func(t *testing.T) {
query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1}
preference, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
expected := &pref.Preference{
Theme: "light",
Timezone: "UTC",
HomeDashboardID: 0,
JSONData: &pref.PreferenceJSONData{},
}
if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
}
func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) {
prefService := &Service{
store: newFake(),
cfg: setting.NewCfg(),
}
insertPrefs(t, prefService.store,
pref.Preference{
OrgID: 1,
HomeDashboardID: 1,
Theme: "dark",
Timezone: "UTC",
WeekStart: "1",
},
pref.Preference{
OrgID: 1,
UserID: 1,
HomeDashboardID: 4,
Theme: "light",
Timezone: "browser",
WeekStart: "2",
},
)
t.Run("prefer user's preferences", func(t *testing.T) {
query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, UserID: 1}
preference, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
expected := &pref.Preference{
Theme: "light",
Timezone: "browser",
WeekStart: "2",
HomeDashboardID: 4,
JSONData: &pref.PreferenceJSONData{},
}
if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
t.Run("ignore other user's preferences", func(t *testing.T) {
prefService.GetDefaults().HomeDashboardID = 1
query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, UserID: 2}
preference, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
expected := &pref.Preference{
Theme: "dark",
Timezone: "UTC",
WeekStart: "1",
HomeDashboardID: 1,
JSONData: &pref.PreferenceJSONData{},
}
if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
}
func TestGetDefaults_JSONData(t *testing.T) {
queryPreference := pref.QueryHistoryPreference{
HomeTab: "hometab",
}
userNavbarPreferences := pref.NavbarPreference{
SavedItems: []pref.NavLink{{
ID: "explore",
@@ -45,20 +151,6 @@ func TestPreferencesService(t *testing.T) {
Url: "/home",
}},
}
emptyQueryPreference := pref.QueryHistoryPreference{}
queryPreference := pref.QueryHistoryPreference{
HomeTab: "hometab",
}
queryPreference2 := pref.QueryHistoryPreference{
HomeTab: "hometab",
}
emptyPreferencesJsonData := pref.PreferenceJSONData{
Navbar: emptyNavbarPreferences,
}
userPreferencesJsonData := pref.PreferenceJSONData{
Navbar: userNavbarPreferences,
QueryHistory: queryPreference,
@@ -73,247 +165,142 @@ func TestPreferencesService(t *testing.T) {
Navbar: team1NavbarPreferences,
}
t.Run("Get should return nothing", func(t *testing.T) {
prefStoreFake.ExpectedGetError = pref.ErrPrefNotFound
preference, err := prefService.Get(context.Background(), &pref.GetPreferenceQuery{})
require.NoError(t, err)
expected := &pref.Preference{}
if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
t.Run("users have precedence over org", func(t *testing.T) {
prefService := &Service{
store: newFake(),
cfg: setting.NewCfg(),
}
prefStoreFake.ExpectedError = nil
})
t.Run("GetDefaults should return defaults", func(t *testing.T) {
prefService.cfg = setting.NewCfg()
prefService.cfg.DefaultTheme = "light"
prefService.cfg.DateFormats.DefaultTimezone = "UTC"
preference := prefService.GetDefaults()
expected := &pref.Preference{
Theme: "light",
Timezone: "UTC",
HomeDashboardID: 0,
JSONData: &pref.PreferenceJSONData{},
}
if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
t.Run("GetDefaults with no saved preferences should return defaults", func(t *testing.T) {
prefStoreFake.ExpectedError = nil
prefStoreFake.ExpectedPreference = &pref.Preference{
Theme: "light",
Timezone: "UTC",
}
query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1}
preference, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
expected := &pref.Preference{
Theme: "light",
Timezone: "UTC",
HomeDashboardID: 0,
JSONData: &emptyPreferencesJsonData,
}
if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
t.Run("GetWithDefaults with saved org and user home dashboard should return user home dashboard", func(t *testing.T) {
prefStoreFake.ExpectedPreference = &pref.Preference{}
prefStoreFake.ExpectedListPreferences = []*pref.Preference{
{
OrgID: 1,
HomeDashboardID: 1,
Theme: "dark",
Timezone: "UTC",
},
{
OrgID: 1,
UserID: 1,
HomeDashboardID: 4,
Theme: "light",
WeekStart: "1",
},
}
query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, UserID: 1}
preference, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
expected := &pref.Preference{
Theme: "light",
Timezone: "UTC",
WeekStart: "1",
HomeDashboardID: 4,
JSONData: &pref.PreferenceJSONData{},
}
if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
t.Run("GetWithDefaults with saved org and other user home dashboard should return org home dashboard", func(t *testing.T) {
prefStoreFake.ExpectedPreference = &pref.Preference{}
prefStoreFake.ExpectedListPreferences = []*pref.Preference{
{
OrgID: 1,
HomeDashboardID: 1,
Theme: "dark",
Timezone: "UTC",
WeekStart: "1",
},
{
OrgID: 1,
UserID: 1,
HomeDashboardID: 4,
Theme: "light",
Timezone: "browser",
WeekStart: "2",
},
}
prefService.GetDefaults().HomeDashboardID = 1
query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, UserID: 2}
preference, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
expected := &pref.Preference{
Theme: "light",
Timezone: "browser",
WeekStart: "2",
HomeDashboardID: 4,
JSONData: &pref.PreferenceJSONData{},
}
if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
t.Run("GetPreferencesWithDefaults with saved org and user json data should return user json data", func(t *testing.T) {
prefStoreFake.ExpectedPreference = &pref.Preference{}
prefStoreFake.ExpectedListPreferences = []*pref.Preference{
{
insertPrefs(t, prefService.store,
pref.Preference{
OrgID: 1,
JSONData: &orgPreferencesJsonData,
},
{
pref.Preference{
OrgID: 1,
UserID: 1,
JSONData: &userPreferencesJsonData,
},
}
)
query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, UserID: 1}
preference, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
require.Equal(t, &pref.Preference{
Theme: "light",
JSONData: &userPreferencesJsonData,
Timezone: "UTC",
}, preference)
})
t.Run("GetWithDefaults with saved org and teams json data should return last team json data", func(t *testing.T) {
prefStoreFake.ExpectedPreference = &pref.Preference{}
prefStoreFake.ExpectedListPreferences = []*pref.Preference{
{
t.Run("teams have precedence over org and are read in ascending order", func(t *testing.T) {
prefService := &Service{
store: newFake(),
cfg: setting.NewCfg(),
}
insertPrefs(t, prefService.store,
pref.Preference{
OrgID: 1,
JSONData: &orgPreferencesJsonData,
},
{
pref.Preference{
OrgID: 1,
TeamID: 2,
JSONData: &team1PreferencesJsonData,
},
{
pref.Preference{
OrgID: 1,
TeamID: 3,
JSONData: &team2PreferencesJsonData,
},
}
)
query := &pref.GetPreferenceWithDefaultsQuery{
OrgID: 1, Teams: []int64{2, 3},
}
preference, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
require.Equal(t, &pref.Preference{
Timezone: "UTC",
Theme: "light",
JSONData: &team2PreferencesJsonData,
}, preference)
})
}
t.Run("GetWithDefaults with saved org and teams home dashboard should return last team home dashboard", func(t *testing.T) {
prefStoreFake.ExpectedPreference = &pref.Preference{
Theme: "dark",
Timezone: "UTC",
}
prefStoreFake.ExpectedListPreferences = []*pref.Preference{
{
OrgID: 1,
HomeDashboardID: 1,
Theme: "light",
Timezone: "browser",
WeekStart: "1",
},
{
OrgID: 1,
UserID: 1,
HomeDashboardID: 4,
Theme: "light",
Timezone: "browser",
WeekStart: "2",
},
}
query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, Teams: []int64{2, 3}}
preferences, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
expected := &pref.Preference{
func TestGetWithDefaults_teams(t *testing.T) {
prefService := &Service{
store: newFake(),
cfg: setting.NewCfg(),
}
insertPrefs(t, prefService.store,
pref.Preference{
OrgID: 1,
HomeDashboardID: 1,
Theme: "light",
Timezone: "browser",
WeekStart: "1",
},
pref.Preference{
OrgID: 1,
TeamID: 2,
HomeDashboardID: 3,
Theme: "light",
Timezone: "browser",
WeekStart: "2",
},
pref.Preference{
OrgID: 1,
TeamID: 3,
HomeDashboardID: 4,
JSONData: &pref.PreferenceJSONData{},
}
if diff := cmp.Diff(expected, preferences); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
t.Run("SavePreferences for a user should store correct values", func(t *testing.T) {
prefStoreFake.ExpectedPreference = &pref.Preference{
ID: 1,
OrgID: 1,
UserID: 3,
TeamID: 6,
HomeDashboardID: 5,
Theme: "light",
Timezone: "browser",
WeekStart: "1",
Theme: "dark",
}
err := prefService.Save(context.Background(),
&pref.SavePreferenceCommand{
Theme: "dark",
Timezone: "browser",
HomeDashboardID: 5,
WeekStart: "1"},
)
require.NoError(t, err)
})
t.Run("SavePreferences for a user should store correct values, when preference not found", func(t *testing.T) {
prefStoreFake.ExpectedGetError = pref.ErrPrefNotFound
prefStoreFake.ExpectedPreference = &pref.Preference{
ID: 1,
OrgID: 1,
UserID: 3,
TeamID: 6,
HomeDashboardID: 5,
Timezone: "browser",
WeekStart: "1",
Theme: "dark",
}
WeekStart: "2",
},
)
query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, Teams: []int64{2, 3}}
preferences, err := prefService.GetWithDefaults(context.Background(), query)
require.NoError(t, err)
expected := &pref.Preference{
Theme: "light",
Timezone: "browser",
WeekStart: "2",
HomeDashboardID: 4,
JSONData: &pref.PreferenceJSONData{},
}
if diff := cmp.Diff(expected, preferences); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
}
func TestPatch_toCreate(t *testing.T) {
prefService := &Service{
store: newFake(),
cfg: setting.NewCfg(),
}
themeValue := "light"
err := prefService.Patch(context.Background(), &pref.PatchPreferenceCommand{
OrgID: 1,
UserID: 2,
Theme: &themeValue,
})
require.NoError(t, err)
stored := prefService.store.(*inmemStore).preference[preferenceKey{OrgID: 1, UserID: 2}]
assert.EqualValues(t, 1, stored.OrgID)
assert.EqualValues(t, 2, stored.UserID)
assert.Equal(t, "light", stored.Theme)
}
func TestSave(t *testing.T) {
prefService := &Service{
store: newFake(),
cfg: setting.NewCfg(),
}
t.Run("insert", func(t *testing.T) {
err := prefService.Save(context.Background(),
&pref.SavePreferenceCommand{
OrgID: 1,
Theme: "dark",
Timezone: "browser",
HomeDashboardID: 5,
@@ -321,138 +308,73 @@ func TestPreferencesService(t *testing.T) {
},
)
require.NoError(t, err)
prefStoreFake.ExpectedGetError = nil
stored := prefService.store.(*inmemStore).preference[preferenceKey{OrgID: 1}]
assert.EqualValues(t, 1, stored.OrgID)
assert.Zero(t, stored.UserID)
assert.Zero(t, stored.TeamID)
assert.Equal(t, "dark", stored.Theme)
assert.Equal(t, "browser", stored.Timezone)
assert.EqualValues(t, 5, stored.HomeDashboardID)
assert.Equal(t, "1", stored.WeekStart)
assert.EqualValues(t, 0, stored.Version)
})
t.Run("SavePreferences for a user should store correct values with nav and query history", func(t *testing.T) {
prefStoreFake.ExpectedPreference = &pref.Preference{
ID: 1,
OrgID: 1,
UserID: 3,
TeamID: 6,
HomeDashboardID: 5,
Timezone: "browser",
WeekStart: "1",
Theme: "dark",
JSONData: &userPreferencesJsonData,
}
t.Run("update", func(t *testing.T) {
err := prefService.Save(context.Background(),
&pref.SavePreferenceCommand{
Theme: "dark",
Timezone: "browser",
HomeDashboardID: 5,
OrgID: 1,
Timezone: "UTC",
HomeDashboardID: 0,
WeekStart: "1",
Navbar: &userNavbarPreferences,
QueryHistory: &emptyQueryPreference,
},
)
require.NoError(t, err)
stored := prefService.store.(*inmemStore).preference[preferenceKey{OrgID: 1}]
assert.EqualValues(t, 1, stored.OrgID)
assert.Zero(t, stored.UserID)
assert.Zero(t, stored.TeamID)
assert.Empty(t, stored.Theme)
assert.Equal(t, "UTC", stored.Timezone)
assert.Zero(t, stored.HomeDashboardID)
assert.Equal(t, "1", stored.WeekStart)
assert.EqualValues(t, 1, stored.Version)
})
t.Run("Get for a user should store correct values", func(t *testing.T) {
prefStoreFake.ExpectedPreference = &pref.Preference{
HomeDashboardID: 5,
Timezone: "browser",
WeekStart: "1",
Theme: "dark",
}
preference, err := prefService.Get(context.Background(), &pref.GetPreferenceQuery{})
t.Run("patch", func(t *testing.T) {
themeValue := "light"
err := prefService.Patch(context.Background(), &pref.PatchPreferenceCommand{
OrgID: 1,
Theme: &themeValue,
})
require.NoError(t, err)
expected := &pref.Preference{
ID: preference.ID,
Version: preference.Version,
HomeDashboardID: 5,
Timezone: "browser",
WeekStart: "1",
Theme: "dark",
Created: preference.Created,
Updated: preference.Updated,
}
if diff := cmp.Diff(expected, preference); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
stored := prefService.store.(*inmemStore).preference[preferenceKey{OrgID: 1}]
assert.EqualValues(t, 1, stored.OrgID)
assert.Zero(t, stored.UserID)
assert.Zero(t, stored.TeamID)
assert.Equal(t, themeValue, stored.Theme)
assert.Equal(t, "UTC", stored.Timezone)
assert.Zero(t, stored.HomeDashboardID)
assert.Equal(t, "1", stored.WeekStart)
assert.EqualValues(t, 2, stored.Version)
})
}
t.Run("Patch for a user should store correct values", func(t *testing.T) {
darkTheme := "dark"
prefStoreFake.ExpectedPreference = &pref.Preference{
HomeDashboardID: 5,
Timezone: "browser",
WeekStart: "1",
Theme: "dark",
JSONData: &userPreferencesJsonData,
}
err := prefService.Patch(context.Background(),
&pref.PatchPreferenceCommand{
Theme: &darkTheme,
Navbar: &userNavbarPreferences,
QueryHistory: &queryPreference2,
})
func insertPrefs(t testing.TB, store store, preferences ...pref.Preference) {
t.Helper()
for _, p := range preferences {
p := p
_, err := store.Insert(context.Background(), &p)
require.NoError(t, err)
})
t.Run("Patch for a user should store correct values, without navbar and query history", func(t *testing.T) {
darkTheme := "dark"
prefStoreFake.ExpectedPreference = &pref.Preference{
HomeDashboardID: 5,
Timezone: "browser",
WeekStart: "1",
Theme: "dark",
}
err := prefService.Patch(context.Background(),
&pref.PatchPreferenceCommand{
Theme: &darkTheme,
Navbar: &userNavbarPreferences,
QueryHistory: &queryPreference2,
})
require.NoError(t, err)
})
t.Run("Patch for a user should store correct values, when preference not found", func(t *testing.T) {
timezone := "browser"
weekStart := "1"
homeDashboardID := int64(5)
prefStoreFake.ExpectedGetError = pref.ErrPrefNotFound
prefStoreFake.ExpectedPreference = nil
err := prefService.Patch(context.Background(),
&pref.PatchPreferenceCommand{
HomeDashboardID: &homeDashboardID,
Timezone: &timezone,
WeekStart: &weekStart,
Navbar: &emptyNavbarPreferences,
QueryHistory: &emptyQueryPreference,
})
require.NoError(t, err)
prefStoreFake.ExpectedGetError = nil
})
}
}
type FakePreferenceStore struct {
ExpectedPreference *pref.Preference
ExpectedListPreferences []*pref.Preference
ExpectedID int64
ExpectedError error
ExpectedGetError error
}
func newPreferenceStoreFake() *FakePreferenceStore {
return &FakePreferenceStore{}
}
func (f *FakePreferenceStore) List(ctx context.Context, query *pref.Preference) ([]*pref.Preference, error) {
return f.ExpectedListPreferences, f.ExpectedError
}
func (f *FakePreferenceStore) Get(ctx context.Context, query *pref.Preference) (*pref.Preference, error) {
return f.ExpectedPreference, f.ExpectedGetError
}
func (f *FakePreferenceStore) Insert(ctx context.Context, cmd *pref.Preference) (int64, error) {
return f.ExpectedID, f.ExpectedError
}
func (f *FakePreferenceStore) Update(ctx context.Context, cmd *pref.Preference) error {
return f.ExpectedError
func newFake() store {
return &inmemStore{
preference: map[preferenceKey]pref.Preference{},
idMap: map[int64]preferenceKey{},
nextID: 1,
}
}