mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- 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 -->
113 lines
3.6 KiB
Go
113 lines
3.6 KiB
Go
// Package testutils provides shared test utilities for the activity bounded context.
|
|
package testutils
|
|
|
|
import (
|
|
"encoding/json"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
|
mysql_testing_utils "github.com/fleetdm/fleet/v4/server/platform/mysql/testing_utils"
|
|
"github.com/go-kit/log"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestDB holds the database connection for tests.
|
|
type TestDB struct {
|
|
DB *sqlx.DB
|
|
Logger log.Logger
|
|
}
|
|
|
|
// SetupTestDB creates a test database with the Fleet schema loaded.
|
|
func SetupTestDB(t *testing.T, testNamePrefix string) *TestDB {
|
|
t.Helper()
|
|
|
|
testName, opts := mysql_testing_utils.ProcessOptions(t, &mysql_testing_utils.DatastoreTestOptions{
|
|
UniqueTestName: testNamePrefix + "_" + t.Name(),
|
|
})
|
|
|
|
_, thisFile, _, _ := runtime.Caller(0)
|
|
schemaPath := filepath.Join(filepath.Dir(thisFile), "../../../../server/datastore/mysql/schema.sql")
|
|
mysql_testing_utils.LoadSchema(t, testName, opts, schemaPath)
|
|
|
|
config := mysql_testing_utils.MysqlTestConfig(testName)
|
|
db, err := common_mysql.NewDB(config, &common_mysql.DBOptions{}, "")
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() { db.Close() })
|
|
|
|
return &TestDB{
|
|
DB: db,
|
|
Logger: log.NewNopLogger(),
|
|
}
|
|
}
|
|
|
|
// Conns returns DBConnections for creating a datastore.
|
|
func (tdb *TestDB) Conns() *common_mysql.DBConnections {
|
|
return &common_mysql.DBConnections{Primary: tdb.DB, Replica: tdb.DB}
|
|
}
|
|
|
|
// TruncateTables clears the activities and users tables.
|
|
func (tdb *TestDB) TruncateTables(t *testing.T) {
|
|
t.Helper()
|
|
mysql_testing_utils.TruncateTables(t, tdb.DB, tdb.Logger, nil, "activities", "users")
|
|
}
|
|
|
|
// InsertUser creates a user in the database and returns the user ID.
|
|
func (tdb *TestDB) InsertUser(t *testing.T, name, email string) uint {
|
|
t.Helper()
|
|
ctx := t.Context()
|
|
|
|
result, err := tdb.DB.ExecContext(ctx, `
|
|
INSERT INTO users (name, email, password, salt, created_at, updated_at)
|
|
VALUES (?, ?, 'password', 'salt', NOW(), NOW())
|
|
`, name, email)
|
|
require.NoError(t, err)
|
|
|
|
id, err := result.LastInsertId()
|
|
require.NoError(t, err)
|
|
return uint(id)
|
|
}
|
|
|
|
// InsertActivity creates an activity in the database and returns the activity ID.
|
|
// Pass nil for userID to create a system activity (no associated user).
|
|
func (tdb *TestDB) InsertActivity(t *testing.T, userID *uint, activityType string, details map[string]any) uint {
|
|
t.Helper()
|
|
return tdb.InsertActivityWithTime(t, userID, activityType, details, time.Now().UTC())
|
|
}
|
|
|
|
// InsertActivityWithTime creates an activity with a specific timestamp.
|
|
// Pass nil for userID to create a system activity (no associated user).
|
|
func (tdb *TestDB) InsertActivityWithTime(t *testing.T, userID *uint, activityType string, details map[string]any, createdAt time.Time) uint {
|
|
t.Helper()
|
|
ctx := t.Context()
|
|
|
|
detailsJSON, err := json.Marshal(details)
|
|
require.NoError(t, err)
|
|
|
|
var userName *string
|
|
var userEmail string // NOT NULL DEFAULT '' in schema
|
|
if userID != nil {
|
|
var user struct {
|
|
Name string `db:"name"`
|
|
Email string `db:"email"`
|
|
}
|
|
err = sqlx.GetContext(ctx, tdb.DB, &user, "SELECT name, email FROM users WHERE id = ?", *userID)
|
|
require.NoError(t, err)
|
|
userName = &user.Name
|
|
userEmail = user.Email
|
|
}
|
|
|
|
result, err := tdb.DB.ExecContext(ctx, `
|
|
INSERT INTO activities (user_id, user_name, user_email, activity_type, details, created_at, host_only, streamed)
|
|
VALUES (?, ?, ?, ?, ?, ?, false, false)
|
|
`, userID, userName, userEmail, activityType, detailsJSON, createdAt)
|
|
require.NoError(t, err)
|
|
|
|
id, err := result.LastInsertId()
|
|
require.NoError(t, err)
|
|
return uint(id)
|
|
}
|