Include timezone write when updating events; Write updated gcal timezone if only event change (#20435)

## Addresses #20431 

https://www.loom.com/share/0d88eceb8fb44ef3bec70d2b0dc7479c?sid=350bb4c2-2abe-4b80-b99f-ef6c8109efac

- Include timezone write when updating events
- Write updated gcal timezone to Fleet events, even if it's the only
change
- Have frontend handle `"UTC"` being set as timezone as if it were `nil`
- Small cleanups
 
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
jacobshandling 2024-07-16 13:27:33 -07:00 committed by GitHub
parent 686d6292d8
commit 22a9eb7d60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 63 additions and 35 deletions

View file

@ -261,7 +261,7 @@ func (c *GoogleCalendar) Configure(userEmail string) error {
return nil
}
func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn func(conflict bool) (body string, ok bool, err error)) (
func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn func(conflict bool) (body string, updated bool, err error)) (
*fleet.CalendarEvent, bool, error,
) {
// We assume that the Fleet event has not already ended. We will simply return it if it has not been modified.
@ -269,11 +269,26 @@ func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn
if err != nil {
return nil, false, err
}
// Set current calendar instance timezone to the latest from google calendar.
c.location, err = getTimezone(c)
if err != nil {
return nil, false, err
}
tzUpdated := c.location.String() != event.TimeZone
gEvent, err := c.config.API.GetEvent(details.ID, details.ETag)
var deleted, channelStopped bool
switch {
// http.StatusNotModified is returned sometimes, but not always, so we need to check ETag explicitly later
case googleapi.IsNotModified(err):
if tzUpdated {
// this condition occurs when the event itself hasn't been updated, but the calendar timezone
// has been, so update the Fleet event's timezone
event.TimeZone = c.location.String()
return event, true, nil
}
return event, false, nil
// http.StatusNotFound should be very rare -- Google keeps events for a while after they are deleted
case isNotFound(err):
@ -284,6 +299,12 @@ func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn
if !deleted && gEvent.Status != "cancelled" {
if details.ETag != "" && details.ETag == gEvent.Etag {
// Event was not modified
if tzUpdated {
// this condition occurs when the event itself hasn't been updated, but the calendar timezone
// has been, so just update the event's timezone
event.TimeZone = c.location.String()
return event, true, nil
}
return event, false, nil
}
if gEvent.End == nil || (gEvent.End.DateTime == "" && gEvent.End.Date == "") {
@ -593,12 +614,12 @@ func adjustEventTimes(endTime time.Time, dayEnd time.Time) (eventStart time.Time
func getTimezone(gCal *GoogleCalendar) (location *time.Location, err error) {
config := gCal.config
// "The ID of the users timezone." https://developers.google.com/calendar/api/v3/reference/settings
tz, err := config.API.GetSetting("timezone")
gCalTz, err := config.API.GetSetting("timezone")
if err != nil {
return nil, ctxerr.Wrap(config.Context, err, "retrieving Google calendar timezone")
}
return getLocation(tz.Value, config), nil
return getLocation(gCalTz.Value, config), nil
}
func getLocation(tz string, config *GoogleCalendarConfig) *time.Location {
@ -616,6 +637,7 @@ func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime ti
resourceID string) (
*fleet.CalendarEvent, error,
) {
fleetEvent := &fleet.CalendarEvent{}
fleetEvent.StartTime = startTime
fleetEvent.EndTime = endTime

View file

@ -2,16 +2,17 @@ package calendar
import (
"context"
"net/http"
"os"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/go-kit/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/api/calendar/v3"
"google.golang.org/api/googleapi"
"net/http"
"os"
"testing"
"time"
)
const (
@ -202,6 +203,8 @@ func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) {
const baseETag = "event-eTag"
const baseEventID = "event-id"
const baseResourceID = "resource-id"
const baseTzName = "America/New_York"
baseTzLocation, _ := time.LoadLocation(baseTzName)
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
assert.Equal(t, baseEventID, id)
assert.Equal(t, baseETag, eTag)
@ -209,6 +212,9 @@ func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) {
Etag: baseETag, // ETag matches -- no modifications to event
}, nil
}
mockAPI.GetSettingFunc = func(name string) (*calendar.Setting, error) {
return &calendar.Setting{Value: baseTzName}, nil
}
genBodyFn := func(bool) (string, bool, error) {
t.Error("genBodyFn should not be called")
return "event-body", false, nil
@ -217,11 +223,12 @@ func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) {
err := cal.Configure(baseUserEmail)
assert.NoError(t, err)
eventStartTime := time.Now().UTC()
eventStartTime := time.Now().In(baseTzLocation)
event := &fleet.CalendarEvent{
StartTime: eventStartTime,
EndTime: time.Now().Add(time.Hour),
EndTime: time.Now().Add(time.Hour).In(baseTzLocation),
Data: []byte(`{"ID":"` + baseEventID + `","ETag":"` + baseETag + `"}`),
TimeZone: baseTzName,
}
// ETag matches
@ -316,16 +323,19 @@ func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) {
assert.Error(t, err)
// Event has been modified, with custom timezone.
tzId := "Africa/Kinshasa"
location, _ := time.LoadLocation(tzId)
startTime = time.Now().Add(time.Minute).Truncate(time.Second).In(location)
endTime = time.Now().Add(time.Hour).Truncate(time.Second).In(location)
newTzName := "Africa/Kinshasa"
newTzLocation, _ := time.LoadLocation(newTzName)
mockAPI.GetSettingFunc = func(name string) (*calendar.Setting, error) {
return &calendar.Setting{Value: newTzName}, nil
}
startTime = time.Now().Add(time.Minute).Truncate(time.Second).In(newTzLocation)
endTime = time.Now().Add(time.Hour).Truncate(time.Second).In(newTzLocation)
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
return &calendar.Event{
Id: baseEventID,
Etag: "new-eTag",
Start: &calendar.EventDateTime{DateTime: startTime.UTC().Format(time.RFC3339), TimeZone: tzId},
End: &calendar.EventDateTime{DateTime: endTime.Format(time.RFC3339), TimeZone: tzId},
Start: &calendar.EventDateTime{DateTime: startTime.UTC().Format(time.RFC3339), TimeZone: newTzName},
End: &calendar.EventDateTime{DateTime: endTime.Format(time.RFC3339), TimeZone: newTzName},
}, nil
}
retrievedEvent, updated, err = cal.GetAndUpdateEvent(event, genBodyFn)
@ -341,9 +351,6 @@ func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) {
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
return nil, &googleapi.Error{Code: http.StatusNotFound}
}
mockAPI.GetSettingFunc = func(name string) (*calendar.Setting, error) {
return &calendar.Setting{Value: "UTC"}, nil
}
mockAPI.ListEventsFunc = func(timeMin, timeMax string) (*calendar.Events, error) {
return &calendar.Events{}, nil
}
@ -383,7 +390,7 @@ func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) {
assert.Equal(t, uuid, retrievedEvent.UUID)
assert.Equal(t, baseUserEmail, retrievedEvent.Email)
newEventDate := calculateNewEventDate(eventStartTime)
expectedStartTime := time.Date(newEventDate.Year(), newEventDate.Month(), newEventDate.Day(), startHour, 0, 0, 0, time.UTC)
expectedStartTime := time.Date(newEventDate.Year(), newEventDate.Month(), newEventDate.Day(), startHour, 0, 0, 0, newTzLocation)
assert.Equal(t, expectedStartTime.UTC(), retrievedEvent.StartTime.UTC())
assert.Equal(t, expectedStartTime.Add(eventLength).UTC(), retrievedEvent.EndTime.UTC())
assert.True(t, eventCreated)

View file

@ -660,7 +660,6 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
isSoftwareEnabled={isSoftwareEnabled}
software={software}
teamId={currentTeamId}
pageIndex={softwarePageIndex}
navTabIndex={softwareNavTabIndex}
onTabChange={onSoftwareTabChange}
onQueryChange={onSoftwareQueryChange}

View file

@ -23,7 +23,6 @@ interface ISoftwareCardProps {
isSoftwareEnabled?: boolean;
software?: ISoftwareResponse;
teamId?: number;
pageIndex: number;
navTabIndex: number;
onTabChange: (index: number, last: number, event: Event) => boolean | void;
onQueryChange?:

View file

@ -364,19 +364,20 @@ const HostSummary = ({
DATE_FNS_FORMAT_STRINGS.dateAtTime
);
const tip = timezone ? (
<>
End user&apos;s time zone:
<br />
(GMT{starts_at.slice(-6)}) {timezone.replace("_", " ")}
</>
) : (
<>
End user&apos;s timezone unavailable.
<br />
Displaying in UTC.
</>
);
const tip =
timezone && timezone !== "UTC" ? (
<>
End user&apos;s time zone:
<br />
(GMT{starts_at.slice(-6)}) {timezone.replace("_", " ")}
</>
) : (
<>
End user&apos;s timezone unavailable.
<br />
Displaying in UTC.
</>
);
return (
<DataSet

View file

@ -745,7 +745,7 @@ type HostMaintenanceWindow struct {
// StartsAt is the start time of the future maintenance window, retrieved from calendar_events,
// represented as a time.Time in the host's associated google calendar user's timezone, which is represented as a time.Location
StartsAt time.Time `json:"starts_at" db:"start_time"`
// TimeZone is the IANA timezone of the user's google calendar, retrieved from calendar_evants
// TimeZone is the IANA timezone of the user's google calendar, retrieved from calendar_events
TimeZone *string `json:"timezone" db:"timezone"`
}