Skip to content

Commit e8ca959

Browse files
authored
Add enterprise integration test for calendar events (#17900)
Integration tests for the calendar feature: #17441. Adding coverage screenshots for the calendar cron and the osquery distributed/write coverage: ![Screenshot 2024-03-27 at 14 20 44](https://github.com/fleetdm/fleet/assets/2073526/40d394ab-2208-4bec-981b-fe22fae8b5c1) ![Screenshot 2024-03-27 at 14 21 20](https://github.com/fleetdm/fleet/assets/2073526/1e4c8611-21ba-48a6-82f8-a163594f7f01)
1 parent 77c8adf commit e8ca959

File tree

10 files changed

+763
-72
lines changed

10 files changed

+763
-72
lines changed

cmd/fleet/serve.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
configpkg "github.com/fleetdm/fleet/v4/server/config"
2929
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
3030
licensectx "github.com/fleetdm/fleet/v4/server/contexts/license"
31+
"github.com/fleetdm/fleet/v4/server/cron"
3132
"github.com/fleetdm/fleet/v4/server/datastore/cached_mysql"
3233
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
3334
"github.com/fleetdm/fleet/v4/server/datastore/mysqlredis"
@@ -773,9 +774,7 @@ the way that the Fleet server works.
773774
if license.IsPremium() {
774775
if err := cronSchedules.StartCronSchedule(
775776
func() (fleet.CronSchedule, error) {
776-
return newCalendarSchedule(
777-
ctx, instanceID, ds, logger,
778-
)
777+
return cron.NewCalendarSchedule(ctx, instanceID, ds, 5*time.Minute, logger)
779778
},
780779
); err != nil {
781780
initFatal(err, "failed to register calendar schedule")

ee/server/calendar/google_calendar_mock.go

+11
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,14 @@ func ClearMockEvents() {
9393
defer mu.Unlock()
9494
mockEvents = make(map[string]*calendar.Event)
9595
}
96+
97+
func SetMockEventsToNow() {
98+
mu.Lock()
99+
defer mu.Unlock()
100+
101+
now := time.Now()
102+
for _, mockEvent := range mockEvents {
103+
mockEvent.Start = &calendar.EventDateTime{DateTime: now.Format(time.RFC3339)}
104+
mockEvent.End = &calendar.EventDateTime{DateTime: now.Add(30 * time.Minute).Format(time.RFC3339)}
105+
}
106+
}

cmd/fleet/calendar_cron.go server/cron/calendar_cron.go

+5-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package main
1+
package cron
22

33
import (
44
"context"
@@ -19,19 +19,19 @@ import (
1919

2020
const calendarConsumers = 18
2121

22-
func newCalendarSchedule(
22+
func NewCalendarSchedule(
2323
ctx context.Context,
2424
instanceID string,
2525
ds fleet.Datastore,
26+
interval time.Duration,
2627
logger kitlog.Logger,
2728
) (*schedule.Schedule, error) {
2829
const (
29-
name = string(fleet.CronCalendar)
30-
defaultInterval = 5 * time.Minute
30+
name = string(fleet.CronCalendar)
3131
)
3232
logger = kitlog.With(logger, "cron", name)
3333
s := schedule.New(
34-
ctx, name, instanceID, defaultInterval, ds, ds,
34+
ctx, name, instanceID, interval, ds, ds,
3535
schedule.WithAltLockID("calendar"),
3636
schedule.WithLogger(logger),
3737
schedule.WithJob(
@@ -318,9 +318,6 @@ func processFailingHostExistingCalendarEvent(
318318
}
319319
// Even if fields haven't changed we want to update the calendar_events.updated_at below.
320320
updated = true
321-
//
322-
// TODO(lucas): Check changing updatedEvent to UTC before consuming.
323-
//
324321
}
325322

326323
if updated {
@@ -367,8 +364,6 @@ func processFailingHostExistingCalendarEvent(
367364
return fmt.Errorf("update host calendar webhook status: %w", err)
368365
}
369366

370-
// TODO(lucas): If this doesn't work at scale, then implement a special refetch
371-
// for policies only.
372367
if err := ds.UpdateHostRefetchRequested(ctx, host.HostID, true); err != nil {
373368
return fmt.Errorf("refetch host: %w", err)
374369
}

cmd/fleet/calendar_cron_test.go server/cron/calendar_cron_test.go

+57-47
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
package main
1+
package cron
22

33
import (
44
"context"
55
"fmt"
6-
"io"
7-
"net/http"
8-
"net/http/httptest"
96
"os"
107
"strconv"
118
"strings"
@@ -17,7 +14,6 @@ import (
1714
"github.com/fleetdm/fleet/v4/server/fleet"
1815
"github.com/fleetdm/fleet/v4/server/mock"
1916
kitlog "github.com/go-kit/log"
20-
2117
"github.com/stretchr/testify/require"
2218
)
2319

@@ -207,16 +203,19 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
207203
calendar.ClearMockEvents()
208204
})
209205

210-
// TODO(lucas): Test!
211-
webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
212-
require.Equal(t, "POST", r.Method)
213-
requestBodyBytes, err := io.ReadAll(r.Body)
214-
require.NoError(t, err)
215-
t.Logf("webhook request: %s\n", requestBodyBytes)
216-
}))
217-
t.Cleanup(func() {
218-
webhookServer.Close()
219-
})
206+
//
207+
// Test setup
208+
//
209+
// team1:
210+
//
211+
// policyID1 (calendar)
212+
// policyID2 (calendar)
213+
//
214+
// hostID1 has [email protected] not passing policies.
215+
// hostID2 has [email protected] passing policies.
216+
// hostID3 does not have example.com email and is not passing policies.
217+
// hostID4 does not have example.com email and is passing policies.
218+
//
220219

221220
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
222221
return &fleet.AppConfig{
@@ -242,7 +241,7 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
242241
Integrations: fleet.TeamIntegrations{
243242
GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
244243
Enable: true,
245-
WebhookURL: webhookServer.URL,
244+
WebhookURL: "https://foo.example.com",
246245
},
247246
},
248247
},
@@ -268,12 +267,13 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
268267

269268
hostID1, userEmail1 := uint(100), "[email protected]"
270269
hostID2, userEmail2 := uint(101), "[email protected]"
271-
hostID3, userEmail3 := uint(102), "[email protected]"
272-
hostID4, userEmail4 := uint(103), "[email protected]"
270+
hostID3 := uint(102)
271+
hostID4 := uint(103)
273272

274273
ds.GetTeamHostsPolicyMembershipsFunc = func(
275274
ctx context.Context, domain string, teamID uint, policyIDs []uint,
276275
) ([]fleet.HostPolicyMembershipData, error) {
276+
require.Equal(t, "example.com", domain)
277277
require.Equal(t, teamID1, teamID)
278278
require.Equal(t, []uint{policyID1, policyID2}, policyIDs)
279279
return []fleet.HostPolicyMembershipData{
@@ -289,12 +289,12 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
289289
},
290290
{
291291
HostID: hostID3,
292-
Email: userEmail3,
292+
Email: "", // because it does not belong to example.com
293293
Passing: false,
294294
},
295295
{
296296
HostID: hostID4,
297-
Email: userEmail4,
297+
Email: "", // because it does not belong to example.com
298298
Passing: true,
299299
},
300300
}, nil
@@ -304,33 +304,54 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
304304
return nil, nil, notFoundErr{}
305305
}
306306

307+
var eventsMu sync.Mutex
308+
calendarEvents := make(map[string]*fleet.CalendarEvent)
309+
hostCalendarEvents := make(map[uint]*fleet.HostCalendarEvent)
310+
307311
ds.CreateOrUpdateCalendarEventFunc = func(ctx context.Context,
308312
email string,
309313
startTime, endTime time.Time,
310314
data []byte,
311315
hostID uint,
312316
webhookStatus fleet.CalendarWebhookStatus,
313317
) (*fleet.CalendarEvent, error) {
314-
switch email {
315-
case userEmail1:
316-
require.Equal(t, hostID1, hostID)
317-
case userEmail2:
318-
require.Equal(t, hostID2, hostID)
319-
case userEmail3:
320-
require.Equal(t, hostID3, hostID)
321-
case userEmail4:
322-
require.Equal(t, hostID4, hostID)
323-
}
318+
require.Equal(t, hostID1, hostID)
319+
require.Equal(t, userEmail1, email)
324320
require.Equal(t, fleet.CalendarWebhookStatusNone, webhookStatus)
325321
require.NotEmpty(t, data)
326322
require.NotZero(t, startTime)
327323
require.NotZero(t, endTime)
328-
// Currently, the returned calendar event is unused.
324+
325+
eventsMu.Lock()
326+
calendarEventID := uint(len(calendarEvents) + 1)
327+
calendarEvents[email] = &fleet.CalendarEvent{
328+
ID: calendarEventID,
329+
Email: email,
330+
StartTime: startTime,
331+
EndTime: endTime,
332+
Data: data,
333+
}
334+
hostCalendarEventID := uint(len(hostCalendarEvents) + 1)
335+
hostCalendarEvents[hostID] = &fleet.HostCalendarEvent{
336+
ID: hostCalendarEventID,
337+
HostID: hostID,
338+
CalendarEventID: calendarEventID,
339+
WebhookStatus: webhookStatus,
340+
}
341+
eventsMu.Unlock()
329342
return nil, nil
330343
}
331344

332345
err := cronCalendarEvents(ctx, ds, logger)
333346
require.NoError(t, err)
347+
348+
eventsMu.Lock()
349+
require.Len(t, calendarEvents, 1)
350+
require.Len(t, hostCalendarEvents, 1)
351+
eventsMu.Unlock()
352+
353+
createdCalendarEvents := calendar.ListGoogleMockEvents()
354+
require.Len(t, createdCalendarEvents, 1)
334355
}
335356

336357
type notFoundErr struct{}
@@ -356,17 +377,6 @@ func TestCalendarEvents1KHosts(t *testing.T) {
356377
calendar.ClearMockEvents()
357378
})
358379

359-
// TODO(lucas): Use for the test.
360-
webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
361-
require.Equal(t, "POST", r.Method)
362-
requestBodyBytes, err := io.ReadAll(r.Body)
363-
require.NoError(t, err)
364-
t.Logf("webhook request: %s\n", requestBodyBytes)
365-
}))
366-
t.Cleanup(func() {
367-
webhookServer.Close()
368-
})
369-
370380
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
371381
return &fleet.AppConfig{
372382
Integrations: fleet.Integrations{
@@ -395,7 +405,7 @@ func TestCalendarEvents1KHosts(t *testing.T) {
395405
Integrations: fleet.TeamIntegrations{
396406
GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
397407
Enable: true,
398-
WebhookURL: webhookServer.URL,
408+
WebhookURL: "https://foo.example.com",
399409
},
400410
},
401411
},
@@ -406,7 +416,7 @@ func TestCalendarEvents1KHosts(t *testing.T) {
406416
Integrations: fleet.TeamIntegrations{
407417
GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
408418
Enable: true,
409-
WebhookURL: webhookServer.URL,
419+
WebhookURL: "https://foo.example.com",
410420
},
411421
},
412422
},
@@ -417,7 +427,7 @@ func TestCalendarEvents1KHosts(t *testing.T) {
417427
Integrations: fleet.TeamIntegrations{
418428
GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
419429
Enable: true,
420-
WebhookURL: webhookServer.URL,
430+
WebhookURL: "https://foo.example.com",
421431
},
422432
},
423433
},
@@ -428,7 +438,7 @@ func TestCalendarEvents1KHosts(t *testing.T) {
428438
Integrations: fleet.TeamIntegrations{
429439
GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
430440
Enable: true,
431-
WebhookURL: webhookServer.URL,
441+
WebhookURL: "https://foo.example.com",
432442
},
433443
},
434444
},
@@ -439,7 +449,7 @@ func TestCalendarEvents1KHosts(t *testing.T) {
439449
Integrations: fleet.TeamIntegrations{
440450
GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
441451
Enable: true,
442-
WebhookURL: webhookServer.URL,
452+
WebhookURL: "https://foo.example.com",
443453
},
444454
},
445455
},

