fleet/cmd/fleetctl/fleetctl/query_test.go
Victor Lyuboslavsky 33396a5d91
Moved some integration tests into their own package. (#28978)
For #27927 

Refactoring to speed up fleetctl tests, no functional changes. Mostly
changing test files.

fleetctl is no longer the long pole in CI, the long pole is mysql,
followed by vuln.

<img width="389" alt="image"
src="https://github.com/user-attachments/assets/9ada64e2-b5e8-42e3-b120-4eb36183ae38"
/>
2025-05-09 09:26:57 -05:00

316 lines
10 KiB
Go

package fleetctl
import (
"context"
"os"
"sync"
"testing"
"time"
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
"github.com/fleetdm/fleet/v4/server/pubsub"
"github.com/fleetdm/fleet/v4/server/service"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSavedLiveQuery(t *testing.T) {
rs := pubsub.NewInmemQueryResults()
lq := live_query_mock.New(t)
logger := kitlog.NewJSONLogger(os.Stdout)
logger = level.NewFilter(logger, level.AllowDebug())
_, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{
Rs: rs,
Lq: lq,
Logger: logger,
})
users, err := ds.ListUsersFunc(context.Background(), fleet.UserListOptions{})
require.NoError(t, err)
var admin *fleet.User
for _, user := range users {
if user.GlobalRole != nil && *user.GlobalRole == fleet.RoleAdmin {
admin = user
}
}
const queryName = "saved-query"
const queryString = "select 42, * from time"
query := fleet.Query{
ID: 42,
Name: queryName,
Query: queryString,
Saved: true,
}
ds.HostIDsByIdentifierFunc = func(ctx context.Context, filter fleet.TeamFilter, hostIdentifiers []string) ([]uint, error) {
if len(hostIdentifiers) == 1 && hostIdentifiers[0] == "1234" {
return []uint{1234}, nil
}
return nil, nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
return nil, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
if opt.MatchQuery == queryName {
return []*fleet.Query{&query}, 1, nil, nil
}
return []*fleet.Query{}, 0, nil, nil
}
ds.NewDistributedQueryCampaignFunc = func(ctx context.Context, camp *fleet.DistributedQueryCampaign) (*fleet.DistributedQueryCampaign, error) {
camp.ID = 321
return camp, nil
}
ds.NewDistributedQueryCampaignTargetFunc = func(ctx context.Context, target *fleet.DistributedQueryCampaignTarget) (*fleet.DistributedQueryCampaignTarget, error) {
return target, nil
}
noHostsTargeted := false
ds.HostIDsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets) ([]uint, error) {
if noHostsTargeted {
return nil, nil
}
return []uint{1}, nil
}
ds.CountHostsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error) {
return fleet.TargetMetrics{TotalHosts: 1, OnlineHosts: 1}, nil
}
lq.On("QueriesForHost", uint(1)).Return(
map[string]string{
"42": queryString,
},
nil,
)
lq.On("QueryCompletedByHost", "42", 99).Return(nil)
lq.On("RunQuery", "321", queryString, []uint{1}).Return(nil)
ds.DistributedQueryCampaignTargetIDsFunc = func(ctx context.Context, id uint) (targets *fleet.HostTargets, err error) {
return &fleet.HostTargets{HostIDs: []uint{99}}, nil
}
ds.DistributedQueryCampaignFunc = func(ctx context.Context, id uint) (*fleet.DistributedQueryCampaign, error) {
return &fleet.DistributedQueryCampaign{
ID: 321,
UserID: admin.ID,
}, nil
}
ds.SaveDistributedQueryCampaignFunc = func(ctx context.Context, camp *fleet.DistributedQueryCampaign) error {
return nil
}
ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) {
return &query, nil
}
ds.IsSavedQueryFunc = func(ctx context.Context, queryID uint) (bool, error) {
return true, nil
}
var GetLiveQueryStatsFuncWg sync.WaitGroup
GetLiveQueryStatsFuncWg.Add(2)
ds.GetLiveQueryStatsFunc = func(ctx context.Context, queryID uint, hostIDs []uint) ([]*fleet.LiveQueryStats, error) {
stats := []*fleet.LiveQueryStats{
{
LastExecuted: time.Now(),
},
}
GetLiveQueryStatsFuncWg.Done()
return stats, nil
}
var UpdateLiveQueryStatsFuncWg sync.WaitGroup
UpdateLiveQueryStatsFuncWg.Add(1)
ds.UpdateLiveQueryStatsFunc = func(ctx context.Context, queryID uint, stats []*fleet.LiveQueryStats) error {
UpdateLiveQueryStatsFuncWg.Done()
return nil
}
var CalculateAggregatedPerfStatsPercentilesFuncWg sync.WaitGroup
CalculateAggregatedPerfStatsPercentilesFuncWg.Add(1)
ds.CalculateAggregatedPerfStatsPercentilesFunc = func(ctx context.Context, aggregate fleet.AggregatedStatsType, queryID uint) error {
CalculateAggregatedPerfStatsPercentilesFuncWg.Done()
return nil
}
go func() {
time.Sleep(2 * time.Second)
require.NoError(t, rs.WriteResult(
fleet.DistributedQueryResult{
DistributedQueryCampaignID: 321,
Rows: []map[string]string{{"bing": "fds"}},
Host: fleet.ResultHostData{
ID: 99,
Hostname: "somehostname",
DisplayName: "somehostname",
},
Stats: &fleet.Stats{
WallTimeMs: 10,
UserTime: 20,
SystemTime: 30,
Memory: 40,
},
},
))
}()
// errors before requesting live query
_, err = RunAppNoChecks([]string{"query", "--hosts", "", "--query-name", queryName})
assert.Error(t, err)
assert.Contains(t, err.Error(), "No hosts or labels targeted")
expected := `{"host":"somehostname","rows":[{"bing":"fds","host_display_name":"somehostname","host_hostname":"somehostname"}]}
`
// Note: runAppForTest never closes the WebSocket connection and does not exit,
// so we are unable to see the activity data that is written after WebSocket disconnects.
assert.Equal(t, expected, RunAppForTest(t, []string{"query", "--hosts", "1234", "--query-name", queryName}))
// We need to use waitGroups to detect whether Database functions were called because this is an asynchronous test which will flag data races otherwise.
c := make(chan struct{})
go func() {
defer close(c)
GetLiveQueryStatsFuncWg.Wait()
UpdateLiveQueryStatsFuncWg.Wait()
CalculateAggregatedPerfStatsPercentilesFuncWg.Wait()
}()
select {
case <-time.After(time.Second):
require.Fail(
t,
"Expected invocation of one of these Database functions did not happen: GetLiveQueryStats, UpdateLiveQueryStats, or CalculateAggregatedPerfStatsPercentiles",
)
case <-c: // All good
}
// Test targeting no hosts (e.g. host does exist)
noHostsTargeted = true
_, err = RunAppNoChecks([]string{"query", "--hosts", "foobar", "--query-name", queryName})
assert.Error(t, err)
assert.Contains(t, err.Error(), "No hosts targeted")
}
func TestAdHocLiveQuery(t *testing.T) {
rs := pubsub.NewInmemQueryResults()
lq := live_query_mock.New(t)
logger := kitlog.NewJSONLogger(os.Stdout)
logger = level.NewFilter(logger, level.AllowDebug())
_, ds := testing_utils.RunServerWithMockedDS(
t, &service.TestServerOpts{
Rs: rs,
Lq: lq,
Logger: logger,
},
)
users, err := ds.ListUsersFunc(context.Background(), fleet.UserListOptions{})
require.NoError(t, err)
var admin *fleet.User
for _, user := range users {
if user.GlobalRole != nil && *user.GlobalRole == fleet.RoleAdmin {
admin = user
}
}
ds.HostIDsByIdentifierFunc = func(ctx context.Context, filter fleet.TeamFilter, hostIdentifiers []string) ([]uint, error) {
return []uint{1234}, nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
return map[string]uint{"label1": uint(1)}, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
ds.NewQueryFunc = func(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) {
query.ID = 42
return query, nil
}
ds.NewDistributedQueryCampaignFunc = func(ctx context.Context, camp *fleet.DistributedQueryCampaign) (
*fleet.DistributedQueryCampaign, error,
) {
camp.ID = 321
return camp, nil
}
ds.NewDistributedQueryCampaignTargetFunc = func(
ctx context.Context, target *fleet.DistributedQueryCampaignTarget,
) (*fleet.DistributedQueryCampaignTarget, error) {
return target, nil
}
ds.HostIDsInTargetsFunc = func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets) ([]uint, error) {
return []uint{1}, nil
}
ds.CountHostsInTargetsFunc = func(
ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets, now time.Time,
) (fleet.TargetMetrics, error) {
return fleet.TargetMetrics{TotalHosts: 1, OnlineHosts: 1}, nil
}
lq.On("QueriesForHost", uint(1)).Return(
map[string]string{
"42": "select 42, * from time",
},
nil,
)
lq.On("QueryCompletedByHost", "42", 99).Return(nil)
lq.On("RunQuery", "321", "select 42, * from time", []uint{1}).Return(nil)
ds.DistributedQueryCampaignTargetIDsFunc = func(ctx context.Context, id uint) (targets *fleet.HostTargets, err error) {
return &fleet.HostTargets{HostIDs: []uint{99}}, nil
}
ds.DistributedQueryCampaignFunc = func(ctx context.Context, id uint) (*fleet.DistributedQueryCampaign, error) {
return &fleet.DistributedQueryCampaign{
ID: 321,
UserID: admin.ID,
}, nil
}
ds.SaveDistributedQueryCampaignFunc = func(ctx context.Context, camp *fleet.DistributedQueryCampaign) error {
return nil
}
ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) {
return &fleet.Query{}, nil
}
ds.IsSavedQueryFunc = func(ctx context.Context, queryID uint) (bool, error) {
return false, nil
}
go func() {
time.Sleep(2 * time.Second)
require.NoError(
t, rs.WriteResult(
fleet.DistributedQueryResult{
DistributedQueryCampaignID: 321,
Rows: []map[string]string{{"bing": "fds"}},
Host: fleet.ResultHostData{
ID: 99,
Hostname: "somehostname",
DisplayName: "somehostname",
},
Stats: &fleet.Stats{
WallTimeMs: 10,
UserTime: 20,
SystemTime: 30,
Memory: 40,
},
},
),
)
}()
// test label not found
_, err = RunAppNoChecks([]string{"query", "--hosts", "1234", "--labels", "iamnotalabel", "--query", "select 42, * from time"})
assert.ErrorContains(t, err, "Invalid label name(s): iamnotalabel.")
// test if some labels were not found
_, err = RunAppNoChecks([]string{"query", "--labels", "label1, mac, windows", "--hosts", "1234", "--query",
"select 42, * from time"})
assert.ErrorContains(t, err, "Invalid label name(s): mac, windows.")
expected := `{"host":"somehostname","rows":[{"bing":"fds","host_display_name":"somehostname","host_hostname":"somehostname"}]}
`
assert.Equal(t, expected, RunAppForTest(t, []string{"query", "--hosts", "1234", "--query", "select 42, * from time"}))
}