fleet/ee/server/calendar/google_calendar_test.go
Victor Lyuboslavsky 16f122f02a
Adding calendar test server and other fixes. (#17751)
- Added a calendar server that can be used for load testing at
/tools/calendar
- Fixed minor calendar bugs

# Checklist for submitter
- [ ] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
2024-03-26 13:39:37 -05:00

650 lines
24 KiB
Go

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 TestGoogleCalendar_ConfigurePlusAddressing(t *testing.T) {
// Do not run this test in t.Parallel(), since it involves modifying a global variable
plusAddressing = true
t.Cleanup(
func() {
plusAddressing = false
},
)
email := "user+my_test+email@example.com"
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, "user@example.com", userToImpersonateEmail)
return nil
}
var cal fleet.UserCalendar = NewGoogleCalendar(makeConfig(mockAPI))
err := cal.Configure(email)
assert.NoError(t, err)
}
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{
ApiKey: map[string]string{
fleet.GoogleCalendarEmail: baseServiceEmail,
fleet.GoogleCalendarPrivateKey: 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)
// Event already deleted
mockAPI.DeleteEventFunc = func(id string) error {
return &googleapi.Error{Code: http.StatusGone}
}
err = cal.DeleteEvent(&fleet.CalendarEvent{Data: []byte(`{"ID":"event-id"}`)})
assert.NoError(t, err)
}
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(bool) 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(conflict bool) string {
assert.False(t, conflict)
return "event-body"
}
eventCreated := false
mockAPI.CreateEventFunc = func(event *calendar.Event) (*calendar.Event, error) {
assert.Equal(t, eventTitle, event.Summary)
assert.Equal(t, genBodyFn(false), 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.DeleteEventFunc = func(id string) error {
assert.Equal(t, baseEventID, id)
return nil
}
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
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
}
genBodyFn := func(conflict bool) string {
assert.False(t, conflict)
return eventBody
}
genBodyConflictFn := func(conflict bool) string {
assert.True(t, conflict)
return eventBody
}
// 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, genBodyFn)
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, genBodyFn)
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, genBodyFn, 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, genBodyFn, 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, genBodyFn)
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, genBodyConflictFn)
require.NoError(t, err)
assert.Equal(t, expectedStartTime.UTC(), event.StartTime.UTC())
assert.Equal(t, expectedStartTime.Add(eventLength).UTC(), event.EndTime.UTC())
// Almost 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-1, 30, 0, 0, location)
gEvents = &calendar.Events{}
gEvent = &calendar.Event{
Id: "9-to-4-30-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 = dayEnd
event, err = gCal.CreateEvent(date, genBodyFn)
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, genBodyFn)
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, genBodyFn)
assert.ErrorIs(t, err, assert.AnError)
}