fleet/server/activity/internal/tests/integration_test.go
Victor Lyuboslavsky 6019fa6d5a
Activity bounded context: /api/latest/fleet/activities (1 of 2) (#38115)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #37806 

This PR creates an activity bounded context and moves the following HTTP
endpoint (including the full vertical slice) there:
`/api/latest/fleet/activities`

NONE of the other activity functionality is moved! This is an
incremental approach starting with just 1 API/service endpoint.

A significant part of this PR is tests. This feature is now receiving
significantly more unit/integration test coverage than before.

Also, this PR does not remove the `ListActivities` datastore method in
the legacy code. That will be done in the follow up PR (part 2 of 2).

This refactoring effort also uncovered an activity/user authorization
issue: https://fleetdm.slack.com/archives/C02A8BRABB5/p1768582236611479

# Checklist for submitter

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

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

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually


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

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Activity listing API now available with query filtering, date-range
filtering, and type-based filtering
* Pagination support for activity results with cursor-based and
offset-based options
* Configurable sorting by creation date or activity ID in ascending or
descending order
* Automatic enrichment of activity records with actor user details
(name, email, avatar)
* Role-based access controls applied to activity visibility based on
user permissions

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-19 09:07:14 -05:00

167 lines
6.2 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},
}
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)
}