mirror of
https://github.com/fleetdm/fleet
synced 2026-05-22 16:39:01 +00:00
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
This commit is contained in:
parent
2940b32a06
commit
16f122f02a
8 changed files with 882 additions and 39 deletions
|
|
@ -6,6 +6,8 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
|
|
@ -19,18 +21,30 @@ import (
|
|||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
// The calendar package has the following features for testing:
|
||||
// 1. High level UserCalendar interface and Low level GoogleCalendarAPI interface can have a custom implementations.
|
||||
// 2. Setting "client_email" to "calendar-mock@example.com" in the API key will use a mock in-memory implementation GoogleCalendarMockAPI of GoogleCalendarAPI.
|
||||
// 3. Setting FLEET_GOOGLE_CALENDAR_PLUS_ADDRESSING environment variable to "1" will strip the "plus addressing" from the user email, effectively allowing a single user
|
||||
// to create multiple events in the same calendar. This is useful for load testing. For example: john+test@example.com becomes john@example.com
|
||||
|
||||
const (
|
||||
eventTitle = "💻🚫Downtime"
|
||||
startHour = 9
|
||||
endHour = 17
|
||||
eventLength = 30 * time.Minute
|
||||
calendarID = "primary"
|
||||
mockEmail = "calendar-mock@example.com"
|
||||
loadEmail = "calendar-load@example.com"
|
||||
)
|
||||
|
||||
var calendarScopes = []string{
|
||||
"https://www.googleapis.com/auth/calendar.events",
|
||||
"https://www.googleapis.com/auth/calendar.settings.readonly",
|
||||
}
|
||||
var (
|
||||
calendarScopes = []string{
|
||||
"https://www.googleapis.com/auth/calendar.events",
|
||||
"https://www.googleapis.com/auth/calendar.settings.readonly",
|
||||
}
|
||||
plusAddressing = os.Getenv("FLEET_GOOGLE_CALENDAR_PLUS_ADDRESSING") == "1"
|
||||
plusAddressingRegex = regexp.MustCompile(`\+.*@`)
|
||||
)
|
||||
|
||||
type GoogleCalendarConfig struct {
|
||||
Context context.Context
|
||||
|
|
@ -43,19 +57,22 @@ type GoogleCalendarConfig struct {
|
|||
// GoogleCalendar is an implementation of the UserCalendar interface that uses the
|
||||
// Google Calendar API to manage events.
|
||||
type GoogleCalendar struct {
|
||||
config *GoogleCalendarConfig
|
||||
currentUserEmail string
|
||||
timezoneOffset *int
|
||||
config *GoogleCalendarConfig
|
||||
currentUserEmail string
|
||||
adjustedUserEmail string
|
||||
location *time.Location
|
||||
}
|
||||
|
||||
func NewGoogleCalendar(config *GoogleCalendarConfig) *GoogleCalendar {
|
||||
if config.API == nil {
|
||||
if config.IntegrationConfig.ApiKey[fleet.GoogleCalendarEmail] == "calendar-mock@example.com" {
|
||||
// Assumes that only 1 Fleet server accesses the calendar, since all mock events are held in memory
|
||||
config.API = &GoogleCalendarMockAPI{}
|
||||
} else {
|
||||
config.API = &GoogleCalendarLowLevelAPI{}
|
||||
}
|
||||
switch {
|
||||
case config.API != nil:
|
||||
// Use the provided API.
|
||||
case config.IntegrationConfig.ApiKey[fleet.GoogleCalendarEmail] == loadEmail:
|
||||
config.API = &GoogleCalendarLoadAPI{Logger: config.Logger}
|
||||
case config.IntegrationConfig.ApiKey[fleet.GoogleCalendarEmail] == mockEmail:
|
||||
config.API = &GoogleCalendarMockAPI{config.Logger}
|
||||
default:
|
||||
config.API = &GoogleCalendarLowLevelAPI{}
|
||||
}
|
||||
return &GoogleCalendar{
|
||||
config: config,
|
||||
|
|
@ -101,6 +118,13 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) Configure(
|
|||
return nil
|
||||
}
|
||||
|
||||
func adjustEmail(email string) string {
|
||||
if plusAddressing {
|
||||
return plusAddressingRegex.ReplaceAllString(email, "@")
|
||||
}
|
||||
return email
|
||||
}
|
||||
|
||||
func (lowLevelAPI *GoogleCalendarLowLevelAPI) GetSetting(name string) (*calendar.Setting, error) {
|
||||
return lowLevelAPI.service.Settings.Get(name).Do()
|
||||
}
|
||||
|
|
@ -130,14 +154,18 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) DeleteEvent(id string) error {
|
|||
}
|
||||
|
||||
func (c *GoogleCalendar) Configure(userEmail string) error {
|
||||
adjustedUserEmail := adjustEmail(userEmail)
|
||||
err := c.config.API.Configure(
|
||||
c.config.Context, c.config.IntegrationConfig.ApiKey[fleet.GoogleCalendarEmail],
|
||||
c.config.IntegrationConfig.ApiKey[fleet.GoogleCalendarPrivateKey], userEmail,
|
||||
c.config.IntegrationConfig.ApiKey[fleet.GoogleCalendarPrivateKey], adjustedUserEmail,
|
||||
)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(c.config.Context, err, "creating Google calendar service")
|
||||
}
|
||||
c.currentUserEmail = userEmail
|
||||
c.adjustedUserEmail = adjustedUserEmail
|
||||
// Clear the timezone offset so that it will be recalculated
|
||||
c.location = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +190,7 @@ func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn
|
|||
return nil, false, ctxerr.Wrap(c.config.Context, err, "retrieving Google calendar event")
|
||||
}
|
||||
if !deleted && gEvent.Status != "cancelled" {
|
||||
if details.ETag == gEvent.Etag {
|
||||
if details.ETag != "" && details.ETag == gEvent.Etag {
|
||||
// Event was not modified
|
||||
return event, false, nil
|
||||
}
|
||||
|
|
@ -246,20 +274,20 @@ func calculateNewEventDate(oldStartDate time.Time) time.Time {
|
|||
}
|
||||
|
||||
func (c *GoogleCalendar) parseDateTime(eventDateTime *calendar.EventDateTime) (*time.Time, error) {
|
||||
var endTime time.Time
|
||||
var t time.Time
|
||||
var err error
|
||||
if eventDateTime.TimeZone != "" {
|
||||
loc := getLocation(eventDateTime.TimeZone, c.config)
|
||||
endTime, err = time.ParseInLocation(time.RFC3339, eventDateTime.DateTime, loc)
|
||||
t, err = time.ParseInLocation(time.RFC3339, eventDateTime.DateTime, loc)
|
||||
} else {
|
||||
endTime, err = time.Parse(time.RFC3339, eventDateTime.DateTime)
|
||||
t, 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
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func isNotFound(err error) bool {
|
||||
|
|
@ -271,6 +299,15 @@ func isNotFound(err error) bool {
|
|||
return ok && ae.Code == http.StatusNotFound
|
||||
}
|
||||
|
||||
func isAlreadyDeleted(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var ae *googleapi.Error
|
||||
ok := errors.As(err, &ae)
|
||||
return ok && ae.Code == http.StatusGone
|
||||
}
|
||||
|
||||
func (c *GoogleCalendar) unmarshalDetails(event *fleet.CalendarEvent) (*eventDetails, error) {
|
||||
var details eventDetails
|
||||
err := json.Unmarshal(event.Data, &details)
|
||||
|
|
@ -293,18 +330,18 @@ func (c *GoogleCalendar) CreateEvent(dayOfEvent time.Time, genBodyFn func(confli
|
|||
func (c *GoogleCalendar) createEvent(
|
||||
dayOfEvent time.Time, genBodyFn func(conflict bool) string, timeNow func() time.Time,
|
||||
) (*fleet.CalendarEvent, error) {
|
||||
if c.timezoneOffset == nil {
|
||||
err := getTimezone(c)
|
||||
var err error
|
||||
if c.location == nil {
|
||||
c.location, err = getTimezone(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
location := time.FixedZone("", *c.timezoneOffset)
|
||||
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)
|
||||
dayStart := time.Date(dayOfEvent.Year(), dayOfEvent.Month(), dayOfEvent.Day(), startHour, 0, 0, 0, c.location)
|
||||
dayEnd := time.Date(dayOfEvent.Year(), dayOfEvent.Month(), dayOfEvent.Day(), endHour, 0, 0, 0, c.location)
|
||||
|
||||
now := timeNow().In(location)
|
||||
now := timeNow().In(c.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"})
|
||||
|
|
@ -342,7 +379,7 @@ func (c *GoogleCalendar) createEvent(
|
|||
// Ignore events that the user has declined
|
||||
var declined bool
|
||||
for _, attendee := range gEvent.Attendees {
|
||||
if attendee.Email == c.currentUserEmail {
|
||||
if attendee.Email == c.adjustedUserEmail {
|
||||
// The user has declined the event, so this time is open for scheduling
|
||||
if attendee.ResponseStatus == "declined" {
|
||||
declined = true
|
||||
|
|
@ -396,7 +433,9 @@ func (c *GoogleCalendar) createEvent(
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
level.Debug(c.config.Logger).Log("msg", "created Google calendar event", "user", c.currentUserEmail, "startTime", eventStart)
|
||||
level.Debug(c.config.Logger).Log(
|
||||
"msg", "created Google calendar event", "user", c.adjustedUserEmail, "startTime", eventStart, "timezone", c.location.String(),
|
||||
)
|
||||
|
||||
return fleetEvent, nil
|
||||
}
|
||||
|
|
@ -419,17 +458,14 @@ func adjustEventTimes(endTime time.Time, dayEnd time.Time) (eventStart time.Time
|
|||
return eventStart, eventEnd, isLastSlot, conflict
|
||||
}
|
||||
|
||||
func getTimezone(gCal *GoogleCalendar) error {
|
||||
func getTimezone(gCal *GoogleCalendar) (*time.Location, error) {
|
||||
config := gCal.config
|
||||
setting, err := config.API.GetSetting("timezone")
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(config.Context, err, "retrieving Google calendar timezone")
|
||||
return nil, ctxerr.Wrap(config.Context, err, "retrieving Google calendar timezone")
|
||||
}
|
||||
|
||||
loc := getLocation(setting.Value, config)
|
||||
_, timezoneOffset := time.Now().In(loc).Zone()
|
||||
gCal.timezoneOffset = &timezoneOffset
|
||||
return nil
|
||||
return getLocation(setting.Value, config), nil
|
||||
}
|
||||
|
||||
func getLocation(name string, config *GoogleCalendarConfig) *time.Location {
|
||||
|
|
@ -467,7 +503,10 @@ func (c *GoogleCalendar) DeleteEvent(event *fleet.CalendarEvent) error {
|
|||
return err
|
||||
}
|
||||
err = c.config.API.DeleteEvent(details.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case isAlreadyDeleted(err):
|
||||
return nil
|
||||
case err != nil:
|
||||
return ctxerr.Wrap(c.config.Context, err, "deleting Google calendar event")
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
132
ee/server/calendar/google_calendar_integration_test.go
Normal file
132
ee/server/calendar/google_calendar_integration_test.go
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package calendar
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/calendar/load_test"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type googleCalendarIntegrationTestSuite struct {
|
||||
suite.Suite
|
||||
server *httptest.Server
|
||||
dbFile *os.File
|
||||
}
|
||||
|
||||
func (s *googleCalendarIntegrationTestSuite) SetupSuite() {
|
||||
dbFile, err := os.CreateTemp("", "calendar.db")
|
||||
s.Require().NoError(err)
|
||||
handler, err := calendartest.Configure(dbFile.Name())
|
||||
s.Require().NoError(err)
|
||||
server := httptest.NewUnstartedServer(handler)
|
||||
server.Listener.Addr()
|
||||
server.Start()
|
||||
s.server = server
|
||||
}
|
||||
|
||||
func (s *googleCalendarIntegrationTestSuite) TearDownSuite() {
|
||||
if s.dbFile != nil {
|
||||
s.dbFile.Close()
|
||||
_ = os.Remove(s.dbFile.Name())
|
||||
}
|
||||
if s.server != nil {
|
||||
s.server.Close()
|
||||
}
|
||||
calendartest.Close()
|
||||
}
|
||||
|
||||
// TestGoogleCalendarIntegration tests should be able to be run in parallel, but this is not natively supported by suites: https://github.com/stretchr/testify/issues/187
|
||||
// There are workarounds that can be explored.
|
||||
func TestGoogleCalendarIntegration(t *testing.T) {
|
||||
testingSuite := new(googleCalendarIntegrationTestSuite)
|
||||
suite.Run(t, testingSuite)
|
||||
}
|
||||
|
||||
func (s *googleCalendarIntegrationTestSuite) TestCreateGetDeleteEvent() {
|
||||
t := s.T()
|
||||
userEmail := "user1@example.com"
|
||||
config := &GoogleCalendarConfig{
|
||||
Context: context.Background(),
|
||||
IntegrationConfig: &fleet.GoogleCalendarIntegration{
|
||||
Domain: "example.com",
|
||||
ApiKey: map[string]string{
|
||||
"client_email": loadEmail,
|
||||
"private_key": s.server.URL,
|
||||
},
|
||||
},
|
||||
Logger: kitlog.NewLogfmtLogger(kitlog.NewSyncWriter(os.Stdout)),
|
||||
}
|
||||
gCal := NewGoogleCalendar(config)
|
||||
err := gCal.Configure(userEmail)
|
||||
require.NoError(t, err)
|
||||
genBodyFn := func(bool) string {
|
||||
return "Test event"
|
||||
}
|
||||
eventDate := time.Now().Add(48 * time.Hour)
|
||||
event, err := gCal.CreateEvent(eventDate, genBodyFn)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, startHour, event.StartTime.Hour())
|
||||
assert.Equal(t, 0, event.StartTime.Minute())
|
||||
|
||||
eventRsp, updated, err := gCal.GetAndUpdateEvent(event, genBodyFn)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updated)
|
||||
assert.Equal(t, event, eventRsp)
|
||||
|
||||
err = gCal.DeleteEvent(event)
|
||||
assert.NoError(t, err)
|
||||
// delete again
|
||||
err = gCal.DeleteEvent(event)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try to get deleted event
|
||||
eventRsp, updated, err = gCal.GetAndUpdateEvent(event, genBodyFn)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
assert.NotEqual(t, event.StartTime.UTC().Truncate(24*time.Hour), eventRsp.StartTime.UTC().Truncate(24*time.Hour))
|
||||
}
|
||||
|
||||
func (s *googleCalendarIntegrationTestSuite) TestFillUpCalendar() {
|
||||
t := s.T()
|
||||
userEmail := "user2@example.com"
|
||||
config := &GoogleCalendarConfig{
|
||||
Context: context.Background(),
|
||||
IntegrationConfig: &fleet.GoogleCalendarIntegration{
|
||||
Domain: "example.com",
|
||||
ApiKey: map[string]string{
|
||||
"client_email": loadEmail,
|
||||
"private_key": s.server.URL,
|
||||
},
|
||||
},
|
||||
Logger: kitlog.NewLogfmtLogger(kitlog.NewSyncWriter(os.Stdout)),
|
||||
}
|
||||
gCal := NewGoogleCalendar(config)
|
||||
err := gCal.Configure(userEmail)
|
||||
require.NoError(t, err)
|
||||
genBodyFn := func(bool) string {
|
||||
return "Test event"
|
||||
}
|
||||
eventDate := time.Now().Add(48 * time.Hour)
|
||||
event, err := gCal.CreateEvent(eventDate, genBodyFn)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, startHour, event.StartTime.Hour())
|
||||
assert.Equal(t, 0, event.StartTime.Minute())
|
||||
|
||||
currentEventTime := event.StartTime
|
||||
for i := 0; i < 20; i++ {
|
||||
if !(currentEventTime.Hour() == endHour-1 && currentEventTime.Minute() == 30) {
|
||||
currentEventTime = currentEventTime.Add(30 * time.Minute)
|
||||
}
|
||||
event, err = gCal.CreateEvent(eventDate, genBodyFn)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, currentEventTime.UTC(), event.StartTime.UTC())
|
||||
}
|
||||
|
||||
}
|
||||
234
ee/server/calendar/google_calendar_load.go
Normal file
234
ee/server/calendar/google_calendar_load.go
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
package calendar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"google.golang.org/api/calendar/v3"
|
||||
"google.golang.org/api/googleapi"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
// GoogleCalendarLoadAPI is used for load testing.
|
||||
type GoogleCalendarLoadAPI struct {
|
||||
Logger kitlog.Logger
|
||||
baseUrl string
|
||||
userToImpersonate string
|
||||
ctx context.Context
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// Configure creates a new Google Calendar service using the provided credentials.
|
||||
func (lowLevelAPI *GoogleCalendarLoadAPI) Configure(ctx context.Context, _ string, privateKey string, userToImpersonate string) error {
|
||||
if lowLevelAPI.Logger == nil {
|
||||
lowLevelAPI.Logger = kitlog.With(kitlog.NewLogfmtLogger(os.Stderr), "mock", "GoogleCalendarLoadAPI", "user", userToImpersonate)
|
||||
}
|
||||
lowLevelAPI.baseUrl = privateKey
|
||||
lowLevelAPI.userToImpersonate = userToImpersonate
|
||||
lowLevelAPI.ctx = ctx
|
||||
if lowLevelAPI.client == nil {
|
||||
lowLevelAPI.client = fleethttp.NewClient()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lowLevelAPI *GoogleCalendarLoadAPI) GetSetting(name string) (*calendar.Setting, error) {
|
||||
reqUrl, err := url.Parse(lowLevelAPI.baseUrl + "/settings")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := reqUrl.Query()
|
||||
query.Set("name", name)
|
||||
query.Set("email", lowLevelAPI.userToImpersonate)
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
req, err := http.NewRequestWithContext(lowLevelAPI.ctx, "GET", reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsp, err := lowLevelAPI.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = rsp.Body.Close()
|
||||
}()
|
||||
if rsp.StatusCode != http.StatusOK {
|
||||
var data []byte
|
||||
if rsp.Body != nil {
|
||||
data, _ = io.ReadAll(rsp.Body)
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected status code: %d with body: %s", rsp.StatusCode, string(data))
|
||||
}
|
||||
var setting calendar.Setting
|
||||
body, err := io.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(body, &setting)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &setting, nil
|
||||
}
|
||||
|
||||
func (lowLevelAPI *GoogleCalendarLoadAPI) CreateEvent(event *calendar.Event) (*calendar.Event, error) {
|
||||
body, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqUrl, err := url.Parse(lowLevelAPI.baseUrl + "/events/add")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := reqUrl.Query()
|
||||
query.Set("email", lowLevelAPI.userToImpersonate)
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
req, err := http.NewRequestWithContext(lowLevelAPI.ctx, "POST", reqUrl.String(), bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rsp, err := lowLevelAPI.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = rsp.Body.Close()
|
||||
}()
|
||||
if rsp.StatusCode != http.StatusCreated {
|
||||
var data []byte
|
||||
if rsp.Body != nil {
|
||||
data, _ = io.ReadAll(rsp.Body)
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected status code: %d with body: %s", rsp.StatusCode, string(data))
|
||||
}
|
||||
var rspEvent calendar.Event
|
||||
body, err = io.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(body, &rspEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rspEvent, nil
|
||||
}
|
||||
|
||||
func (lowLevelAPI *GoogleCalendarLoadAPI) GetEvent(id, _ string) (*calendar.Event, error) {
|
||||
reqUrl, err := url.Parse(lowLevelAPI.baseUrl + "/events")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := reqUrl.Query()
|
||||
query.Set("id", id)
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
req, err := http.NewRequestWithContext(lowLevelAPI.ctx, "GET", reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsp, err := lowLevelAPI.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = rsp.Body.Close()
|
||||
}()
|
||||
if rsp.StatusCode == http.StatusNotFound {
|
||||
return nil, &googleapi.Error{Code: http.StatusNotFound}
|
||||
}
|
||||
if rsp.StatusCode != http.StatusOK {
|
||||
var data []byte
|
||||
if rsp.Body != nil {
|
||||
data, _ = io.ReadAll(rsp.Body)
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected status code: %d with body: %s", rsp.StatusCode, string(data))
|
||||
}
|
||||
var rspEvent calendar.Event
|
||||
body, err := io.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(body, &rspEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rspEvent, nil
|
||||
}
|
||||
|
||||
func (lowLevelAPI *GoogleCalendarLoadAPI) ListEvents(timeMin string, timeMax string) (*calendar.Events, error) {
|
||||
reqUrl, err := url.Parse(lowLevelAPI.baseUrl + "/events/list")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := reqUrl.Query()
|
||||
query.Set("timemin", timeMin)
|
||||
query.Set("timemax", timeMax)
|
||||
query.Set("email", lowLevelAPI.userToImpersonate)
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
req, err := http.NewRequestWithContext(lowLevelAPI.ctx, "GET", reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsp, err := lowLevelAPI.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = rsp.Body.Close()
|
||||
}()
|
||||
if rsp.StatusCode != http.StatusOK {
|
||||
var data []byte
|
||||
if rsp.Body != nil {
|
||||
data, _ = io.ReadAll(rsp.Body)
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected status code: %d with body: %s", rsp.StatusCode, string(data))
|
||||
}
|
||||
var events calendar.Events
|
||||
body, err := io.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(body, &events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &events, nil
|
||||
}
|
||||
|
||||
func (lowLevelAPI *GoogleCalendarLoadAPI) DeleteEvent(id string) error {
|
||||
reqUrl, err := url.Parse(lowLevelAPI.baseUrl + "/events/delete")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query := reqUrl.Query()
|
||||
query.Set("id", id)
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
req, err := http.NewRequestWithContext(lowLevelAPI.ctx, "DELETE", reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rsp, err := lowLevelAPI.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = rsp.Body.Close()
|
||||
}()
|
||||
if rsp.StatusCode == http.StatusGone {
|
||||
return &googleapi.Error{Code: http.StatusGone}
|
||||
}
|
||||
if rsp.StatusCode != http.StatusOK {
|
||||
var data []byte
|
||||
if rsp.Body != nil {
|
||||
data, _ = io.ReadAll(rsp.Body)
|
||||
}
|
||||
return fmt.Errorf("unexpected status code: %d with body: %s", rsp.StatusCode, string(data))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ type GoogleCalendarMockAPI struct {
|
|||
logger kitlog.Logger
|
||||
}
|
||||
|
||||
var events = make(map[string]*calendar.Event)
|
||||
var mockEvents = make(map[string]*calendar.Event)
|
||||
var mu sync.Mutex
|
||||
var id uint64
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ func (lowLevelAPI *GoogleCalendarMockAPI) CreateEvent(event *calendar.Event) (*c
|
|||
id += 1
|
||||
event.Id = strconv.FormatUint(id, 10)
|
||||
lowLevelAPI.logger.Log("msg", "CreateEvent", "id", event.Id, "start", event.Start.DateTime)
|
||||
events[event.Id] = event
|
||||
mockEvents[event.Id] = event
|
||||
return event, nil
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ func (lowLevelAPI *GoogleCalendarMockAPI) GetEvent(id, _ string) (*calendar.Even
|
|||
time.Sleep(latency)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
event, ok := events[id]
|
||||
event, ok := mockEvents[id]
|
||||
if !ok {
|
||||
return nil, &googleapi.Error{Code: http.StatusNotFound}
|
||||
}
|
||||
|
|
@ -76,6 +76,6 @@ func (lowLevelAPI *GoogleCalendarMockAPI) DeleteEvent(id string) error {
|
|||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
lowLevelAPI.logger.Log("msg", "DeleteEvent", "id", id)
|
||||
delete(events, id)
|
||||
delete(mockEvents, id)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,29 @@ func TestGoogleCalendar_Configure(t *testing.T) {
|
|||
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 {
|
||||
|
|
@ -125,6 +148,13 @@ func TestGoogleCalendar_DeleteEvent(t *testing.T) {
|
|||
}
|
||||
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) {
|
||||
|
|
|
|||
343
ee/server/calendar/load_test/calendar_http_handler.go
Normal file
343
ee/server/calendar/load_test/calendar_http_handler.go
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
// Package calendartest is not imported in production code, so it will not be compiled for Fleet server.
|
||||
package calendartest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5" //nolint:gosec // (only used in testing)
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"google.golang.org/api/calendar/v3"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This calendar does not support all-day events.
|
||||
|
||||
var db *sql.DB
|
||||
var timezones = []string{
|
||||
"America/Chicago",
|
||||
"America/New_York",
|
||||
"America/Los_Angeles",
|
||||
"America/Anchorage",
|
||||
"Pacific/Honolulu",
|
||||
"America/Argentina/Buenos_Aires",
|
||||
"Asia/Kolkata",
|
||||
"Europe/London",
|
||||
"Europe/Paris",
|
||||
"Australia/Sydney",
|
||||
}
|
||||
|
||||
func Configure(dbPath string) (http.Handler, error) {
|
||||
var err error
|
||||
db, err = sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout, "", log.LstdFlags)
|
||||
logger.Println("Server is starting...")
|
||||
|
||||
// Initialize the database schema if needed
|
||||
err = initializeSchema()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("/settings", getSetting)
|
||||
router.HandleFunc("/events", getEvent)
|
||||
router.HandleFunc("/events/list", getEvents)
|
||||
router.HandleFunc("/events/add", addEvent)
|
||||
router.HandleFunc("/events/delete", deleteEvent)
|
||||
return logging(logger)(router), nil
|
||||
}
|
||||
|
||||
func logging(logger *log.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
logger.Println(r.Method, r.URL.String(), r.RemoteAddr)
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func Close() {
|
||||
_ = db.Close()
|
||||
}
|
||||
|
||||
func getSetting(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Query().Get("name")
|
||||
if name == "" {
|
||||
http.Error(w, "missing name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if name != "timezone" {
|
||||
http.Error(w, "unsupported setting", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
email := r.URL.Query().Get("email")
|
||||
if email == "" {
|
||||
http.Error(w, "missing email", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
timezone := getTimezone(email)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
setting := calendar.Setting{Value: timezone}
|
||||
err := json.NewEncoder(w).Encode(setting)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// The timezone is determined by the user's email address
|
||||
func getTimezone(email string) string {
|
||||
index := hash(email) % uint32(len(timezones))
|
||||
timezone := timezones[index]
|
||||
return timezone
|
||||
}
|
||||
|
||||
func hash(s string) uint32 {
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(s))
|
||||
return h.Sum32()
|
||||
}
|
||||
|
||||
// getEvent handles GET /events?id=123
|
||||
func getEvent(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
http.Error(w, "missing id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sqlStmt := "SELECT email, start, end, summary, description, status FROM events WHERE id = ?"
|
||||
var start, end int64
|
||||
var email, summary, description, status string
|
||||
err := db.QueryRow(sqlStmt, id).Scan(&email, &start, &end, &summary, &description, &status)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
timezone := getTimezone(email)
|
||||
loc, err := time.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
calEvent := calendar.Event{}
|
||||
calEvent.Id = id
|
||||
calEvent.Start = &calendar.EventDateTime{DateTime: time.Unix(start, 0).In(loc).Format(time.RFC3339)}
|
||||
calEvent.End = &calendar.EventDateTime{DateTime: time.Unix(end, 0).In(loc).Format(time.RFC3339)}
|
||||
calEvent.Summary = summary
|
||||
calEvent.Description = description
|
||||
calEvent.Status = status
|
||||
calEvent.Etag = computeETag(start, end, summary, description, status)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err = json.NewEncoder(w).Encode(calEvent)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getEvents(w http.ResponseWriter, r *http.Request) {
|
||||
email := r.URL.Query().Get("email")
|
||||
if email == "" {
|
||||
http.Error(w, "missing email", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
timeMin := r.URL.Query().Get("timemin")
|
||||
if email == "" {
|
||||
http.Error(w, "missing timemin", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
timeMax := r.URL.Query().Get("timemax")
|
||||
if email == "" {
|
||||
http.Error(w, "missing timemax", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
minTime, err := parseDateTime(r.Context(), &calendar.EventDateTime{DateTime: timeMin})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
maxTime, err := parseDateTime(r.Context(), &calendar.EventDateTime{DateTime: timeMax})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sqlStmt := "SELECT id, start, end, summary, description, status FROM events WHERE email = ? AND end > ? AND start < ?"
|
||||
rows, err := db.Query(sqlStmt, email, minTime.Unix(), maxTime.Unix())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
timezone := getTimezone(email)
|
||||
loc, err := time.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
events := calendar.Events{}
|
||||
events.Items = make([]*calendar.Event, 0)
|
||||
for rows.Next() {
|
||||
var id, start, end int64
|
||||
var summary, description, status string
|
||||
err = rows.Scan(&id, &start, &end, &summary, &description, &status)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
calEvent := calendar.Event{}
|
||||
calEvent.Id = fmt.Sprintf("%d", id)
|
||||
calEvent.Start = &calendar.EventDateTime{DateTime: time.Unix(start, 0).In(loc).Format(time.RFC3339)}
|
||||
calEvent.End = &calendar.EventDateTime{DateTime: time.Unix(end, 0).In(loc).Format(time.RFC3339)}
|
||||
calEvent.Summary = summary
|
||||
calEvent.Description = description
|
||||
calEvent.Status = status
|
||||
calEvent.Etag = computeETag(start, end, summary, description, status)
|
||||
events.Items = append(events.Items, &calEvent)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err = json.NewEncoder(w).Encode(events)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// addEvent handles POST /events/add?email=user@example.com
|
||||
func addEvent(w http.ResponseWriter, r *http.Request) {
|
||||
var event calendar.Event
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(body, &event)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
email := r.URL.Query().Get("email")
|
||||
if email == "" {
|
||||
http.Error(w, "missing email", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
start, err := parseDateTime(r.Context(), event.Start)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
end, err := parseDateTime(r.Context(), event.End)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
status := "confirmed"
|
||||
sqlStmt := `INSERT INTO events (email, start, end, summary, description, status) VALUES (?, ?, ?, ?, ?, ?)`
|
||||
result, err := db.Exec(sqlStmt, email, start.Unix(), end.Unix(), event.Summary, event.Description, status)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
event.Id = fmt.Sprintf("%d", id)
|
||||
event.Etag = computeETag(start.Unix(), end.Unix(), event.Summary, event.Description, status)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
err = json.NewEncoder(w).Encode(event)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func computeETag(args ...any) string {
|
||||
h := md5.New() //nolint:gosec // (only used for tests)
|
||||
_, _ = fmt.Fprint(h, args...)
|
||||
checksum := h.Sum(nil)
|
||||
return hex.EncodeToString(checksum)
|
||||
}
|
||||
|
||||
// deleteEvent handles DELETE /events/delete?id=123
|
||||
func deleteEvent(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
http.Error(w, "missing id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sqlStmt := "DELETE FROM events WHERE id = ?"
|
||||
_, err := db.Exec(sqlStmt, id)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
http.Error(w, "not found", http.StatusGone)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func initializeSchema() error {
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS events (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"email" TEXT NOT NULL,
|
||||
"start" INTEGER NOT NULL,
|
||||
"end" INTEGER NOT NULL,
|
||||
"summary" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL
|
||||
);`
|
||||
_, err := db.Exec(createTableSQL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create table: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseDateTime(ctx context.Context, eventDateTime *calendar.EventDateTime) (*time.Time, error) {
|
||||
var t time.Time
|
||||
var err error
|
||||
if eventDateTime.TimeZone != "" {
|
||||
var loc *time.Location
|
||||
loc, err = time.LoadLocation(eventDateTime.TimeZone)
|
||||
if err == nil {
|
||||
t, err = time.ParseInLocation(time.RFC3339, eventDateTime.DateTime, loc)
|
||||
}
|
||||
} else {
|
||||
t, err = time.Parse(time.RFC3339, eventDateTime.DateTime)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(
|
||||
ctx, err, fmt.Sprintf("parsing calendar event time: %s", eventDateTime.DateTime),
|
||||
)
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
26
tools/calendar/README.md
Normal file
26
tools/calendar/README.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Calendar server for load testing
|
||||
|
||||
Test calendar server that provides a REST API for managing events.
|
||||
Since we may not have access to a real calendar server (such as Google Calendar API), this server will be used to test the calendar feature during load testing.
|
||||
|
||||
Start the server like:
|
||||
```shell
|
||||
go run calendar.go --port 8083 --db ./calendar.db
|
||||
```
|
||||
|
||||
The server uses a SQLite database to store events. This database can be modified during testing.
|
||||
|
||||
On the fleet server, configure Google Calendar API key where `client_email` is the specified value and the `private_key` is the base URL of the calendar server:
|
||||
```json
|
||||
{
|
||||
"client_email": "calendar-load@example.com",
|
||||
"private_key": "http://localhost:8083"
|
||||
}
|
||||
```
|
||||
|
||||
## Useful tricks
|
||||
|
||||
To update all the events in SQLite database to start at the current time, do SQL query:
|
||||
```sql
|
||||
UPDATE events SET start = unixepoch('now'), end = unixepoch('now', '+30 minutes');
|
||||
```
|
||||
39
tools/calendar/calendar.go
Normal file
39
tools/calendar/calendar.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
calendartest "github.com/fleetdm/fleet/v4/ee/server/calendar/load_test"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := flag.Uint("port", 8083, "Port to listen on")
|
||||
dbFileName := flag.String("db", "./calendar.db", "SQLite db file name")
|
||||
flag.Parse()
|
||||
|
||||
handler, err := calendartest.Configure(*dbFileName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer calendartest.Close()
|
||||
|
||||
listenAddr := fmt.Sprintf(":%d", *port)
|
||||
errLogger := log.New(os.Stderr, "", log.LstdFlags)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: handler,
|
||||
ErrorLog: errLogger,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
// Start the HTTP server
|
||||
log.Fatal(server.ListenAndServe())
|
||||
}
|
||||
Loading…
Reference in a new issue