mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Refactoring. No functional changes. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Simplified internal pointer value construction across activity-related test and service code by consolidating helper functions and using Go's built-in operators. No changes to user-facing functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
421 lines
13 KiB
Go
421 lines
13 KiB
Go
package mysql
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
activityapi "github.com/fleetdm/fleet/v4/server/activity/api"
|
|
"github.com/fleetdm/fleet/v4/server/activity/internal/testutils"
|
|
"github.com/fleetdm/fleet/v4/server/activity/internal/types"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// testEnv holds test dependencies.
|
|
type testEnv struct {
|
|
*testutils.TestDB
|
|
ds *Datastore
|
|
}
|
|
|
|
func TestListActivities(t *testing.T) {
|
|
tdb := testutils.SetupTestDB(t, "activity_mysql")
|
|
ds := NewDatastore(tdb.Conns(), tdb.Logger)
|
|
env := &testEnv{TestDB: tdb, ds: ds}
|
|
|
|
cases := []struct {
|
|
name string
|
|
fn func(t *testing.T, env *testEnv)
|
|
}{
|
|
{"Basic", testListActivitiesBasic},
|
|
{"Streamed", testListActivitiesStreamed},
|
|
{"PaginationMetadata", testListActivitiesPaginationMetadata},
|
|
{"ActivityTypeFilter", testListActivitiesActivityTypeFilter},
|
|
{"DateRangeFilter", testListActivitiesDateRangeFilter},
|
|
{"MatchQuery", testListActivitiesMatchQuery},
|
|
{"Ordering", testListActivitiesOrdering},
|
|
{"CursorPagination", testListActivitiesCursorPagination},
|
|
{"HostPastActivities", testListHostPastActivities},
|
|
{"MarkActivitiesAsStreamed", testMarkActivitiesAsStreamed},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
defer env.TruncateTables(t)
|
|
c.fn(t, env)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testListActivitiesBasic(t *testing.T, env *testEnv) {
|
|
ctx := t.Context()
|
|
userID := env.InsertUser(t, "testuser", "test@example.com")
|
|
|
|
// Create user activities and a system activity (nil user)
|
|
for i := range 3 {
|
|
env.InsertActivity(t, &userID, fmt.Sprintf("test_activity_%d", i), map[string]any{"detail": i})
|
|
}
|
|
env.InsertActivity(t, nil, "system_activity", map[string]any{})
|
|
|
|
activities, meta, err := env.ds.ListActivities(ctx, listOpts(withMetadata()))
|
|
require.NoError(t, err)
|
|
assert.Len(t, activities, 4)
|
|
assert.NotNil(t, meta)
|
|
|
|
// Verify user activities have actor info
|
|
for _, a := range activities {
|
|
assert.NotZero(t, a.ID)
|
|
assert.NotEmpty(t, a.Type)
|
|
assert.NotNil(t, a.Details)
|
|
|
|
if a.Type == "system_activity" {
|
|
// System activity has no actor
|
|
assert.Nil(t, a.ActorID)
|
|
assert.Nil(t, a.ActorFullName)
|
|
} else {
|
|
require.NotNil(t, a.ActorID)
|
|
assert.Equal(t, userID, *a.ActorID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testListActivitiesStreamed(t *testing.T, env *testEnv) {
|
|
ctx := t.Context()
|
|
userID := env.InsertUser(t, "testuser", "test@example.com")
|
|
|
|
activityIDs := make([]uint, 0, 3)
|
|
for i := range 3 {
|
|
id := env.InsertActivity(t, &userID, "test_activity", map[string]any{"detail": i})
|
|
activityIDs = append(activityIDs, id)
|
|
}
|
|
|
|
// Mark first activity as streamed
|
|
_, err := env.DB.ExecContext(ctx, "UPDATE activity_past SET streamed = true WHERE id = ?", activityIDs[0])
|
|
require.NoError(t, err)
|
|
|
|
cases := []struct {
|
|
name string
|
|
streamed *bool
|
|
expectedIDs []uint
|
|
}{
|
|
{"all", nil, activityIDs},
|
|
{"non-streamed only", new(false), activityIDs[1:]},
|
|
{"streamed only", new(true), activityIDs[:1]},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
activities, _, err := env.ds.ListActivities(ctx, listOpts(withStreamed(tc.streamed)))
|
|
require.NoError(t, err)
|
|
gotIDs := make([]uint, len(activities))
|
|
for i, a := range activities {
|
|
gotIDs[i] = a.ID
|
|
}
|
|
assert.ElementsMatch(t, tc.expectedIDs, gotIDs)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testListActivitiesPaginationMetadata(t *testing.T, env *testEnv) {
|
|
ctx := t.Context()
|
|
userID := env.InsertUser(t, "testuser", "test@example.com")
|
|
|
|
for i := range 3 {
|
|
env.InsertActivity(t, &userID, fmt.Sprintf("test_%d", i), map[string]any{})
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
perPage uint
|
|
page uint
|
|
wantCount int
|
|
wantNext bool
|
|
wantPrev bool
|
|
}{
|
|
{"first page with more", 2, 0, 2, true, false},
|
|
{"second page partial", 2, 1, 1, false, true},
|
|
{"all results", 100, 0, 3, false, false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
activities, meta, err := env.ds.ListActivities(ctx, listOpts(withPerPage(tc.perPage), withPage(tc.page), withMetadata()))
|
|
require.NoError(t, err)
|
|
assert.Len(t, activities, tc.wantCount)
|
|
require.NotNil(t, meta)
|
|
assert.Equal(t, tc.wantNext, meta.HasNextResults)
|
|
assert.Equal(t, tc.wantPrev, meta.HasPreviousResults)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testListActivitiesActivityTypeFilter(t *testing.T, env *testEnv) {
|
|
ctx := t.Context()
|
|
userID := env.InsertUser(t, "testuser", "test@example.com")
|
|
|
|
env.InsertActivity(t, &userID, "edited_script", map[string]any{})
|
|
env.InsertActivity(t, &userID, "edited_script", map[string]any{})
|
|
env.InsertActivity(t, &userID, "mdm_enrolled", map[string]any{})
|
|
|
|
cases := []struct {
|
|
activityType string
|
|
wantCount int
|
|
}{
|
|
{"edited_script", 2},
|
|
{"mdm_enrolled", 1},
|
|
{"non_existent", 0},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.activityType, func(t *testing.T) {
|
|
activities, _, err := env.ds.ListActivities(ctx, listOpts(withActivityType(tc.activityType)))
|
|
require.NoError(t, err)
|
|
assert.Len(t, activities, tc.wantCount)
|
|
for _, a := range activities {
|
|
assert.Equal(t, tc.activityType, a.Type)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testListActivitiesDateRangeFilter(t *testing.T, env *testEnv) {
|
|
ctx := t.Context()
|
|
userID := env.InsertUser(t, "testuser", "test@example.com")
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
|
|
// Only create activities in the past/present (activities can't have future creation dates)
|
|
dates := []time.Time{
|
|
now.Add(-48 * time.Hour),
|
|
now.Add(-24 * time.Hour),
|
|
now,
|
|
}
|
|
for _, dt := range dates {
|
|
env.InsertActivityWithTime(t, &userID, "test_activity", map[string]any{}, dt)
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
start string
|
|
end string
|
|
wantCount int
|
|
}{
|
|
{"no filter", "", "", 3},
|
|
{"start only", now.Add(-72 * time.Hour).Format(time.RFC3339), "", 3},
|
|
{"start and end", now.Add(-72 * time.Hour).Format(time.RFC3339), now.Add(-12 * time.Hour).Format(time.RFC3339), 2},
|
|
{"end only", "", now.Add(-30 * time.Hour).Format(time.RFC3339), 1},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
activities, _, err := env.ds.ListActivities(ctx, listOpts(withDateRange(tc.start, tc.end)))
|
|
require.NoError(t, err)
|
|
assert.Len(t, activities, tc.wantCount)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testListActivitiesMatchQuery(t *testing.T, env *testEnv) {
|
|
ctx := t.Context()
|
|
|
|
johnUserID := env.InsertUser(t, "john_doe", "john@example.com")
|
|
janeUserID := env.InsertUser(t, "jane_smith", "jane@example.com")
|
|
|
|
env.InsertActivity(t, &johnUserID, "test_activity", map[string]any{})
|
|
env.InsertActivity(t, &janeUserID, "test_activity", map[string]any{})
|
|
|
|
cases := []struct {
|
|
name string
|
|
query string
|
|
matchingUserIDs []uint
|
|
wantCount int
|
|
}{
|
|
{"by username prefix", "john", nil, 1},
|
|
{"by email prefix", "jane@", nil, 1},
|
|
{"no match", "nomatch", nil, 0},
|
|
{"via matching user IDs", "nomatch", []uint{johnUserID}, 1},
|
|
{"via multiple matching user IDs", "nomatch", []uint{johnUserID, janeUserID}, 2},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
opts := listOpts(withMatchQuery(tc.query))
|
|
opts.MatchingUserIDs = tc.matchingUserIDs
|
|
activities, _, err := env.ds.ListActivities(ctx, opts)
|
|
require.NoError(t, err)
|
|
assert.Len(t, activities, tc.wantCount)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testListActivitiesOrdering(t *testing.T, env *testEnv) {
|
|
ctx := t.Context()
|
|
userID := env.InsertUser(t, "testuser", "test@example.com")
|
|
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
env.InsertActivityWithTime(t, &userID, "activity_oldest", map[string]any{}, now.Add(-2*time.Hour))
|
|
env.InsertActivityWithTime(t, &userID, "activity_middle", map[string]any{}, now.Add(-1*time.Hour))
|
|
env.InsertActivityWithTime(t, &userID, "activity_newest", map[string]any{}, now)
|
|
|
|
cases := []struct {
|
|
name string
|
|
orderKey string
|
|
orderDir activityapi.OrderDirection
|
|
wantFirst string
|
|
wantLast string
|
|
}{
|
|
{"created_at desc", "created_at", activityapi.OrderDescending, "activity_newest", "activity_oldest"},
|
|
{"created_at asc", "created_at", activityapi.OrderAscending, "activity_oldest", "activity_newest"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
activities, _, err := env.ds.ListActivities(ctx, listOpts(withOrder(tc.orderKey, tc.orderDir)))
|
|
require.NoError(t, err)
|
|
require.Len(t, activities, 3)
|
|
assert.Equal(t, tc.wantFirst, activities[0].Type)
|
|
assert.Equal(t, tc.wantLast, activities[2].Type)
|
|
})
|
|
}
|
|
|
|
// Verify ID ordering works
|
|
activities, _, err := env.ds.ListActivities(ctx, listOpts(withOrder("id", activityapi.OrderAscending)))
|
|
require.NoError(t, err)
|
|
require.Len(t, activities, 3)
|
|
assert.Less(t, activities[0].ID, activities[1].ID)
|
|
assert.Less(t, activities[1].ID, activities[2].ID)
|
|
}
|
|
|
|
func testListActivitiesCursorPagination(t *testing.T, env *testEnv) {
|
|
ctx := t.Context()
|
|
userID := env.InsertUser(t, "testuser", "test@example.com")
|
|
|
|
for i := range 5 {
|
|
env.InsertActivity(t, &userID, fmt.Sprintf("activity_%d", i), map[string]any{})
|
|
}
|
|
|
|
// Get first page
|
|
activities, _, err := env.ds.ListActivities(ctx, listOpts(withPerPage(2), withOrder("id", activityapi.OrderAscending)))
|
|
require.NoError(t, err)
|
|
require.Len(t, activities, 2)
|
|
lastID := activities[1].ID
|
|
|
|
// Get next page using cursor
|
|
activities, _, err = env.ds.ListActivities(ctx, listOpts(withPerPage(2), withOrder("id", activityapi.OrderAscending), withAfter(fmt.Sprintf("%d", lastID))))
|
|
require.NoError(t, err)
|
|
require.Len(t, activities, 2)
|
|
for _, a := range activities {
|
|
assert.Greater(t, a.ID, lastID)
|
|
}
|
|
}
|
|
|
|
// Test helpers for building ListOptions
|
|
|
|
type listOptsFunc func(*types.ListOptions)
|
|
|
|
func listOpts(opts ...listOptsFunc) types.ListOptions {
|
|
o := types.ListOptions{ListOptions: activityapi.ListOptions{PerPage: 100}}
|
|
for _, fn := range opts {
|
|
fn(&o)
|
|
}
|
|
return o
|
|
}
|
|
|
|
func withPerPage(n uint) listOptsFunc {
|
|
return func(o *types.ListOptions) { o.PerPage = n }
|
|
}
|
|
|
|
func withPage(n uint) listOptsFunc {
|
|
return func(o *types.ListOptions) { o.Page = n }
|
|
}
|
|
|
|
func withMetadata() listOptsFunc {
|
|
return func(o *types.ListOptions) { o.IncludeMetadata = true }
|
|
}
|
|
|
|
func withStreamed(s *bool) listOptsFunc {
|
|
return func(o *types.ListOptions) { o.Streamed = s }
|
|
}
|
|
|
|
func withActivityType(t string) listOptsFunc {
|
|
return func(o *types.ListOptions) { o.ActivityType = t }
|
|
}
|
|
|
|
func withMatchQuery(q string) listOptsFunc {
|
|
return func(o *types.ListOptions) { o.MatchQuery = q }
|
|
}
|
|
|
|
func withDateRange(start, end string) listOptsFunc {
|
|
return func(o *types.ListOptions) {
|
|
o.StartCreatedAt = start
|
|
o.EndCreatedAt = end
|
|
}
|
|
}
|
|
|
|
func withOrder(key string, dir activityapi.OrderDirection) listOptsFunc {
|
|
return func(o *types.ListOptions) {
|
|
o.OrderKey = key
|
|
o.OrderDirection = dir
|
|
}
|
|
}
|
|
|
|
func withAfter(cursor string) listOptsFunc {
|
|
return func(o *types.ListOptions) { o.After = cursor }
|
|
}
|
|
|
|
func testListHostPastActivities(t *testing.T, env *testEnv) {
|
|
ctx := t.Context()
|
|
userID := env.InsertUser(t, "user1", "user1@example.com")
|
|
hostID := env.InsertHost(t, "h1.local", nil)
|
|
|
|
// Create activities linked to host with different types
|
|
env.InsertHostActivity(t, hostID, env.InsertActivity(t, &userID, "ran_script", map[string]any{"host_id": float64(hostID)}))
|
|
env.InsertHostActivity(t, hostID, env.InsertActivity(t, &userID, "installed_software", map[string]any{"host_id": float64(hostID)}))
|
|
|
|
cases := []struct {
|
|
name string
|
|
opts types.ListOptions
|
|
wantLen int
|
|
wantMeta *activityapi.PaginationMetadata
|
|
wantTypes []string
|
|
}{
|
|
{"first page", listOpts(withPerPage(1), withPage(0), withMetadata()), 1, &activityapi.PaginationMetadata{HasNextResults: true, HasPreviousResults: false}, []string{"ran_script"}},
|
|
{"second page", listOpts(withPerPage(1), withPage(1), withMetadata()), 1, &activityapi.PaginationMetadata{HasNextResults: false, HasPreviousResults: true}, []string{"installed_software"}},
|
|
{"all activities", listOpts(withPerPage(2), withPage(0), withMetadata()), 2, &activityapi.PaginationMetadata{HasNextResults: false, HasPreviousResults: false}, []string{"ran_script", "installed_software"}},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
acts, meta, err := env.ds.ListHostPastActivities(ctx, hostID, tc.opts)
|
|
require.NoError(t, err)
|
|
require.Len(t, acts, tc.wantLen)
|
|
require.Equal(t, tc.wantMeta, meta)
|
|
|
|
// Verify fields and activity types
|
|
for i, a := range acts {
|
|
require.Equal(t, "user1@example.com", *a.ActorEmail)
|
|
require.Equal(t, "user1", *a.ActorFullName)
|
|
require.Equal(t, tc.wantTypes[i], a.Type)
|
|
require.Equal(t, userID, *a.ActorID)
|
|
require.NotNil(t, a.Details)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testMarkActivitiesAsStreamed(t *testing.T, env *testEnv) {
|
|
ctx := t.Context()
|
|
userID := env.InsertUser(t, "testuser", "test@example.com")
|
|
|
|
actA := env.InsertActivity(t, &userID, "activity_a", map[string]any{})
|
|
actB := env.InsertActivity(t, &userID, "activity_b", map[string]any{})
|
|
|
|
err := env.ds.MarkActivitiesAsStreamed(ctx, []uint{actA, actB})
|
|
require.NoError(t, err)
|
|
|
|
// Verify marked as streamed
|
|
activities, meta, err := env.ds.ListActivities(ctx, listOpts(withStreamed(new(true)), withMetadata()))
|
|
require.NoError(t, err)
|
|
assert.Len(t, activities, 2)
|
|
require.NotNil(t, meta)
|
|
assert.False(t, meta.HasNextResults)
|
|
assert.False(t, meta.HasPreviousResults)
|
|
}
|