fleet/server/datastore/cached_mysql/cached_mysql_test.go
Roberto Dip 545e56d288
19016 ingest certs on start (#19360)
For #19016

This changes all the places where we previously assumed that certs were
hardcoded when the Fleet server started to query the database instead.

The plan is to loadtest afterwards, but as a first preemptive measure,
this adds a caching layer on top the mysql datastore.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [x] Manual QA for all new/changed functionality
2024-05-30 18:18:42 -03:00

848 lines
26 KiB
Go

package cached_mysql
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/stretchr/testify/require"
)
type nilCloner struct{}
func (n *nilCloner) Clone() (fleet.Cloner, error) {
var nn *nilCloner
return nn, nil
}
func TestClone(t *testing.T) {
tests := []struct {
name string
src fleet.Cloner
want fleet.Cloner
}{
{
name: "appconfig",
src: &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
EnableAnalytics: true,
},
},
want: &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
EnableAnalytics: true,
},
},
},
{
name: "nil",
src: (*nilCloner)(nil),
want: (*nilCloner)(nil),
},
{
name: "appconfig with nested slice",
src: &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
DebugHostIDs: []uint{1, 2, 3},
},
},
want: &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
DebugHostIDs: []uint{1, 2, 3},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
clone, err := tc.src.Clone()
require.NoError(t, err)
require.Equal(t, tc.want, clone)
// ensure that writing to src does not alter the cloned value (i.e. that
// the nested fields are deeply cloned too).
switch src := tc.src.(type) {
case *fleet.AppConfig:
if len(src.ServerSettings.DebugHostIDs) > 0 {
src.ServerSettings.DebugHostIDs[0] = 999
require.NotEqual(t, src.ServerSettings.DebugHostIDs, clone.(*fleet.AppConfig).ServerSettings.DebugHostIDs)
}
}
})
}
}
func TestCachedAppConfig(t *testing.T) {
t.Parallel()
mockedDS := new(mock.Store)
ds := New(mockedDS)
var appConfigSet *fleet.AppConfig
mockedDS.NewAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) (*fleet.AppConfig, error) {
appConfigSet = info
return info, nil
}
mockedDS.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return appConfigSet, nil
}
mockedDS.SaveAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) error {
appConfigSet = info
return nil
}
_, err := ds.NewAppConfig(context.Background(), &fleet.AppConfig{
Features: fleet.Features{
AdditionalQueries: ptr.RawMessage(json.RawMessage(`"TestCachedAppConfig"`)),
},
})
require.NoError(t, err)
t.Run("NewAppConfig", func(t *testing.T) {
data, err := ds.AppConfig(context.Background())
require.NoError(t, err)
require.NotEmpty(t, data)
require.Equal(t, json.RawMessage(`"TestCachedAppConfig"`), *data.Features.AdditionalQueries)
})
t.Run("AppConfig", func(t *testing.T) {
require.False(t, mockedDS.AppConfigFuncInvoked)
ac, err := ds.AppConfig(context.Background())
require.NoError(t, err)
require.False(t, mockedDS.AppConfigFuncInvoked)
require.Equal(t, ptr.RawMessage(json.RawMessage(`"TestCachedAppConfig"`)), ac.Features.AdditionalQueries)
})
t.Run("SaveAppConfig", func(t *testing.T) {
require.NoError(t, ds.SaveAppConfig(context.Background(), &fleet.AppConfig{
Features: fleet.Features{
AdditionalQueries: ptr.RawMessage(json.RawMessage(`"NewSAVED"`)),
},
}))
require.True(t, mockedDS.SaveAppConfigFuncInvoked)
ac, err := ds.AppConfig(context.Background())
require.NoError(t, err)
require.NotNil(t, ac.Features.AdditionalQueries)
require.Equal(t, json.RawMessage(`"NewSAVED"`), *ac.Features.AdditionalQueries)
})
t.Run("External SaveAppConfig gets caught", func(t *testing.T) {
mockedDS.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
Features: fleet.Features{
AdditionalQueries: ptr.RawMessage(json.RawMessage(`"SavedSomewhereElse"`)),
},
}, nil
}
time.Sleep(2 * time.Second)
ac, err := ds.AppConfig(context.Background())
require.NoError(t, err)
require.NotNil(t, ac.Features.AdditionalQueries)
require.Equal(t, json.RawMessage(`"SavedSomewhereElse"`), *ac.Features.AdditionalQueries)
})
}
func TestBypassAppConfig(t *testing.T) {
t.Parallel()
mockedDS := new(mock.Store)
ds := New(mockedDS, WithAppConfigExpiration(time.Minute))
var appConfigSet *fleet.AppConfig
mockedDS.NewAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) (*fleet.AppConfig, error) {
appConfigSet = info
return info, nil
}
mockedDS.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return appConfigSet, nil
}
mockedDS.SaveAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) error {
appConfigSet = info
return nil
}
// calling NewAppConfig initializes the cache
_, err := ds.NewAppConfig(context.Background(), &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{
OrgName: "A",
},
})
require.NoError(t, err)
ctx := context.Background()
// used the cached value
ac, err := ds.AppConfig(ctx)
require.NoError(t, err)
require.Equal(t, "A", ac.OrgInfo.OrgName)
require.False(t, mockedDS.AppConfigFuncInvoked)
// update and save it, calls the DB
ac.OrgInfo.OrgName = "B"
err = ds.SaveAppConfig(ctx, ac)
require.NoError(t, err)
require.True(t, mockedDS.SaveAppConfigFuncInvoked)
mockedDS.SaveAppConfigFuncInvoked = false
// read it back, uses the cache
ac, err = ds.AppConfig(ctx)
require.NoError(t, err)
require.Equal(t, "B", ac.OrgInfo.OrgName)
require.False(t, mockedDS.AppConfigFuncInvoked)
// simulate a database change from another process, not via the cached_mysql store
ac.OrgInfo.OrgName = "C"
err = mockedDS.SaveAppConfig(ctx, ac)
require.NoError(t, err)
// reading it via the store uses the old cached value
ac, err = ds.AppConfig(ctx)
require.NoError(t, err)
require.Equal(t, "B", ac.OrgInfo.OrgName)
require.False(t, mockedDS.AppConfigFuncInvoked)
// force-bypassing the cache gets the updated value
ctx = ctxdb.BypassCachedMysql(ctx, true)
ac, err = ds.AppConfig(ctx)
require.NoError(t, err)
require.Equal(t, "C", ac.OrgInfo.OrgName)
require.True(t, mockedDS.AppConfigFuncInvoked)
mockedDS.AppConfigFuncInvoked = false
// bypassing the cache to read AppConfig did update the cache, so if we don't
// bypass it anymore, it now gets the updated value from the cache
ctx = ctxdb.BypassCachedMysql(ctx, false)
ac, err = ds.AppConfig(ctx)
require.NoError(t, err)
require.Equal(t, "C", ac.OrgInfo.OrgName)
require.False(t, mockedDS.AppConfigFuncInvoked)
}
func TestCachedPacksforHost(t *testing.T) {
t.Parallel()
mockedDS := new(mock.Store)
ds := New(mockedDS, WithPacksExpiration(100*time.Millisecond))
dbPacks := []*fleet.Pack{
{
ID: 1,
Name: "test-pack-1",
},
{
ID: 2,
Name: "test-pack-2",
},
}
called := 0
mockedDS.ListPacksForHostFunc = func(ctx context.Context, hid uint) (packs []*fleet.Pack, err error) {
called++
return dbPacks, nil
}
// first call gets the result from the DB
packs, err := ds.ListPacksForHost(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, dbPacks, packs)
require.Same(t, dbPacks[0], packs[0])
require.Equal(t, 1, called)
// change "stored" dbPacks.
dbPacks = []*fleet.Pack{
{
ID: 1,
Name: "test-pack-1",
},
{
ID: 3,
Name: "test-pack-3",
},
}
// this call gets it from the cache
packs2, err := ds.ListPacksForHost(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, packs, packs2) // returns the old cached value
require.NotSame(t, packs[0], packs2[0]) // have been cloned
require.Equal(t, 1, called)
time.Sleep(200 * time.Millisecond)
// this call gets it from the DB again since the cache expired
packs3, err := ds.ListPacksForHost(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, dbPacks, packs3)
require.Same(t, dbPacks[0], packs3[0])
require.Equal(t, 2, called)
}
func TestCachedListScheduledQueriesInPack(t *testing.T) {
t.Parallel()
mockedDS := new(mock.Store)
ds := New(mockedDS, WithScheduledQueriesExpiration(100*time.Millisecond))
dbScheduledQueries := fleet.ScheduledQueryList{
{
ID: 1,
Name: "test-schedule-1",
},
{
ID: 2,
Name: "test-schedule-2",
},
}
called := 0
mockedDS.ListScheduledQueriesInPackFunc = func(ctx context.Context, packID uint) (fleet.ScheduledQueryList, error) {
called++
return dbScheduledQueries, nil
}
// this initial call gets the result from the DB
scheduledQueries, err := ds.ListScheduledQueriesInPack(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, dbScheduledQueries, scheduledQueries)
require.Same(t, dbScheduledQueries[0], scheduledQueries[0])
require.Equal(t, 1, called)
// change "stored" dbScheduledQueries.
dbScheduledQueries = fleet.ScheduledQueryList{
{
ID: 3,
Name: "test-schedule-3",
},
}
// this call gets it from the cache
scheduledQueries2, err := ds.ListScheduledQueriesInPack(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, scheduledQueries2, scheduledQueries)
require.NotSame(t, scheduledQueries[0], scheduledQueries2[0]) // has been cloned
require.Equal(t, 1, called)
time.Sleep(200 * time.Millisecond)
// this call gets it from the DB again, since the cache expired
scheduledQueries3, err := ds.ListScheduledQueriesInPack(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, dbScheduledQueries, scheduledQueries3)
require.Same(t, dbScheduledQueries[0], scheduledQueries3[0])
require.Equal(t, 2, called)
}
func TestCachedTeamAgentOptions(t *testing.T) {
t.Parallel()
mockedDS := new(mock.Store)
ds := New(mockedDS, WithTeamAgentOptionsExpiration(100*time.Millisecond))
testOptions := json.RawMessage(`
{
"config": {
"options": {
"logger_plugin": "tls",
"pack_delimiter": "/",
"logger_tls_period": 10,
"distributed_plugin": "tls",
"disable_distributed": false,
"logger_tls_endpoint": "/api/osquery/log",
"distributed_interval": 10,
"distributed_tls_max_attempts": 3
},
"decorators": {
"load": [
"SELECT uuid AS host_uuid FROM system_info;",
"SELECT hostname AS hostname FROM system_info;"
]
}
},
"overrides": {}
}
`)
testTeam := &fleet.Team{
ID: 1,
CreatedAt: time.Now(),
Name: "test",
Config: fleet.TeamConfig{
AgentOptions: &testOptions,
},
}
deleted := false
mockedDS.TeamAgentOptionsFunc = func(ctx context.Context, teamID uint) (*json.RawMessage, error) {
if deleted {
return nil, errors.New("not found")
}
return &testOptions, nil
}
mockedDS.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
return team, nil
}
mockedDS.DeleteTeamFunc = func(ctx context.Context, teamID uint) error {
deleted = true
return nil
}
// initial call reads from the DB
options, err := ds.TeamAgentOptions(context.Background(), 1)
require.NoError(t, err)
require.JSONEq(t, string(testOptions), string(*options))
require.Same(t, &testOptions, options)
require.True(t, mockedDS.TeamAgentOptionsFuncInvoked)
mockedDS.TeamAgentOptionsFuncInvoked = false
// subsequent call reads from the cache
options, err = ds.TeamAgentOptions(context.Background(), 1)
require.NoError(t, err)
require.JSONEq(t, string(testOptions), string(*options))
require.NotSame(t, &testOptions, options)
require.False(t, mockedDS.TeamAgentOptionsFuncInvoked)
// saving a team updates agent options in cache
updateOptions := json.RawMessage(`
{}
`)
updateTeam := &fleet.Team{
ID: testTeam.ID,
CreatedAt: testTeam.CreatedAt,
Name: testTeam.Name,
Config: fleet.TeamConfig{
AgentOptions: &updateOptions,
},
}
_, err = ds.SaveTeam(context.Background(), updateTeam)
require.NoError(t, err)
require.True(t, mockedDS.SaveTeamFuncInvoked)
mockedDS.SaveTeamFuncInvoked = false
// reading reads it from the cache with the updated data
options, err = ds.TeamAgentOptions(context.Background(), testTeam.ID)
require.NoError(t, err)
require.JSONEq(t, string(updateOptions), string(*options))
require.NotSame(t, &updateOptions, options)
require.False(t, mockedDS.TeamAgentOptionsFuncInvoked)
// deleting a team removes the agent options from the cache
err = ds.DeleteTeam(context.Background(), testTeam.ID)
require.NoError(t, err)
require.True(t, mockedDS.DeleteTeamFuncInvoked)
mockedDS.DeleteTeamFuncInvoked = false
// reading hits the DB as the cached item was removed
_, err = ds.TeamAgentOptions(context.Background(), testTeam.ID)
require.Error(t, err)
require.True(t, mockedDS.TeamAgentOptionsFuncInvoked)
}
func TestCachedTeamFeatures(t *testing.T) {
t.Parallel()
mockedDS := new(mock.Store)
ds := New(mockedDS, WithTeamFeaturesExpiration(100*time.Millisecond))
ao := json.RawMessage(`{}`)
aq := json.RawMessage(`{"foo": "bar"}`)
testFeatures := fleet.Features{
EnableHostUsers: false,
EnableSoftwareInventory: true,
AdditionalQueries: &aq,
DetailQueryOverrides: map[string]*string{"a": ptr.String("A"), "b": ptr.String("B")},
}
testTeam := fleet.Team{
ID: 1,
CreatedAt: time.Now(),
Name: "test",
Config: fleet.TeamConfig{
Features: testFeatures,
AgentOptions: &ao,
},
}
deleted := false
mockedDS.TeamFeaturesFunc = func(ctx context.Context, teamID uint) (*fleet.Features, error) {
if deleted {
return nil, errors.New("not found")
}
return &testFeatures, nil
}
mockedDS.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
return team, nil
}
mockedDS.DeleteTeamFunc = func(ctx context.Context, teamID uint) error {
deleted = true
return nil
}
// get it the first time, it will populate the cache
features, err := ds.TeamFeatures(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, testFeatures, *features)
require.Same(t, &testFeatures, features)
require.True(t, mockedDS.TeamFeaturesFuncInvoked)
mockedDS.TeamFeaturesFuncInvoked = false
// get it again, will retrieve it from the cache
features, err = ds.TeamFeatures(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, testFeatures, *features)
require.NotSame(t, &testFeatures, features)
require.False(t, mockedDS.TeamFeaturesFuncInvoked)
// changing e.g. the DetailQueryOverrides map doesn't affect the stored value
ptrA := features.DetailQueryOverrides["a"]
*ptrA = "AAA"
features.DetailQueryOverrides["c"] = ptr.String("C")
require.NotEqual(t, testFeatures.DetailQueryOverrides, features.DetailQueryOverrides)
// saving a team updates features in cache
aq = json.RawMessage(`{"bar": "baz"}`)
updateFeatures := fleet.Features{
EnableHostUsers: true,
EnableSoftwareInventory: false,
AdditionalQueries: &aq,
DetailQueryOverrides: map[string]*string{"c": ptr.String("C")},
}
updateTeam := &fleet.Team{
ID: testTeam.ID,
CreatedAt: testTeam.CreatedAt,
Name: testTeam.Name,
Config: fleet.TeamConfig{
Features: updateFeatures,
AgentOptions: &ao,
},
}
_, err = ds.SaveTeam(context.Background(), updateTeam)
require.NoError(t, err)
require.True(t, mockedDS.SaveTeamFuncInvoked)
mockedDS.SaveTeamFuncInvoked = false
// this call gets it from the cache and gets the updated value set by SaveTeam
features, err = ds.TeamFeatures(context.Background(), testTeam.ID)
require.NoError(t, err)
require.Equal(t, updateFeatures, *features)
require.NotSame(t, &updateFeatures, features)
require.False(t, mockedDS.TeamFeaturesFuncInvoked)
// deleting a team removes the features from the cache
err = ds.DeleteTeam(context.Background(), testTeam.ID)
require.NoError(t, err)
// reading hits the DB as the cached item was removed
_, err = ds.TeamFeatures(context.Background(), testTeam.ID)
require.Error(t, err)
require.True(t, mockedDS.TeamFeaturesFuncInvoked)
}
func TestCachedTeamMDMConfig(t *testing.T) {
t.Parallel()
mockedDS := new(mock.Store)
ds := New(mockedDS, WithTeamMDMConfigExpiration(100*time.Millisecond))
ao := json.RawMessage(`{}`)
testMDMConfig := fleet.TeamMDM{
EnableDiskEncryption: true,
MacOSUpdates: fleet.MacOSUpdates{
MinimumVersion: optjson.SetString("10.10.10"),
Deadline: optjson.SetString("1992-03-01"),
},
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []fleet.MDMProfileSpec{{Path: "a"}, {Path: "b"}},
DeprecatedEnableDiskEncryption: ptr.Bool(false),
},
MacOSSetup: fleet.MacOSSetup{
BootstrapPackage: optjson.SetString("bootstrap"),
},
}
testTeam := fleet.Team{
ID: 1,
CreatedAt: time.Now(),
Name: "test",
Config: fleet.TeamConfig{
MDM: testMDMConfig,
AgentOptions: &ao,
},
}
deleted := false
mockedDS.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) {
if deleted {
return nil, errors.New("not found")
}
return &testMDMConfig, nil
}
mockedDS.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
return team, nil
}
mockedDS.DeleteTeamFunc = func(ctx context.Context, teamID uint) error {
deleted = true
return nil
}
// get the team's config, will load it into cache
mdmConfig, err := ds.TeamMDMConfig(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, testMDMConfig, *mdmConfig)
require.Same(t, &testMDMConfig, mdmConfig)
require.True(t, mockedDS.TeamMDMConfigFuncInvoked)
mockedDS.TeamMDMConfigFuncInvoked = false
// get it again, will get it from cache
mdmConfig, err = ds.TeamMDMConfig(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, testMDMConfig, *mdmConfig)
require.NotSame(t, &testMDMConfig, mdmConfig)
require.False(t, mockedDS.TeamMDMConfigFuncInvoked)
// changing some deep value doesn't affect the stored value
mdmConfig.MacOSSettings.CustomSettings[0] = fleet.MDMProfileSpec{Path: "c"}
require.NotEqual(t, testMDMConfig, *mdmConfig)
// saving a team updates config in cache
updateMDMConfig := fleet.TeamMDM{
MacOSUpdates: fleet.MacOSUpdates{
MinimumVersion: optjson.SetString("13.13.13"),
Deadline: optjson.SetString("2022-03-01"),
},
MacOSSettings: fleet.MacOSSettings{
CustomSettings: nil,
DeprecatedEnableDiskEncryption: ptr.Bool(true),
},
}
updateTeam := &fleet.Team{
ID: testTeam.ID,
CreatedAt: testTeam.CreatedAt,
Name: testTeam.Name,
Config: fleet.TeamConfig{
MDM: updateMDMConfig,
AgentOptions: &ao,
},
}
_, err = ds.SaveTeam(context.Background(), updateTeam)
require.NoError(t, err)
require.True(t, mockedDS.SaveTeamFuncInvoked)
mockedDS.SaveTeamFuncInvoked = false
// this call gets it from the cache, with the updated value set by SaveTeam
mdmConfig, err = ds.TeamMDMConfig(context.Background(), testTeam.ID)
require.NoError(t, err)
require.Equal(t, updateMDMConfig, *mdmConfig)
require.NotSame(t, &updateMDMConfig, mdmConfig)
require.False(t, mockedDS.TeamMDMConfigFuncInvoked)
// deleting a team removes the config from the cache
err = ds.DeleteTeam(context.Background(), testTeam.ID)
require.NoError(t, err)
// reading hits the DB as the cached item was removed
_, err = ds.TeamMDMConfig(context.Background(), testTeam.ID)
require.Error(t, err)
require.True(t, mockedDS.TeamMDMConfigFuncInvoked)
}
func TestCachedQueryByName(t *testing.T) {
t.Parallel()
mockedDS := new(mock.Store)
ds := New(mockedDS, WithQueryByNameExpiration(100*time.Millisecond))
testQuery := &fleet.Query{
ID: 1,
TeamID: ptr.Uint(1),
Packs: []fleet.Pack{{ID: 2, Name: "a"}},
}
mockedDS.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
return testQuery, nil
}
// first call gets the result from the DB
query1, err := ds.QueryByName(context.Background(), nil, "q")
require.NoError(t, err)
require.Equal(t, testQuery, query1)
require.Same(t, testQuery, query1)
require.True(t, mockedDS.QueryByNameFuncInvoked)
mockedDS.QueryByNameFuncInvoked = false
// change "stored" query.
testQuery = &fleet.Query{
ID: 1,
TeamID: ptr.Uint(2),
Packs: []fleet.Pack{{ID: 3, Name: "b"}},
}
// this call gets it from the cache
query2, err := ds.QueryByName(context.Background(), nil, "q")
require.NoError(t, err)
require.Equal(t, query1, query2) // returns the old cached value
require.NotSame(t, query1, query2) // have been cloned
require.False(t, mockedDS.QueryByNameFuncInvoked)
// a deep change doesn't alter the stored value
query2.Packs[0].Name = "Z"
require.NotEqual(t, query1, query2)
time.Sleep(200 * time.Millisecond)
// this call gets it from the DB again since the cache expired
query3, err := ds.QueryByName(context.Background(), nil, "q")
require.NoError(t, err)
require.Equal(t, testQuery, query3)
require.Same(t, testQuery, query3)
require.True(t, mockedDS.QueryByNameFuncInvoked)
}
func TestCachedResultCountForQuery(t *testing.T) {
t.Parallel()
mockedDS := new(mock.Store)
ds := New(mockedDS, WithQueryResultsCountExpiration(100*time.Millisecond))
testCount := 1
mockedDS.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
return testCount, nil
}
// first call gets the result from the DB
c1, err := ds.ResultCountForQuery(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, testCount, c1)
require.True(t, mockedDS.ResultCountForQueryFuncInvoked)
mockedDS.ResultCountForQueryFuncInvoked = false
// change "stored" count.
testCount = 2
// this call gets it from the cache
c2, err := ds.ResultCountForQuery(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, c1, c2) // returns the old cached value
require.False(t, mockedDS.ResultCountForQueryFuncInvoked)
time.Sleep(200 * time.Millisecond)
// this call gets it from the DB again since the cache expired
c3, err := ds.ResultCountForQuery(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, testCount, c3)
require.True(t, mockedDS.ResultCountForQueryFuncInvoked)
}
func TestGetAllMDMConfigAssetsByName(t *testing.T) {
t.Parallel()
mockedDS := new(mock.Store)
ds := New(mockedDS)
assetNames := []fleet.MDMAssetName{"asset1", "asset2", "asset3"}
assetMap := map[fleet.MDMAssetName]fleet.MDMConfigAsset{
"asset1": {Name: "asset1", Value: []byte("value1")},
"asset2": {Name: "asset2", Value: []byte("value2")},
"asset3": {Name: "asset2", Value: []byte("value3")},
}
assetHashes := map[fleet.MDMAssetName]string{
"asset1": "hash1",
"asset2": "hash2",
"asset3": "hash3",
}
mockedDS.GetAllMDMConfigAssetsHashesFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]string, error) {
result := map[fleet.MDMAssetName]string{}
for _, n := range assetNames {
result[n] = assetHashes[n]
}
return result, nil
}
mockedDS.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
result := map[fleet.MDMAssetName]fleet.MDMConfigAsset{}
for _, n := range assetNames {
result[n] = assetMap[n]
}
return result, nil
}
// returns cached assets if hashes match
result, err := ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"asset1", "asset2"})
require.NoError(t, err)
require.True(t, mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked)
require.True(t, mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked)
mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked = false
mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked = false
require.Equal(t, assetMap["asset1"], result["asset1"])
require.Equal(t, assetMap["asset2"], result["asset2"])
require.NotContains(t, result, "asset3")
result, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"asset1", "asset2"})
require.NoError(t, err)
require.False(t, mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked)
require.True(t, mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked)
mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked = false
mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked = false
require.Equal(t, assetMap["asset1"], result["asset1"])
require.Equal(t, assetMap["asset2"], result["asset2"])
require.NotContains(t, result, "asset3")
mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked = false
mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked = false
// fetches missing assets from the db
result, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"asset1", "asset2", "asset3"})
require.NoError(t, err)
require.Equal(t, assetMap["asset1"], result["asset1"])
require.Equal(t, assetMap["asset2"], result["asset2"])
require.Equal(t, assetMap["asset3"], result["asset3"])
require.True(t, mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked)
require.True(t, mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked)
mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked = false
mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked = false
// fetches updated assets from the db
assetHashes["asset1"] = "newhash"
assetMap["asset1"] = fleet.MDMConfigAsset{Name: "asset1", Value: []byte("newvalue")}
result, err = ds.GetAllMDMConfigAssetsByName(context.Background(), assetNames)
require.NoError(t, err)
require.Equal(t, assetMap["asset1"], result["asset1"])
require.Equal(t, assetMap["asset2"], result["asset2"])
require.Equal(t, assetMap["asset3"], result["asset3"])
require.True(t, mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked)
require.True(t, mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked)
mockedDS.GetAllMDMConfigAssetsByNameFuncInvoked = false
mockedDS.GetAllMDMConfigAssetsHashesFuncInvoked = false
// passes errors fetching assets from downstream
mockedDS.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return nil, errors.New("error fetching assets")
}
_, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"not exists"})
require.Error(t, err)
require.Equal(t, "error fetching assets", err.Error())
// passes errors fetching hashes from downstream
mockedDS.GetAllMDMConfigAssetsHashesFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]string, error) {
return nil, errors.New("error fetching hashes")
}
_, err = ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{"not exists"})
require.Error(t, err)
require.Equal(t, "error fetching hashes", err.Error())
}