server/datastore/mysql/calendar_events.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (ds *Datastore) CreateOrUpdateCalendarEvent(
5252
} else {
5353
stmt := `SELECT id FROM calendar_events WHERE email = ?`
5454
if err := sqlx.GetContext(ctx, tx, &id, stmt, email); err != nil {
55-
return ctxerr.Wrap(ctx, err, "query mdm solution id")
55+
return ctxerr.Wrap(ctx, err, "calendar event id")
5656
}
5757
}
5858

server/datastore/mysql/policies.go

-1
Original file line numberDiff line numberDiff line change
@@ -1171,7 +1171,6 @@ func (ds *Datastore) GetCalendarPolicies(ctx context.Context, teamID uint) ([]fl
11711171
return policies, nil
11721172
}
11731173

1174-
// TODO(lucas): Must be tested at scale.
11751174
func (ds *Datastore) GetTeamHostsPolicyMemberships(
11761175
ctx context.Context,
11771176
domain string,

server/fleet/datastore.go

+5
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,11 @@ type Datastore interface {
594594

595595
PolicyQueriesForHost(ctx context.Context, host *Host) (map[string]string, error)
596596

597+
// GetTeamHostsPolicyMembmerships returns the hosts that belong to the given team and their pass/fail statuses
598+
// around the provided policyIDs.
599+
// - Returns hosts of the team that are failing one or more of the provided policies.
600+
// - Returns hosts of the team that are passing all the policies (or are not running any of the provided policies)
601+
// and have a calendar event scheduled.
597602
GetTeamHostsPolicyMemberships(ctx context.Context, domain string, teamID uint, policyIDs []uint) ([]HostPolicyMembershipData, error)
598603
GetCalendarPolicies(ctx context.Context, teamID uint) ([]PolicyCalendarData, error)
599604

0 commit comments

Comments
 (0)