fleet/server/activity/internal/tests/integration_test.go
Victor Lyuboslavsky 61f635dd44
Activity bounded context: Complete read operations (#38555)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #38534

moved `/api/_version_/fleet/hosts/{id:[0-9]+}/activities` endpoint and
`MarkActivitiesAsStreamed` to activity bounded context

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added new endpoint to retrieve host-specific past activities with
pagination metadata.
  
* **Refactor**
* Refactored activity service architecture and authorization layer to
improve data provider integration and activity streaming capabilities.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-09 15:29:12 -06:00

236 lines
8.8 KiB
Go

package tests
import (
"net/http"
"strconv"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIntegration(t *testing.T) {
s := setupIntegrationTest(t)
cases := []struct {
name string
fn func(t *testing.T, s *integrationTestSuite)
}{
{"ListActivities", testListActivities},
{"ListActivitiesPagination", testListActivitiesPagination},
{"ListActivitiesCursorPagination", testListActivitiesCursorPagination},
{"ListActivitiesFilters", testListActivitiesFilters},
{"ListActivitiesUserEnrichment", testListActivitiesUserEnrichment},
{"ListHostPastActivities", testListHostPastActivities},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer s.truncateTables(t)
c.fn(t, s)
})
}
}
func testListActivities(t *testing.T, s *integrationTestSuite) {
userID := s.insertUser(t, "admin", "admin@example.com")
// Insert activities
s.InsertActivity(t, ptr.Uint(userID), "applied_spec_pack", map[string]any{})
s.InsertActivity(t, ptr.Uint(userID), "deleted_pack", map[string]any{})
s.InsertActivity(t, ptr.Uint(userID), "edited_pack", map[string]any{})
result, statusCode := s.getActivities(t, "per_page=100")
assert.Equal(t, http.StatusOK, statusCode)
assert.Len(t, result.Activities, 3)
assert.NotNil(t, result.Meta)
// Verify order (newest first by default)
assert.Equal(t, "edited_pack", result.Activities[0].Type)
assert.Equal(t, "deleted_pack", result.Activities[1].Type)
assert.Equal(t, "applied_spec_pack", result.Activities[2].Type)
}
func testListActivitiesPagination(t *testing.T, s *integrationTestSuite) {
userID := s.insertUser(t, "admin", "admin@example.com")
// Insert 5 activities
for i := range 5 {
s.InsertActivity(t, ptr.Uint(userID), "test_activity", map[string]any{"index": i})
}
// First page
result, _ := s.getActivities(t, "per_page=2&order_key=id&order_direction=asc")
assert.Len(t, result.Activities, 2)
assert.True(t, result.Meta.HasNextResults)
assert.False(t, result.Meta.HasPreviousResults)
// Second page
result, _ = s.getActivities(t, "per_page=2&page=1&order_key=id&order_direction=asc")
assert.Len(t, result.Activities, 2)
assert.True(t, result.Meta.HasNextResults)
assert.True(t, result.Meta.HasPreviousResults)
// Last page
result, _ = s.getActivities(t, "per_page=2&page=2&order_key=id&order_direction=asc")
assert.Len(t, result.Activities, 1)
assert.False(t, result.Meta.HasNextResults)
assert.True(t, result.Meta.HasPreviousResults)
}
func testListActivitiesCursorPagination(t *testing.T, s *integrationTestSuite) {
userID := s.insertUser(t, "admin", "admin@example.com")
// Insert 3 activities
s.InsertActivity(t, ptr.Uint(userID), "applied_spec_pack", map[string]any{})
s.InsertActivity(t, ptr.Uint(userID), "deleted_pack", map[string]any{})
s.InsertActivity(t, ptr.Uint(userID), "edited_pack", map[string]any{})
// Test cursor-based pagination with after=0
// Meta should be nil for cursor-based pagination (doesn't return metadata)
result, statusCode := s.getActivities(t, "per_page=1&order_key=id&after=0")
assert.Equal(t, http.StatusOK, statusCode)
assert.Len(t, result.Activities, 1)
assert.Nil(t, result.Meta)
assert.Equal(t, "applied_spec_pack", result.Activities[0].Type)
// Test cursor pagination to get the next activity
firstID := result.Activities[0].ID
result, _ = s.getActivities(t, "per_page=1&order_key=id&after="+strconv.FormatUint(uint64(firstID), 10))
assert.Len(t, result.Activities, 1)
assert.Nil(t, result.Meta)
assert.Equal(t, "deleted_pack", result.Activities[0].Type)
// Test descending order with cursor
result, _ = s.getActivities(t, "per_page=1&order_key=id&order_direction=desc&after=999999")
assert.Len(t, result.Activities, 1)
assert.Nil(t, result.Meta)
// Descending order, so the newest (edited_pack) should be first
assert.Equal(t, "edited_pack", result.Activities[0].Type)
}
func testListActivitiesFilters(t *testing.T, s *integrationTestSuite) {
johnUserID := s.insertUser(t, "john_doe", "john@example.com")
janeUserID := s.insertUser(t, "jane_smith", "jane@example.com")
now := time.Now().UTC().Truncate(time.Second)
// Insert activities with different types, times, and users
s.InsertActivityWithTime(t, ptr.Uint(johnUserID), "type_a", map[string]any{}, now.Add(-48*time.Hour))
s.InsertActivityWithTime(t, ptr.Uint(johnUserID), "type_a", map[string]any{}, now.Add(-24*time.Hour))
s.InsertActivityWithTime(t, ptr.Uint(johnUserID), "type_b", map[string]any{}, now)
s.InsertActivityWithTime(t, ptr.Uint(janeUserID), "type_a", map[string]any{}, now) // Jane's activity
// Filter by type
result, _ := s.getActivities(t, "per_page=100&activity_type=type_a")
assert.Len(t, result.Activities, 3) // 2 from john + 1 from jane
for _, a := range result.Activities {
assert.Equal(t, "type_a", a.Type)
}
// Filter by date range
startDate := now.Add(-36 * time.Hour).Format(time.RFC3339)
result, _ = s.getActivities(t, "per_page=100&start_created_at="+startDate)
assert.Len(t, result.Activities, 3) // -24h, now (john), now (jane)
// Filter by user search query - should only return john's activities
result, _ = s.getActivities(t, "per_page=100&query=john")
assert.Len(t, result.Activities, 3) // Only john's 3 activities, not jane's
for _, a := range result.Activities {
require.NotNil(t, a.ActorID)
assert.Equal(t, johnUserID, *a.ActorID)
}
// Filter by user search query - should only return jane's activities
result, _ = s.getActivities(t, "per_page=100&query=jane")
assert.Len(t, result.Activities, 1) // Only jane's 1 activity
require.NotNil(t, result.Activities[0].ActorID)
assert.Equal(t, janeUserID, *result.Activities[0].ActorID)
}
func testListActivitiesUserEnrichment(t *testing.T, s *integrationTestSuite) {
userID := s.insertUser(t, "John Doe", "john@example.com")
s.InsertActivity(t, ptr.Uint(userID), "test_activity", map[string]any{})
result, _ := s.getActivities(t, "per_page=100")
require.Len(t, result.Activities, 1)
// Verify user enrichment from mock user provider
a := result.Activities[0]
assert.NotNil(t, a.ActorID)
assert.Equal(t, userID, *a.ActorID)
assert.NotNil(t, a.ActorFullName)
assert.Equal(t, "John Doe", *a.ActorFullName)
assert.NotNil(t, a.ActorEmail)
assert.Equal(t, "john@example.com", *a.ActorEmail)
}
func testListHostPastActivities(t *testing.T, s *integrationTestSuite) {
userID := s.insertUser(t, "admin", "admin@example.com")
// Create two hosts
hostA := s.insertHost(t, "host-a.example.com", nil)
hostB := s.insertHost(t, "host-b.example.com", nil)
// Create activities linked to different hosts
actA := s.InsertActivity(t, ptr.Uint(userID), "ran_script", map[string]any{"host": "host-a"})
actB := s.InsertActivity(t, ptr.Uint(userID), "installed_software", map[string]any{"host": "host-a"})
actC := s.InsertActivity(t, ptr.Uint(userID), "ran_script", map[string]any{"host": "host-b"})
// Link activities to hosts
s.InsertHostActivity(t, hostA, actA)
s.InsertHostActivity(t, hostA, actB)
s.InsertHostActivity(t, hostB, actC)
t.Run("returns activities for specific host", func(t *testing.T) {
result, statusCode := s.getHostPastActivities(t, hostA, "per_page=100")
assert.Equal(t, http.StatusOK, statusCode)
assert.Len(t, result.Activities, 2)
assert.NotNil(t, result.Meta)
// Verify order (newest first by default)
assert.Equal(t, "installed_software", result.Activities[0].Type)
assert.Equal(t, "ran_script", result.Activities[1].Type)
// Host B should only have its own activity
result, statusCode = s.getHostPastActivities(t, hostB, "per_page=100")
assert.Equal(t, http.StatusOK, statusCode)
assert.Len(t, result.Activities, 1)
assert.Equal(t, "ran_script", result.Activities[0].Type)
})
t.Run("returns 404 for non-existent host", func(t *testing.T) {
_, statusCode := s.getHostPastActivities(t, 99999, "per_page=100")
assert.Equal(t, http.StatusNotFound, statusCode)
})
t.Run("pagination", func(t *testing.T) {
host := s.insertHost(t, "pagination-host.example.com", nil)
// Insert 5 activities for the host
for i := range 5 {
actID := s.InsertActivity(t, ptr.Uint(userID), "test_activity", map[string]any{"index": i})
s.InsertHostActivity(t, host, actID)
}
// First page
result, _ := s.getHostPastActivities(t, host, "per_page=2")
assert.Len(t, result.Activities, 2)
assert.True(t, result.Meta.HasNextResults)
assert.False(t, result.Meta.HasPreviousResults)
// Second page
result, _ = s.getHostPastActivities(t, host, "per_page=2&page=1")
assert.Len(t, result.Activities, 2)
assert.True(t, result.Meta.HasNextResults)
assert.True(t, result.Meta.HasPreviousResults)
// Last page
result, _ = s.getHostPastActivities(t, host, "per_page=2&page=2")
assert.Len(t, result.Activities, 1)
assert.False(t, result.Meta.HasNextResults)
assert.True(t, result.Meta.HasPreviousResults)
})
}