mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Calendar interface (tests and associated fixes) (#17665)
Completed unit tests for Google calendar interface, along with bug fixes. # Checklist for submitter - [ ] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality
This commit is contained in:
parent
21f95d8b5d
commit
712d776be1
@ -111,7 +111,14 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) GetEvent(id, eTag string) (*calend
|
||||
|
||||
func (lowLevelAPI *GoogleCalendarLowLevelAPI) ListEvents(timeMin, timeMax string) (*calendar.Events, error) {
|
||||
// Default maximum number of events returned is 250, which should be sufficient for most calendars.
|
||||
return lowLevelAPI.service.Events.List(calendarID).EventTypes("default").OrderBy("startTime").SingleEvents(true).TimeMin(timeMin).TimeMax(timeMax).Do()
|
||||
return lowLevelAPI.service.Events.List(calendarID).
|
||||
EventTypes("default").
|
||||
OrderBy("startTime").
|
||||
SingleEvents(true).
|
||||
TimeMin(timeMin).
|
||||
TimeMax(timeMax).
|
||||
ShowDeleted(false).
|
||||
Do()
|
||||
}
|
||||
|
||||
func (lowLevelAPI *GoogleCalendarLowLevelAPI) DeleteEvent(id string) error {
|
||||
@ -130,9 +137,7 @@ func (c *GoogleCalendar) Configure(userEmail string) error {
|
||||
}
|
||||
|
||||
func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn func() string) (*fleet.CalendarEvent, bool, error) {
|
||||
if event.EndTime.Before(time.Now()) {
|
||||
return nil, false, ctxerr.Errorf(c.config.Context, "cannot get and update an event that has already ended: %s", event.EndTime)
|
||||
}
|
||||
// We assume that the Fleet event has not already ended. We will simply return it if it has not been modified.
|
||||
details, err := c.unmarshalDetails(event)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
@ -143,6 +148,7 @@ func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn
|
||||
// http.StatusNotModified is returned sometimes, but not always, so we need to check ETag explicitly later
|
||||
case googleapi.IsNotModified(err):
|
||||
return event, false, nil
|
||||
// http.StatusNotFound should be very rare -- Google keeps events for a while after they are deleted
|
||||
case isNotFound(err):
|
||||
deleted = true
|
||||
case err != nil:
|
||||
@ -153,21 +159,50 @@ func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn
|
||||
// Event was not modified
|
||||
return event, false, nil
|
||||
}
|
||||
endTime, err := time.Parse(time.RFC3339, gEvent.End.DateTime)
|
||||
if err != nil {
|
||||
return nil, false, ctxerr.Wrap(
|
||||
c.config.Context, err, fmt.Sprintf("parsing Google calendar event end time: %s", gEvent.End.DateTime),
|
||||
)
|
||||
if gEvent.End == nil || (gEvent.End.DateTime == "" && gEvent.End.Date == "") {
|
||||
// We should not see this error. If we do, we can work around by treating event as deleted.
|
||||
return nil, false, ctxerr.Errorf(c.config.Context, "missing end date/time for Google calendar event: %s", gEvent.Id)
|
||||
}
|
||||
// If event already ended, it is effectively deleted
|
||||
if endTime.After(time.Now()) {
|
||||
startTime, err := time.Parse(time.RFC3339, gEvent.Start.DateTime)
|
||||
|
||||
if gEvent.End.DateTime == "" {
|
||||
// User has modified the event to be an all-day event. All-day events are problematic because they depend on the user's timezone.
|
||||
// We won't handle all-day events at this time, and treat the event as deleted.
|
||||
deleted = true
|
||||
}
|
||||
|
||||
var endTime *time.Time
|
||||
if !deleted {
|
||||
endTime, err = c.parseDateTime(gEvent.End)
|
||||
if err != nil {
|
||||
return nil, false, ctxerr.Wrap(
|
||||
c.config.Context, err, fmt.Sprintf("parsing Google calendar event start time: %s", gEvent.Start.DateTime),
|
||||
)
|
||||
return nil, false, err
|
||||
}
|
||||
fleetEvent, err := c.googleEventToFleetEvent(startTime, endTime, gEvent)
|
||||
if !endTime.After(time.Now()) {
|
||||
// If event already ended, it is effectively deleted
|
||||
// Delete this event to prevent confusion. This operation should be rare.
|
||||
err = c.DeleteEvent(event)
|
||||
if err != nil {
|
||||
level.Warn(c.config.Logger).Log("msg", "deleting Google calendar event which is in the past", "err", err)
|
||||
}
|
||||
deleted = true
|
||||
}
|
||||
}
|
||||
if !deleted {
|
||||
if gEvent.Start == nil || (gEvent.Start.DateTime == "" && gEvent.Start.Date == "") {
|
||||
// We should not see this error. If we do, we can work around by treating event as deleted.
|
||||
return nil, false, ctxerr.Errorf(c.config.Context, "missing start date/time for Google calendar event: %s", gEvent.Id)
|
||||
}
|
||||
if gEvent.Start.DateTime == "" {
|
||||
// User has modified the event to be an all-day event. All-day events are problematic because they depend on the user's timezone.
|
||||
// We won't handle all-day events at this time, and treat the event as deleted.
|
||||
deleted = true
|
||||
}
|
||||
}
|
||||
if !deleted {
|
||||
startTime, err := c.parseDateTime(gEvent.Start)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
fleetEvent, err := c.googleEventToFleetEvent(*startTime, *endTime, gEvent)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@ -175,12 +210,7 @@ func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn
|
||||
}
|
||||
}
|
||||
|
||||
newStartDate := event.StartTime.Add(24 * time.Hour)
|
||||
if newStartDate.Weekday() == time.Saturday {
|
||||
newStartDate = newStartDate.Add(48 * time.Hour)
|
||||
} else if newStartDate.Weekday() == time.Sunday {
|
||||
newStartDate = newStartDate.Add(24 * time.Hour)
|
||||
}
|
||||
newStartDate := calculateNewEventDate(event.StartTime)
|
||||
|
||||
fleetEvent, err := c.CreateEvent(newStartDate, genBodyFn())
|
||||
if err != nil {
|
||||
@ -189,6 +219,34 @@ func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn
|
||||
return fleetEvent, true, nil
|
||||
}
|
||||
|
||||
func calculateNewEventDate(oldStartDate time.Time) time.Time {
|
||||
// Note: we do not handle time changes (daylight savings time, etc.) -- assuming 1 day is always 24 hours.
|
||||
newStartDate := oldStartDate.Add(24 * time.Hour)
|
||||
if newStartDate.Weekday() == time.Saturday {
|
||||
newStartDate = newStartDate.Add(48 * time.Hour)
|
||||
} else if newStartDate.Weekday() == time.Sunday {
|
||||
newStartDate = newStartDate.Add(24 * time.Hour)
|
||||
}
|
||||
return newStartDate
|
||||
}
|
||||
|
||||
func (c *GoogleCalendar) parseDateTime(eventDateTime *calendar.EventDateTime) (*time.Time, error) {
|
||||
var endTime time.Time
|
||||
var err error
|
||||
if eventDateTime.TimeZone != "" {
|
||||
loc := getLocation(eventDateTime.TimeZone, c.config)
|
||||
endTime, err = time.ParseInLocation(time.RFC3339, eventDateTime.DateTime, loc)
|
||||
} else {
|
||||
endTime, err = time.Parse(time.RFC3339, eventDateTime.DateTime)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(
|
||||
c.config.Context, err, fmt.Sprintf("parsing Google calendar event time: %s", eventDateTime.DateTime),
|
||||
)
|
||||
}
|
||||
return &endTime, nil
|
||||
}
|
||||
|
||||
func isNotFound(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
@ -212,6 +270,12 @@ func (c *GoogleCalendar) unmarshalDetails(event *fleet.CalendarEvent) (*eventDet
|
||||
}
|
||||
|
||||
func (c *GoogleCalendar) CreateEvent(dayOfEvent time.Time, body string) (*fleet.CalendarEvent, error) {
|
||||
return c.createEvent(dayOfEvent, body, time.Now)
|
||||
}
|
||||
|
||||
// createEvent creates a new event on the calendar on the given date. timeNow is a function that returns the current time.
|
||||
// timeNow can be overwritten for testing
|
||||
func (c *GoogleCalendar) createEvent(dayOfEvent time.Time, body string, timeNow func() time.Time) (*fleet.CalendarEvent, error) {
|
||||
if c.timezoneOffset == nil {
|
||||
err := getTimezone(c)
|
||||
if err != nil {
|
||||
@ -223,27 +287,26 @@ func (c *GoogleCalendar) CreateEvent(dayOfEvent time.Time, body string) (*fleet.
|
||||
dayStart := time.Date(dayOfEvent.Year(), dayOfEvent.Month(), dayOfEvent.Day(), startHour, 0, 0, 0, location)
|
||||
dayEnd := time.Date(dayOfEvent.Year(), dayOfEvent.Month(), dayOfEvent.Day(), endHour, 0, 0, 0, location)
|
||||
|
||||
now := time.Now().In(location)
|
||||
now := timeNow().In(location)
|
||||
if dayEnd.Before(now) {
|
||||
// The workday has already ended.
|
||||
return nil, ctxerr.Wrap(c.config.Context, fleet.DayEndedError{Msg: "cannot schedule an event for a day that has already ended"})
|
||||
}
|
||||
|
||||
// Adjust day start if workday already started
|
||||
if dayStart.Before(now) {
|
||||
if !dayStart.After(now) {
|
||||
dayStart = now.Truncate(eventLength)
|
||||
if dayStart.Before(now) {
|
||||
dayStart = dayStart.Add(eventLength)
|
||||
}
|
||||
if dayStart.Equal(dayEnd) {
|
||||
if !dayStart.Before(dayEnd) {
|
||||
return nil, ctxerr.Wrap(c.config.Context, fleet.DayEndedError{Msg: "no time available for event"})
|
||||
}
|
||||
}
|
||||
eventStart := dayStart
|
||||
eventEnd := dayStart.Add(eventLength)
|
||||
|
||||
searchStart := dayStart.Add(-24 * time.Hour)
|
||||
events, err := c.config.API.ListEvents(searchStart.Format(time.RFC3339), dayEnd.Format(time.RFC3339))
|
||||
events, err := c.config.API.ListEvents(dayStart.Format(time.RFC3339), dayEnd.Format(time.RFC3339))
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(c.config.Context, err, "listing Google calendar events")
|
||||
}
|
||||
@ -253,48 +316,44 @@ func (c *GoogleCalendar) CreateEvent(dayOfEvent time.Time, body string) (*fleet.
|
||||
continue
|
||||
}
|
||||
|
||||
// Ignore all day events
|
||||
if gEvent.Start == nil || gEvent.Start.DateTime == "" || gEvent.End == nil || gEvent.End.DateTime == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Ignore events that the user has declined
|
||||
var attending bool
|
||||
if len(gEvent.Attendees) == 0 {
|
||||
// No attendees, so we assume the user is attending
|
||||
attending = true
|
||||
} else {
|
||||
for _, attendee := range gEvent.Attendees {
|
||||
if attendee.Email == c.currentUserEmail {
|
||||
if attendee.ResponseStatus != "declined" {
|
||||
attending = true
|
||||
}
|
||||
var declined bool
|
||||
for _, attendee := range gEvent.Attendees {
|
||||
if attendee.Email == c.currentUserEmail {
|
||||
// The user has declined the event, so this time is open for scheduling
|
||||
if attendee.ResponseStatus == "declined" {
|
||||
declined = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !attending {
|
||||
if declined {
|
||||
continue
|
||||
}
|
||||
|
||||
// Ignore events that will end before our event
|
||||
endTime, err := time.Parse(time.RFC3339, gEvent.End.DateTime)
|
||||
endTime, err := c.parseDateTime(gEvent.End)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(
|
||||
c.config.Context, err, fmt.Sprintf("parsing Google calendar event end time: %s", gEvent.End.DateTime),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
if endTime.Before(eventStart) || endTime.Equal(eventStart) {
|
||||
if !endTime.After(eventStart) {
|
||||
continue
|
||||
}
|
||||
|
||||
startTime, err := time.Parse(time.RFC3339, gEvent.Start.DateTime)
|
||||
startTime, err := c.parseDateTime(gEvent.Start)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(
|
||||
c.config.Context, err, fmt.Sprintf("parsing Google calendar event start time: %s", gEvent.Start.DateTime),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if startTime.Before(eventEnd) {
|
||||
// Event occurs during our event, so we need to adjust.
|
||||
fmt.Printf("VICTOR Adjusting event times due to %s: %s - %s\n", gEvent.Summary, eventStart, eventEnd)
|
||||
var isLastSlot bool
|
||||
eventStart, eventEnd, isLastSlot = adjustEventTimes(endTime, dayEnd)
|
||||
eventStart, eventEnd, isLastSlot = adjustEventTimes(*endTime, dayEnd)
|
||||
if isLastSlot {
|
||||
break
|
||||
}
|
||||
@ -349,17 +408,22 @@ func getTimezone(gCal *GoogleCalendar) error {
|
||||
return ctxerr.Wrap(config.Context, err, "retrieving Google calendar timezone")
|
||||
}
|
||||
|
||||
loc, err := time.LoadLocation(setting.Value)
|
||||
if err != nil {
|
||||
// Could not load location, use EST
|
||||
level.Warn(config.Logger).Log("msg", "parsing Google calendar timezone", "timezone", setting.Value, "err", err)
|
||||
loc, _ = time.LoadLocation("America/New_York")
|
||||
}
|
||||
loc := getLocation(setting.Value, config)
|
||||
_, timezoneOffset := time.Now().In(loc).Zone()
|
||||
gCal.timezoneOffset = &timezoneOffset
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLocation(name string, config *GoogleCalendarConfig) *time.Location {
|
||||
loc, err := time.LoadLocation(name)
|
||||
if err != nil {
|
||||
// Could not load location, use EST
|
||||
level.Warn(config.Logger).Log("msg", "parsing Google calendar timezone", "timezone", name, "err", err)
|
||||
loc, _ = time.LoadLocation("America/New_York")
|
||||
}
|
||||
return loc
|
||||
}
|
||||
|
||||
func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime time.Time, event *calendar.Event) (
|
||||
*fleet.CalendarEvent, error,
|
||||
) {
|
||||
|
589
ee/server/calendar/google_calendar_test.go
Normal file
589
ee/server/calendar/google_calendar_test.go
Normal file
@ -0,0 +1,589 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/go-kit/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 (
|
||||
baseServiceEmail = "service@example.com"
|
||||
basePrivateKey = "private-key"
|
||||
baseUserEmail = "user@example.com"
|
||||
)
|
||||
|
||||
var (
|
||||
baseCtx = context.Background()
|
||||
logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout))
|
||||
)
|
||||
|
||||
type MockGoogleCalendarLowLevelAPI struct {
|
||||
ConfigureFunc func(ctx context.Context, serviceAccountEmail, privateKey, userToImpersonateEmail string) error
|
||||
GetSettingFunc func(name string) (*calendar.Setting, error)
|
||||
ListEventsFunc func(timeMin, timeMax string) (*calendar.Events, error)
|
||||
CreateEventFunc func(event *calendar.Event) (*calendar.Event, error)
|
||||
GetEventFunc func(id, eTag string) (*calendar.Event, error)
|
||||
DeleteEventFunc func(id string) error
|
||||
}
|
||||
|
||||
func (m *MockGoogleCalendarLowLevelAPI) Configure(
|
||||
ctx context.Context, serviceAccountEmail, privateKey, userToImpersonateEmail string,
|
||||
) error {
|
||||
return m.ConfigureFunc(ctx, serviceAccountEmail, privateKey, userToImpersonateEmail)
|
||||
}
|
||||
|
||||
func (m *MockGoogleCalendarLowLevelAPI) GetSetting(name string) (*calendar.Setting, error) {
|
||||
return m.GetSettingFunc(name)
|
||||
}
|
||||
|
||||
func (m *MockGoogleCalendarLowLevelAPI) ListEvents(timeMin, timeMax string) (*calendar.Events, error) {
|
||||
return m.ListEventsFunc(timeMin, timeMax)
|
||||
}
|
||||
|
||||
func (m *MockGoogleCalendarLowLevelAPI) CreateEvent(event *calendar.Event) (*calendar.Event, error) {
|
||||
return m.CreateEventFunc(event)
|
||||
}
|
||||
|
||||
func (m *MockGoogleCalendarLowLevelAPI) GetEvent(id, eTag string) (*calendar.Event, error) {
|
||||
return m.GetEventFunc(id, eTag)
|
||||
}
|
||||
|
||||
func (m *MockGoogleCalendarLowLevelAPI) DeleteEvent(id string) error {
|
||||
return m.DeleteEventFunc(id)
|
||||
}
|
||||
|
||||
func TestGoogleCalendar_Configure(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockAPI := &MockGoogleCalendarLowLevelAPI{}
|
||||
mockAPI.ConfigureFunc = func(ctx context.Context, serviceAccountEmail, privateKey, userToImpersonateEmail string) error {
|
||||
assert.Equal(t, baseCtx, ctx)
|
||||
assert.Equal(t, baseServiceEmail, serviceAccountEmail)
|
||||
assert.Equal(t, basePrivateKey, privateKey)
|
||||
assert.Equal(t, baseUserEmail, userToImpersonateEmail)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Happy path test
|
||||
var cal fleet.UserCalendar = NewGoogleCalendar(makeConfig(mockAPI))
|
||||
err := cal.Configure(baseUserEmail)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Configure error test
|
||||
mockAPI.ConfigureFunc = func(ctx context.Context, serviceAccountEmail, privateKey, userToImpersonateEmail string) error {
|
||||
return assert.AnError
|
||||
}
|
||||
err = cal.Configure(baseUserEmail)
|
||||
assert.ErrorIs(t, err, assert.AnError)
|
||||
}
|
||||
|
||||
func makeConfig(mockAPI *MockGoogleCalendarLowLevelAPI) *GoogleCalendarConfig {
|
||||
if mockAPI != nil && mockAPI.ConfigureFunc == nil {
|
||||
mockAPI.ConfigureFunc = func(ctx context.Context, serviceAccountEmail, privateKey, userToImpersonateEmail string) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
config := &GoogleCalendarConfig{
|
||||
Context: context.Background(),
|
||||
IntegrationConfig: &fleet.GoogleCalendarIntegration{
|
||||
Email: baseServiceEmail,
|
||||
PrivateKey: basePrivateKey,
|
||||
},
|
||||
Logger: logger,
|
||||
API: mockAPI,
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func TestGoogleCalendar_DeleteEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockAPI := &MockGoogleCalendarLowLevelAPI{}
|
||||
mockAPI.DeleteEventFunc = func(id string) error {
|
||||
assert.Equal(t, "event-id", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Happy path test
|
||||
var cal fleet.UserCalendar = NewGoogleCalendar(makeConfig(mockAPI))
|
||||
err := cal.Configure(baseUserEmail)
|
||||
assert.NoError(t, err)
|
||||
err = cal.DeleteEvent(&fleet.CalendarEvent{Data: []byte(`{"ID":"event-id"}`)})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// API error test
|
||||
mockAPI.DeleteEventFunc = func(id string) error {
|
||||
return assert.AnError
|
||||
}
|
||||
err = cal.DeleteEvent(&fleet.CalendarEvent{Data: []byte(`{"ID":"event-id"}`)})
|
||||
assert.ErrorIs(t, err, assert.AnError)
|
||||
}
|
||||
|
||||
func TestGoogleCalendar_unmarshalDetails(t *testing.T) {
|
||||
t.Parallel()
|
||||
var gCal = NewGoogleCalendar(makeConfig(&MockGoogleCalendarLowLevelAPI{}))
|
||||
err := gCal.Configure(baseUserEmail)
|
||||
assert.NoError(t, err)
|
||||
details, err := gCal.unmarshalDetails(&fleet.CalendarEvent{Data: []byte(`{"id":"event-id","etag":"event-eTag"}`)})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "event-id", details.ID)
|
||||
assert.Equal(t, "event-eTag", details.ETag)
|
||||
|
||||
// Missing ETag is OK
|
||||
details, err = gCal.unmarshalDetails(&fleet.CalendarEvent{Data: []byte(`{"id":"event-id"}`)})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "event-id", details.ID)
|
||||
assert.Equal(t, "", details.ETag)
|
||||
|
||||
// Bad JSON
|
||||
_, err = gCal.unmarshalDetails(&fleet.CalendarEvent{Data: []byte(`{"bozo`)})
|
||||
assert.Error(t, err)
|
||||
|
||||
// Missing id
|
||||
_, err = gCal.unmarshalDetails(&fleet.CalendarEvent{Data: []byte(`{"myId":"event-id","etag":"event-eTag"}`)})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockAPI := &MockGoogleCalendarLowLevelAPI{}
|
||||
const baseETag = "event-eTag"
|
||||
const baseEventID = "event-id"
|
||||
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
|
||||
assert.Equal(t, baseEventID, id)
|
||||
assert.Equal(t, baseETag, eTag)
|
||||
return &calendar.Event{
|
||||
Etag: baseETag, // ETag matches -- no modifications to event
|
||||
}, nil
|
||||
}
|
||||
genBodyFn := func() string {
|
||||
t.Error("genBodyFn should not be called")
|
||||
return "event-body"
|
||||
}
|
||||
var cal fleet.UserCalendar = NewGoogleCalendar(makeConfig(mockAPI))
|
||||
err := cal.Configure(baseUserEmail)
|
||||
assert.NoError(t, err)
|
||||
|
||||
eventStartTime := time.Now().UTC()
|
||||
event := &fleet.CalendarEvent{
|
||||
StartTime: eventStartTime,
|
||||
EndTime: time.Now().Add(time.Hour),
|
||||
Data: []byte(`{"ID":"` + baseEventID + `","ETag":"` + baseETag + `"}`),
|
||||
}
|
||||
|
||||
// ETag matches
|
||||
retrievedEvent, updated, err := cal.GetAndUpdateEvent(event, genBodyFn)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, updated)
|
||||
assert.Equal(t, event, retrievedEvent)
|
||||
|
||||
// http.StatusNotModified response (ETag matches)
|
||||
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
|
||||
return nil, &googleapi.Error{Code: http.StatusNotModified}
|
||||
}
|
||||
retrievedEvent, updated, err = cal.GetAndUpdateEvent(event, genBodyFn)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, updated)
|
||||
assert.Equal(t, event, retrievedEvent)
|
||||
|
||||
// Cannot unmarshal details
|
||||
eventBadDetails := &fleet.CalendarEvent{
|
||||
StartTime: time.Now(),
|
||||
EndTime: time.Now().Add(time.Hour),
|
||||
Data: []byte(`{"bozo`),
|
||||
}
|
||||
_, _, err = cal.GetAndUpdateEvent(eventBadDetails, genBodyFn)
|
||||
assert.Error(t, err)
|
||||
|
||||
// API error test
|
||||
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
_, _, err = cal.GetAndUpdateEvent(event, genBodyFn)
|
||||
assert.ErrorIs(t, err, assert.AnError)
|
||||
|
||||
// Event has been modified
|
||||
startTime := time.Now().Add(time.Minute).Truncate(time.Second)
|
||||
endTime := time.Now().Add(time.Hour).Truncate(time.Second)
|
||||
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
|
||||
return &calendar.Event{
|
||||
Id: baseEventID,
|
||||
Etag: "new-eTag",
|
||||
Start: &calendar.EventDateTime{DateTime: startTime.Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: endTime.Format(time.RFC3339)},
|
||||
}, nil
|
||||
}
|
||||
retrievedEvent, updated, err = cal.GetAndUpdateEvent(event, genBodyFn)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
assert.NotEqual(t, event, retrievedEvent)
|
||||
require.NotNil(t, retrievedEvent)
|
||||
assert.Equal(t, startTime.UTC(), retrievedEvent.StartTime.UTC())
|
||||
assert.Equal(t, endTime.UTC(), retrievedEvent.EndTime.UTC())
|
||||
assert.Equal(t, baseUserEmail, retrievedEvent.Email)
|
||||
gCal, _ := cal.(*GoogleCalendar)
|
||||
details, err := gCal.unmarshalDetails(retrievedEvent)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "new-eTag", details.ETag)
|
||||
assert.Equal(t, baseEventID, details.ID)
|
||||
|
||||
// missing end time
|
||||
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
|
||||
return &calendar.Event{
|
||||
Id: baseEventID,
|
||||
Etag: "new-eTag",
|
||||
Start: &calendar.EventDateTime{DateTime: startTime.Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: ""},
|
||||
}, nil
|
||||
}
|
||||
_, _, err = cal.GetAndUpdateEvent(event, genBodyFn)
|
||||
assert.Error(t, err)
|
||||
|
||||
// missing start time
|
||||
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
|
||||
return &calendar.Event{
|
||||
Id: baseEventID,
|
||||
Etag: "new-eTag",
|
||||
End: &calendar.EventDateTime{DateTime: endTime.Format(time.RFC3339)},
|
||||
}, nil
|
||||
}
|
||||
_, _, err = cal.GetAndUpdateEvent(event, genBodyFn)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Bad time format
|
||||
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
|
||||
return &calendar.Event{
|
||||
Id: baseEventID,
|
||||
Etag: "new-eTag",
|
||||
Start: &calendar.EventDateTime{DateTime: startTime.Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: "bozo"},
|
||||
}, nil
|
||||
}
|
||||
_, _, err = cal.GetAndUpdateEvent(event, genBodyFn)
|
||||
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)
|
||||
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},
|
||||
}, nil
|
||||
}
|
||||
retrievedEvent, updated, err = cal.GetAndUpdateEvent(event, genBodyFn)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
assert.NotEqual(t, event, retrievedEvent)
|
||||
require.NotNil(t, retrievedEvent)
|
||||
assert.Equal(t, startTime.UTC(), retrievedEvent.StartTime.UTC())
|
||||
assert.Equal(t, endTime.UTC(), retrievedEvent.EndTime.UTC())
|
||||
assert.Equal(t, baseUserEmail, retrievedEvent.Email)
|
||||
|
||||
// 404 response (deleted)
|
||||
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
|
||||
}
|
||||
genBodyFn = func() string {
|
||||
return "event-body"
|
||||
}
|
||||
eventCreated := false
|
||||
mockAPI.CreateEventFunc = func(event *calendar.Event) (*calendar.Event, error) {
|
||||
assert.Equal(t, eventTitle, event.Summary)
|
||||
assert.Equal(t, genBodyFn(), event.Description)
|
||||
event.Id = baseEventID
|
||||
event.Etag = baseETag
|
||||
eventCreated = true
|
||||
return event, nil
|
||||
}
|
||||
retrievedEvent, updated, err = cal.GetAndUpdateEvent(event, genBodyFn)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
assert.NotEqual(t, event, retrievedEvent)
|
||||
require.NotNil(t, retrievedEvent)
|
||||
assert.Equal(t, baseUserEmail, retrievedEvent.Email)
|
||||
newEventDate := calculateNewEventDate(eventStartTime)
|
||||
expectedStartTime := time.Date(newEventDate.Year(), newEventDate.Month(), newEventDate.Day(), startHour, 0, 0, 0, time.UTC)
|
||||
assert.Equal(t, expectedStartTime.UTC(), retrievedEvent.StartTime.UTC())
|
||||
assert.Equal(t, expectedStartTime.Add(eventLength).UTC(), retrievedEvent.EndTime.UTC())
|
||||
assert.True(t, eventCreated)
|
||||
|
||||
// cancelled (deleted)
|
||||
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
|
||||
return &calendar.Event{
|
||||
Id: baseEventID,
|
||||
Etag: "new-eTag",
|
||||
Start: &calendar.EventDateTime{DateTime: startTime.Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: endTime.Format(time.RFC3339)},
|
||||
Status: "cancelled",
|
||||
}, nil
|
||||
}
|
||||
eventCreated = false
|
||||
retrievedEvent, updated, err = cal.GetAndUpdateEvent(event, genBodyFn)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
require.NotNil(t, retrievedEvent)
|
||||
assert.NotEqual(t, event, retrievedEvent)
|
||||
assert.Equal(t, expectedStartTime.UTC(), retrievedEvent.StartTime.UTC())
|
||||
assert.Equal(t, expectedStartTime.Add(eventLength).UTC(), retrievedEvent.EndTime.UTC())
|
||||
assert.True(t, eventCreated)
|
||||
|
||||
// all day event (deleted)
|
||||
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
|
||||
return &calendar.Event{
|
||||
Id: baseEventID,
|
||||
Etag: "new-eTag",
|
||||
Start: &calendar.EventDateTime{Date: startTime.Format("2006-01-02")},
|
||||
End: &calendar.EventDateTime{DateTime: endTime.Format(time.RFC3339)},
|
||||
}, nil
|
||||
}
|
||||
eventCreated = false
|
||||
retrievedEvent, updated, err = cal.GetAndUpdateEvent(event, genBodyFn)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
require.NotNil(t, retrievedEvent)
|
||||
assert.NotEqual(t, event, retrievedEvent)
|
||||
assert.Equal(t, expectedStartTime.UTC(), retrievedEvent.StartTime.UTC())
|
||||
assert.Equal(t, expectedStartTime.Add(eventLength).UTC(), retrievedEvent.EndTime.UTC())
|
||||
assert.True(t, eventCreated)
|
||||
|
||||
// moved in the past event (deleted)
|
||||
mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) {
|
||||
return &calendar.Event{
|
||||
Id: baseEventID,
|
||||
Etag: "new-eTag",
|
||||
Start: &calendar.EventDateTime{DateTime: startTime.Add(-2 * time.Hour).Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: endTime.Add(-2 * time.Hour).Format(time.RFC3339)},
|
||||
}, nil
|
||||
}
|
||||
eventCreated = false
|
||||
mockAPI.DeleteEventFunc = func(id string) error {
|
||||
assert.Equal(t, baseEventID, id)
|
||||
return nil
|
||||
}
|
||||
retrievedEvent, updated, err = cal.GetAndUpdateEvent(event, genBodyFn)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
require.NotNil(t, retrievedEvent)
|
||||
assert.NotEqual(t, event, retrievedEvent)
|
||||
assert.Equal(t, expectedStartTime.UTC(), retrievedEvent.StartTime.UTC())
|
||||
assert.Equal(t, expectedStartTime.Add(eventLength).UTC(), retrievedEvent.EndTime.UTC())
|
||||
assert.True(t, eventCreated)
|
||||
}
|
||||
|
||||
func TestGoogleCalendar_CreateEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockAPI := &MockGoogleCalendarLowLevelAPI{}
|
||||
const baseEventID = "event-id"
|
||||
const baseETag = "event-eTag"
|
||||
const eventBody = "event-body"
|
||||
var cal fleet.UserCalendar = NewGoogleCalendar(makeConfig(mockAPI))
|
||||
err := cal.Configure(baseUserEmail)
|
||||
assert.NoError(t, err)
|
||||
|
||||
tzId := "Africa/Kinshasa"
|
||||
mockAPI.GetSettingFunc = func(name string) (*calendar.Setting, error) {
|
||||
return &calendar.Setting{Value: tzId}, nil
|
||||
}
|
||||
mockAPI.ListEventsFunc = func(timeMin, timeMax string) (*calendar.Events, error) {
|
||||
return &calendar.Events{}, nil
|
||||
}
|
||||
mockAPI.CreateEventFunc = func(event *calendar.Event) (*calendar.Event, error) {
|
||||
assert.Equal(t, eventTitle, event.Summary)
|
||||
assert.Equal(t, eventBody, event.Description)
|
||||
event.Id = baseEventID
|
||||
event.Etag = baseETag
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// Happy path test -- empty calendar
|
||||
date := time.Now().Add(48 * time.Hour)
|
||||
location, _ := time.LoadLocation(tzId)
|
||||
expectedStartTime := time.Date(date.Year(), date.Month(), date.Day(), startHour, 0, 0, 0, location)
|
||||
_, expectedOffset := expectedStartTime.Zone()
|
||||
event, err := cal.CreateEvent(date, eventBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, baseUserEmail, event.Email)
|
||||
assert.Equal(t, expectedStartTime.UTC(), event.StartTime.UTC())
|
||||
assert.Equal(t, expectedStartTime.Add(eventLength).UTC(), event.EndTime.UTC())
|
||||
_, offset := event.StartTime.Zone()
|
||||
assert.Equal(t, expectedOffset, offset)
|
||||
_, offset = event.EndTime.Zone()
|
||||
assert.Equal(t, expectedOffset, offset)
|
||||
gCal, _ := cal.(*GoogleCalendar)
|
||||
details, err := gCal.unmarshalDetails(event)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, baseETag, details.ETag)
|
||||
assert.Equal(t, baseEventID, details.ID)
|
||||
|
||||
// Workday already ended
|
||||
date = time.Now().Add(-48 * time.Hour)
|
||||
_, err = cal.CreateEvent(date, eventBody)
|
||||
assert.ErrorAs(t, err, &fleet.DayEndedError{})
|
||||
|
||||
// There is no time left in the day to schedule an event
|
||||
date = time.Now().Add(48 * time.Hour)
|
||||
timeNow := func() time.Time {
|
||||
now := time.Date(date.Year(), date.Month(), date.Day(), endHour-1, 45, 0, 0, location)
|
||||
return now
|
||||
}
|
||||
_, err = gCal.createEvent(date, eventBody, timeNow)
|
||||
assert.ErrorAs(t, err, &fleet.DayEndedError{})
|
||||
|
||||
// Workday already started
|
||||
date = time.Now().Add(48 * time.Hour)
|
||||
expectedStartTime = time.Date(date.Year(), date.Month(), date.Day(), endHour-1, 30, 0, 0, location)
|
||||
timeNow = func() time.Time {
|
||||
return expectedStartTime
|
||||
}
|
||||
event, err = gCal.createEvent(date, eventBody, timeNow)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedStartTime.UTC(), event.StartTime.UTC())
|
||||
assert.Equal(t, expectedStartTime.Add(eventLength).UTC(), event.EndTime.UTC())
|
||||
|
||||
// Busy calendar
|
||||
date = time.Now().Add(48 * time.Hour)
|
||||
dayStart := time.Date(date.Year(), date.Month(), date.Day(), startHour, 0, 0, 0, location)
|
||||
dayEnd := time.Date(date.Year(), date.Month(), date.Day(), endHour, 0, 0, 0, location)
|
||||
gEvents := &calendar.Events{}
|
||||
// Cancelled event
|
||||
gEvent := &calendar.Event{
|
||||
Id: "cancelled-event-id",
|
||||
Start: &calendar.EventDateTime{DateTime: dayStart.Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: dayEnd.Format(time.RFC3339)},
|
||||
Status: "cancelled",
|
||||
}
|
||||
gEvents.Items = append(gEvents.Items, gEvent)
|
||||
// All day events
|
||||
gEvent = &calendar.Event{
|
||||
Id: "all-day-event-id",
|
||||
Start: &calendar.EventDateTime{Date: dayStart.Format(time.DateOnly)},
|
||||
End: &calendar.EventDateTime{DateTime: dayEnd.Format(time.RFC3339)},
|
||||
}
|
||||
gEvents.Items = append(gEvents.Items, gEvent)
|
||||
gEvent = &calendar.Event{
|
||||
Id: "all-day2-event-id",
|
||||
Start: &calendar.EventDateTime{DateTime: dayStart.Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{Date: dayEnd.Format(time.DateOnly)},
|
||||
}
|
||||
gEvents.Items = append(gEvents.Items, gEvent)
|
||||
// User-declined event
|
||||
gEvent = &calendar.Event{
|
||||
Id: "user-declined-event-id",
|
||||
Start: &calendar.EventDateTime{DateTime: dayStart.Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: dayEnd.Format(time.RFC3339)},
|
||||
Attendees: []*calendar.EventAttendee{{Email: baseUserEmail, ResponseStatus: "declined"}},
|
||||
}
|
||||
gEvents.Items = append(gEvents.Items, gEvent)
|
||||
// Event before day
|
||||
gEvent = &calendar.Event{
|
||||
Id: "before-event-id",
|
||||
Start: &calendar.EventDateTime{DateTime: dayStart.Add(-time.Hour).Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: dayStart.Add(-30 * time.Minute).Format(time.RFC3339)},
|
||||
}
|
||||
gEvents.Items = append(gEvents.Items, gEvent)
|
||||
|
||||
// Event from 6am to 11am
|
||||
eventStart := time.Date(date.Year(), date.Month(), date.Day(), 6, 0, 0, 0, location)
|
||||
eventEnd := time.Date(date.Year(), date.Month(), date.Day(), 11, 0, 0, 0, location)
|
||||
gEvent = &calendar.Event{
|
||||
Id: "6-to-11-event-id",
|
||||
Start: &calendar.EventDateTime{DateTime: eventStart.Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: eventEnd.Format(time.RFC3339)},
|
||||
Attendees: []*calendar.EventAttendee{{Email: baseUserEmail, ResponseStatus: "accepted"}},
|
||||
}
|
||||
gEvents.Items = append(gEvents.Items, gEvent)
|
||||
|
||||
// Event from 10am to 10:30am
|
||||
eventStart = time.Date(date.Year(), date.Month(), date.Day(), 10, 0, 0, 0, location)
|
||||
eventEnd = time.Date(date.Year(), date.Month(), date.Day(), 10, 30, 0, 0, location)
|
||||
gEvent = &calendar.Event{
|
||||
Id: "10-to-10-30-event-id",
|
||||
Start: &calendar.EventDateTime{DateTime: eventStart.Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: eventEnd.Format(time.RFC3339)},
|
||||
Attendees: []*calendar.EventAttendee{{Email: "other@example.com", ResponseStatus: "accepted"}},
|
||||
}
|
||||
gEvents.Items = append(gEvents.Items, gEvent)
|
||||
// Event from 11am to 11:45am
|
||||
eventStart = time.Date(date.Year(), date.Month(), date.Day(), 11, 0, 0, 0, location)
|
||||
eventEnd = time.Date(date.Year(), date.Month(), date.Day(), 11, 45, 0, 0, location)
|
||||
gEvent = &calendar.Event{
|
||||
Id: "11-to-11-45-event-id",
|
||||
Start: &calendar.EventDateTime{DateTime: eventStart.Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: eventEnd.Format(time.RFC3339)},
|
||||
Attendees: []*calendar.EventAttendee{{Email: "other@example.com", ResponseStatus: "accepted"}},
|
||||
}
|
||||
gEvents.Items = append(gEvents.Items, gEvent)
|
||||
|
||||
// Event after day
|
||||
eventStart = time.Date(date.Year(), date.Month(), date.Day(), endHour, 0, 0, 0, location)
|
||||
eventEnd = time.Date(date.Year(), date.Month(), date.Day(), endHour, 45, 0, 0, location)
|
||||
gEvent = &calendar.Event{
|
||||
Id: "after-event-id",
|
||||
Start: &calendar.EventDateTime{DateTime: eventStart.Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: eventEnd.Format(time.RFC3339)},
|
||||
Attendees: []*calendar.EventAttendee{{Email: "other@example.com", ResponseStatus: "accepted"}},
|
||||
}
|
||||
gEvents.Items = append(gEvents.Items, gEvent)
|
||||
mockAPI.ListEventsFunc = func(timeMin, timeMax string) (*calendar.Events, error) {
|
||||
return gEvents, nil
|
||||
}
|
||||
expectedStartTime = time.Date(date.Year(), date.Month(), date.Day(), 12, 0, 0, 0, location)
|
||||
event, err = gCal.CreateEvent(date, eventBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedStartTime.UTC(), event.StartTime.UTC())
|
||||
assert.Equal(t, expectedStartTime.Add(eventLength).UTC(), event.EndTime.UTC())
|
||||
|
||||
// Full schedule -- pick the last slot
|
||||
date = time.Now().Add(48 * time.Hour)
|
||||
dayStart = time.Date(date.Year(), date.Month(), date.Day(), startHour, 0, 0, 0, location)
|
||||
dayEnd = time.Date(date.Year(), date.Month(), date.Day(), endHour, 0, 0, 0, location)
|
||||
gEvents = &calendar.Events{}
|
||||
gEvent = &calendar.Event{
|
||||
Id: "9-to-5-event-id",
|
||||
Start: &calendar.EventDateTime{DateTime: dayStart.Format(time.RFC3339)},
|
||||
End: &calendar.EventDateTime{DateTime: dayEnd.Format(time.RFC3339)},
|
||||
}
|
||||
gEvents.Items = append(gEvents.Items, gEvent)
|
||||
mockAPI.ListEventsFunc = func(timeMin, timeMax string) (*calendar.Events, error) {
|
||||
return gEvents, nil
|
||||
}
|
||||
expectedStartTime = time.Date(date.Year(), date.Month(), date.Day(), endHour-1, 30, 0, 0, location)
|
||||
event, err = gCal.CreateEvent(date, eventBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedStartTime.UTC(), event.StartTime.UTC())
|
||||
assert.Equal(t, expectedStartTime.Add(eventLength).UTC(), event.EndTime.UTC())
|
||||
|
||||
// API error in ListEvents
|
||||
mockAPI.ListEventsFunc = func(timeMin, timeMax string) (*calendar.Events, error) {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
_, err = gCal.CreateEvent(date, eventBody)
|
||||
assert.ErrorIs(t, err, assert.AnError)
|
||||
|
||||
// API error in CreateEvent
|
||||
mockAPI.ListEventsFunc = func(timeMin, timeMax string) (*calendar.Events, error) {
|
||||
return &calendar.Events{}, nil
|
||||
}
|
||||
mockAPI.CreateEventFunc = func(event *calendar.Event) (*calendar.Event, error) {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
_, err = gCal.CreateEvent(date, eventBody)
|
||||
assert.ErrorIs(t, err, assert.AnError)
|
||||
}
|
Loading…
Reference in New Issue
Block a user