diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e54277c2..ef6611db 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/go { "name": "automated-garden", - "image": "mcr.microsoft.com/devcontainers/go:1-1.21-bullseye", + "image": "mcr.microsoft.com/devcontainers/go:1-1.22-bullseye", "features": { "ghcr.io/devcontainers/features/sshd:1": { "version": "latest" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c5d6d6c0..9c20c7de 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.22" cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v3 @@ -38,7 +38,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.22" - name: Test run: task -t GithubActionTasks.yml main:unit-test @@ -62,7 +62,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.22" - name: Integration Test run: task -t GithubActionTasks.yml main:integration-test diff --git a/Taskfile.yml b/Taskfile.yml index a3bde4a4..d06c7262 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -62,6 +62,17 @@ tasks: cmds: - DEV_TEMPLATE=server/templates/* go run main.go serve --config config.yaml + run-vcr: + desc: Run backend Go app server with VCR enabled + aliases: [vcr] + dir: ./garden-app + vars: + vcr_base_dir: server/vcr/testdata/vcr_server + env: + VCR_CASSETTE: "{{ .vcr_base_dir }}/fixtures/{{ .CLI_ARGS }}" + cmds: + - go run main.go serve --config ./{{ .vcr_base_dir }}/config.yaml + run-controller: desc: Run mock controller aliases: [rc] diff --git a/garden-app/.gitignore b/garden-app/.gitignore index 3ade8f06..1d3d0356 100644 --- a/garden-app/.gitignore +++ b/garden-app/.gitignore @@ -1,7 +1,6 @@ garden-app -config.yaml -plants.yaml -gardens.yaml +/config.yaml +/gardens.yaml cover.out coverage.out integration_coverage.out diff --git a/garden-app/Dockerfile b/garden-app/Dockerfile index 1c33201c..1656cc09 100644 --- a/garden-app/Dockerfile +++ b/garden-app/Dockerfile @@ -1,6 +1,6 @@ # build go app -FROM golang:1.21-alpine AS build +FROM golang:1.22-alpine AS build RUN mkdir /build ADD . /build WORKDIR /build diff --git a/garden-app/clock/clock.go b/garden-app/clock/clock.go new file mode 100644 index 00000000..93df08a4 --- /dev/null +++ b/garden-app/clock/clock.go @@ -0,0 +1,43 @@ +package clock + +import ( + "time" + + "github.com/benbjohnson/clock" + "github.com/go-co-op/gocron" +) + +// Clock allows mocking time +type Clock struct { + clock.Clock +} + +// DefaultClock is the underlying Clock used and can be overridden to mock +var DefaultClock = Clock{clock.New()} + +var _ gocron.TimeWrapper = Clock{} + +func (c Clock) Now(loc *time.Location) time.Time { + return c.Clock.Now().In(loc) +} + +func (c Clock) Unix(sec int64, nsec int64) time.Time { + return time.Unix(sec, nsec).In(DefaultClock.Clock.Now().Location()) +} + +func Now() time.Time { + return DefaultClock.Clock.Now() +} + +// MockTime sets up the DefaultClock with a consistent time so it can be used across tests +func MockTime() *clock.Mock { + mock := clock.NewMock() + mock.Set(time.Date(2023, time.August, 23, 10, 0, 0, 0, time.UTC)) + DefaultClock = Clock{Clock: mock} + return mock +} + +// Reset returns the DefaultClock to real time +func Reset() { + DefaultClock = Clock{clock.New()} +} diff --git a/garden-app/cmd/root.go b/garden-app/cmd/root.go index 5d2545ec..63c5c1fe 100644 --- a/garden-app/cmd/root.go +++ b/garden-app/cmd/root.go @@ -6,6 +6,8 @@ import ( "strings" "github.com/calvinmclean/automated-garden/garden-app/server" + "github.com/calvinmclean/automated-garden/garden-app/server/vcr" + "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -16,6 +18,8 @@ var ( ) func Execute() { + defer vcr.StopRecorder() + api := server.NewAPI() command := api.Command() diff --git a/garden-app/controller/controller.go b/garden-app/controller/controller.go index e616ab8c..6259093b 100644 --- a/garden-app/controller/controller.go +++ b/garden-app/controller/controller.go @@ -11,6 +11,7 @@ import ( "syscall" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg/action" "github.com/calvinmclean/automated-garden/garden-app/pkg/mqtt" "github.com/calvinmclean/automated-garden/garden-app/server" @@ -115,7 +116,7 @@ func NewController(cfg Config) (*Controller, error) { } // Override configured ClientID with the TopicPrefix from command flags - controller.MQTTConfig.ClientID = fmt.Sprintf(controller.TopicPrefix) + controller.MQTTConfig.ClientID = fmt.Sprint(controller.TopicPrefix) controller.mqttClient, err = mqtt.NewClient(controller.MQTTConfig, mqtt.DefaultHandler(controller.logger), handlers...) if err != nil { return nil, fmt.Errorf("unable to initialize MQTT client: %w", err) @@ -132,6 +133,7 @@ func (c *Controller) Start() { // Initialize scheduler and schedule publishing Jobs c.logger.Debug("initializing scheduler") scheduler := gocron.NewScheduler(time.Local) + scheduler.CustomTime(clock.DefaultClock) if c.MoistureInterval != 0 { for p := 0; p < c.NumZones; p++ { c.logger.With( @@ -170,7 +172,7 @@ func (c *Controller) Start() { var shutdownStart time.Time go func() { <-c.quit - shutdownStart = time.Now() + shutdownStart = clock.Now() c.logger.Info("gracefully shutting down controller") scheduler.Stop() diff --git a/garden-app/go.mod b/garden-app/go.mod index a34340a4..523c3f5c 100644 --- a/garden-app/go.mod +++ b/garden-app/go.mod @@ -1,11 +1,14 @@ module github.com/calvinmclean/automated-garden/garden-app -go 1.21.3 +go 1.22 + +toolchain go1.23.0 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/ajg/form v1.5.1 - github.com/calvinmclean/babyapi v0.22.0 + github.com/benbjohnson/clock v1.3.5 + github.com/calvinmclean/babyapi v0.23.0 github.com/eclipse/paho.mqtt.golang v1.4.3 github.com/go-chi/render v1.0.3 github.com/go-co-op/gocron v1.35.2 @@ -24,7 +27,7 @@ require ( github.com/tarmac-project/hord v0.6.0 github.com/tarmac-project/hord/drivers/hashmap v0.6.0 github.com/tarmac-project/hord/drivers/redis v0.6.0 - gopkg.in/dnaeon/go-vcr.v3 v3.2.0 + gopkg.in/dnaeon/go-vcr.v4 v4.0.0-20240818155041-3873f09a3029 gopkg.in/yaml.v3 v3.0.1 ) @@ -136,3 +139,7 @@ require ( google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) + +replace gopkg.in/dnaeon/go-vcr.v4 => github.com/calvinmclean/go-vcr v0.4.0 + +//replace gopkg.in/dnaeon/go-vcr.v4 => ../../go-vcr diff --git a/garden-app/go.sum b/garden-app/go.sum index 1928e87e..c26fab8d 100644 --- a/garden-app/go.sum +++ b/garden-app/go.sum @@ -65,6 +65,8 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7D github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -72,8 +74,10 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1 github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= -github.com/calvinmclean/babyapi v0.22.0 h1:g27F4DFdGeQfI97Pn+vGZtRkvR/RdWwqq2OMgSO7BAk= -github.com/calvinmclean/babyapi v0.22.0/go.mod h1:zSNiVRsL3DBPOMkXxMJOTFNtzU1ZrPFKD0LFx2JVp4I= +github.com/calvinmclean/babyapi v0.23.0 h1:rmYnAz80cX6TTDtbjQGt8DzjR7CDZNxkJm0/KvWRQHY= +github.com/calvinmclean/babyapi v0.23.0/go.mod h1:zSNiVRsL3DBPOMkXxMJOTFNtzU1ZrPFKD0LFx2JVp4I= +github.com/calvinmclean/go-vcr v0.4.0 h1:QFl8yETQkARY+9pQErhvSW4o6foKEZFGkV5L+iTRcX4= +github.com/calvinmclean/go-vcr v0.4.0/go.mod h1:65yxh9goQVrudqofKtHA4JNFWd6XZRkWfKN4YpMx7KI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -797,8 +801,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/dnaeon/go-vcr.v3 v3.2.0 h1:Rltp0Vf+Aq0u4rQXgmXgtgoRDStTnFN83cWgSGSoRzM= -gopkg.in/dnaeon/go-vcr.v3 v3.2.0/go.mod h1:2IMOnnlx9I6u9x+YBsM3tAMx6AlOxnJ0pWxQAzZ79Ag= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/garden-app/integration_tests/main_test.go b/garden-app/integration_tests/main_test.go index e3ed1676..0193f24b 100644 --- a/garden-app/integration_tests/main_test.go +++ b/garden-app/integration_tests/main_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/controller" "github.com/calvinmclean/automated-garden/garden-app/pkg" "github.com/calvinmclean/automated-garden/garden-app/pkg/action" @@ -161,7 +162,7 @@ func GardenTests(t *testing.T) { // Create new Garden with LightOnTime in the near future, so LightDelay will assume the light is currently off, // meaning adhoc action is going to be predictably delayed maxZones := uint(1) - startTime := pkg.NewStartTime(time.Now().In(time.Local).Add(1 * time.Second).Truncate(time.Second)) + startTime := pkg.NewStartTime(clock.Now().In(time.Local).Add(1 * time.Second).Truncate(time.Second)) newGarden := &pkg.Garden{ Name: "TestGarden", TopicPrefix: "test", @@ -208,30 +209,36 @@ func GardenTests(t *testing.T) { ) }) t.Run("ChangeLightScheduleStartTimeResetsLightSchedule", func(t *testing.T) { - // Reschedule Light to turn in in 1 second, for 1 second - newStartTime := pkg.NewStartTime(time.Now().Add(1 * time.Second).Truncate(time.Second)) - var g server.GardenResponse - status, err := makeRequest(http.MethodPatch, "/gardens/"+gardenID, pkg.Garden{ - LightSchedule: &pkg.LightSchedule{ - StartTime: newStartTime, - Duration: &pkg.Duration{Duration: time.Second}, - }, - }, &g) - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, status) - assert.Equal(t, newStartTime.String(), g.LightSchedule.StartTime.String()) + // Reschedule Light to turn in in 2 second, for 1 second + newStartTimeDelay := 2 * time.Second + newStartTime := pkg.NewStartTime(clock.Now().Add(newStartTimeDelay).Truncate(time.Second)) + + t.Run("ModifyLightSchedule", func(t *testing.T) { + var g server.GardenResponse + status, err := makeRequest(http.MethodPatch, "/gardens/"+gardenID, pkg.Garden{ + LightSchedule: &pkg.LightSchedule{ + StartTime: newStartTime, + Duration: &pkg.Duration{Duration: time.Second}, + }, + }, &g) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, status) + assert.Equal(t, newStartTime.String(), g.LightSchedule.StartTime.String()) + }) time.Sleep(100 * time.Millisecond) - // Make sure NextOnTime and state are changed - var g2 server.GardenResponse - status, err = makeRequest(http.MethodGet, "/gardens/"+gardenID, nil, &g2) - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, status) - assert.Equal(t, newStartTime.String(), pkg.NewStartTime(g2.NextLightAction.Time.Local()).String()) - assert.Equal(t, pkg.LightStateOn, g2.NextLightAction.State) + t.Run("CheckNewNextOnTime", func(t *testing.T) { + var g server.GardenResponse + status, err := makeRequest(http.MethodGet, "/gardens/"+gardenID, nil, &g) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, status) + assert.Equal(t, newStartTime.String(), pkg.NewStartTime(g.NextLightAction.Time.Local()).String()) + assert.Equal(t, pkg.LightStateOn, g.NextLightAction.State) + }) - time.Sleep(2 * time.Second) + // wait a little extra + time.Sleep(2*newStartTimeDelay + 500*time.Millisecond) // Assert both LightActions c.AssertLightActions(t, @@ -368,7 +375,7 @@ func ZoneTests(t *testing.T) { }) t.Run("ChangeWaterScheduleStartTimeResetsWaterSchedule", func(t *testing.T) { // Reschedule to Water in 2 second, for 1 second - newStartTime := time.Now().Add(2 * time.Second).Truncate(time.Second) + newStartTime := clock.Now().Add(2 * time.Second).Truncate(time.Second) var ws server.WaterScheduleResponse status, err := makeRequest(http.MethodPatch, "/water_schedules/"+waterScheduleID, pkg.WaterSchedule{ StartTime: pkg.NewStartTime(newStartTime), @@ -419,7 +426,7 @@ func WaterScheduleTests(t *testing.T) { }) // Reschedule to Water in 2 second, for 1 second - newStartTime := time.Now().Add(2 * time.Second).Truncate(time.Second) + newStartTime := clock.Now().Add(2 * time.Second).Truncate(time.Second) var ws server.WaterScheduleResponse status, err := makeRequest(http.MethodPatch, "/water_schedules/"+waterScheduleID, pkg.WaterSchedule{ StartTime: pkg.NewStartTime(newStartTime), diff --git a/garden-app/pkg/garden.go b/garden-app/pkg/garden.go index bfc65dfc..c07641dc 100644 --- a/garden-app/pkg/garden.go +++ b/garden-app/pkg/garden.go @@ -8,6 +8,7 @@ import ( "regexp" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg/influxdb" "github.com/calvinmclean/babyapi" ) @@ -92,7 +93,7 @@ func (g *Garden) Health(ctx context.Context, influxdbClient influxdb.Client) *Ga // EndDated returns true if the Garden is end-dated func (g *Garden) EndDated() bool { - return g.EndDate != nil && g.EndDate.Before(time.Now()) + return g.EndDate != nil && g.EndDate.Before(clock.Now()) } func (g *Garden) SetEndDate(now time.Time) { @@ -156,7 +157,7 @@ func (g *Garden) Bind(r *http.Request) error { return err } - now := time.Now() + now := clock.Now() switch r.Method { case http.MethodPost: g.CreatedAt = &now @@ -196,6 +197,11 @@ func (g *Garden) Bind(r *http.Request) error { return errors.New("missing required light_schedule.start_time field") } } + + // Ignore empty string provided for NotificationClientID + if g.NotificationClientID != nil && *g.NotificationClientID == "" { + g.NotificationClientID = nil + } case http.MethodPatch: illegalRegexp := regexp.MustCompile(`[\$\#\*\>\+\/]`) if illegalRegexp.MatchString(g.TopicPrefix) { diff --git a/garden-app/pkg/garden_test.go b/garden-app/pkg/garden_test.go index 70290fc1..ad6c49a9 100644 --- a/garden-app/pkg/garden_test.go +++ b/garden-app/pkg/garden_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg/influxdb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -21,13 +22,13 @@ func TestHealth(t *testing.T) { }{ { "GardenIsUp", - time.Now(), + clock.Now(), nil, HealthStatusUp, }, { "GardenIsDown", - time.Now().Add(-5 * time.Minute), + clock.Now().Add(-5 * time.Minute), nil, HealthStatusDown, }, @@ -61,8 +62,8 @@ func TestHealth(t *testing.T) { } func TestGardenEndDated(t *testing.T) { - pastDate := time.Now().Add(-1 * time.Minute) - futureDate := time.Now().Add(time.Minute) + pastDate := clock.Now().Add(-1 * time.Minute) + futureDate := clock.Now().Add(time.Minute) tests := []struct { name string endDate *time.Time @@ -84,7 +85,7 @@ func TestGardenEndDated(t *testing.T) { } func TestGardenPatch(t *testing.T) { - now := time.Now() + now := clock.Now() ten := uint(10) trueBool := true falseBool := false @@ -172,7 +173,7 @@ func TestGardenPatch(t *testing.T) { }) t.Run("PatchDoesNotAddEndDate", func(t *testing.T) { - now := time.Now() + now := clock.Now() g := &Garden{} err := g.Patch(&Garden{EndDate: &now}) @@ -184,7 +185,7 @@ func TestGardenPatch(t *testing.T) { }) t.Run("PatchRemoveEndDate", func(t *testing.T) { - now := time.Now() + now := clock.Now() g := &Garden{ EndDate: &now, } diff --git a/garden-app/pkg/influxdb/client.go b/garden-app/pkg/influxdb/client.go index bd4bb95a..71c94d34 100644 --- a/garden-app/pkg/influxdb/client.go +++ b/garden-app/pkg/influxdb/client.go @@ -3,6 +3,7 @@ package influxdb import ( "bytes" "context" + "sync" "text/template" "time" @@ -44,6 +45,12 @@ const ( |> mean()` ) +func init() { + sync.OnceFunc(func() { + prometheus.MustRegister(influxDBClientSummary) + })() +} + var influxDBClientSummary = prometheus.NewSummaryVec(prometheus.SummaryOpts{ Namespace: "garden_app", Name: "influxdb_client_duration_seconds", @@ -95,7 +102,6 @@ type client struct { // NewClient creates an InfluxDB client from the viper config func NewClient(config Config) Client { - prometheus.MustRegister(influxDBClientSummary) return &client{ influxdb2.NewClient(config.Address, config.Token), config, diff --git a/garden-app/pkg/start_time_test.go b/garden-app/pkg/start_time_test.go index 5f3cd465..b94391bb 100644 --- a/garden-app/pkg/start_time_test.go +++ b/garden-app/pkg/start_time_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/ajg/form" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/stretchr/testify/assert" ) @@ -33,7 +34,7 @@ func TestTimeLocationFromOffset(t *testing.T) { }, } - now := time.Now() + now := clock.Now() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { expectedLoc, _ := time.LoadLocation(tt.expectedLoc) diff --git a/garden-app/pkg/water_schedule.go b/garden-app/pkg/water_schedule.go index 42adf40a..f83fc9cb 100644 --- a/garden-app/pkg/water_schedule.go +++ b/garden-app/pkg/water_schedule.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg/weather" "github.com/calvinmclean/babyapi" "github.com/rs/xid" @@ -47,7 +48,7 @@ func (ws *WaterSchedule) GetNotificationClientID() string { // EndDated returns true if the WaterSchedule is end-dated func (ws *WaterSchedule) EndDated() bool { - return ws.EndDate != nil && ws.EndDate.Before(time.Now()) + return ws.EndDate != nil && ws.EndDate.Before(clock.Now()) } func (ws *WaterSchedule) SetEndDate(now time.Time) { @@ -246,7 +247,7 @@ func (ws *WaterSchedule) Bind(r *http.Request) error { } // If StartDate is not included, default to today if ws.StartDate == nil { - now := time.Now() + now := clock.Now() ws.StartDate = &now } if ws.WeatherControl != nil { diff --git a/garden-app/pkg/water_schedule_test.go b/garden-app/pkg/water_schedule_test.go index 831df392..41a72938 100644 --- a/garden-app/pkg/water_schedule_test.go +++ b/garden-app/pkg/water_schedule_test.go @@ -5,14 +5,15 @@ import ( "testing" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg/weather" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestWaterScheduleEndDated(t *testing.T) { - pastDate := time.Now().Add(-1 * time.Minute) - futureDate := time.Now().Add(time.Minute) + pastDate := clock.Now().Add(-1 * time.Minute) + futureDate := clock.Now().Add(time.Minute) tests := []struct { name string endDate *time.Time @@ -36,7 +37,7 @@ func TestWaterScheduleEndDated(t *testing.T) { func TestWaterSchedulePatch(t *testing.T) { one := 1 float := float32(1) - now := time.Now() + now := clock.Now() tests := []struct { name string newWaterSchedule *WaterSchedule @@ -139,7 +140,7 @@ func TestWaterSchedulePatch(t *testing.T) { } t.Run("PatchDoesNotAddEndDate", func(t *testing.T) { - now := time.Now() + now := clock.Now() ws := &WaterSchedule{} err := ws.Patch(&WaterSchedule{EndDate: &now}) @@ -151,7 +152,7 @@ func TestWaterSchedulePatch(t *testing.T) { }) t.Run("PatchRemoveEndDate", func(t *testing.T) { - now := time.Now() + now := clock.Now() ws := &WaterSchedule{ EndDate: &now, } @@ -377,7 +378,7 @@ func TestWaterScheduleIsActive(t *testing.T) { currentMonth, err := time.Parse("January", tt.currentMonth) assert.NoError(t, err) - currentTime := currentMonth.AddDate(time.Now().Year(), 0, 0) + currentTime := currentMonth.AddDate(clock.Now().Year(), 0, 0) // Check every day in this month for currentTime.Month() == currentMonth.Month() { @@ -390,6 +391,6 @@ func TestWaterScheduleIsActive(t *testing.T) { } t.Run("NoActivePeriod", func(t *testing.T) { - assert.Equal(t, true, (&WaterSchedule{}).IsActive(time.Now())) + assert.Equal(t, true, (&WaterSchedule{}).IsActive(clock.Now())) }) } diff --git a/garden-app/pkg/weather/client.go b/garden-app/pkg/weather/client.go index 9d35c77b..7a8d8a69 100644 --- a/garden-app/pkg/weather/client.go +++ b/garden-app/pkg/weather/client.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg/weather/fake" "github.com/calvinmclean/automated-garden/garden-app/pkg/weather/netatmo" "github.com/calvinmclean/babyapi" @@ -127,7 +128,7 @@ func newMetricsWrapperClient(client Client, config *Config) Client { // GetTotalRain ... func (c *clientWrapper) GetTotalRain(since time.Duration) (float32, error) { - now := time.Now() + now := clock.Now() cached := false defer func() { weatherClientSummary.WithLabelValues("GetTotalRain", fmt.Sprintf("%t", cached)).Observe(time.Since(now).Seconds()) @@ -151,7 +152,7 @@ func (c *clientWrapper) GetTotalRain(since time.Duration) (float32, error) { // GetAverageHighTemperature ... func (c *clientWrapper) GetAverageHighTemperature(since time.Duration) (float32, error) { - now := time.Now() + now := clock.Now() cached := false defer func() { weatherClientSummary.WithLabelValues("GetAverageHighTemperature", fmt.Sprintf("%t", cached)).Observe(time.Since(now).Seconds()) diff --git a/garden-app/pkg/weather/netatmo/client.go b/garden-app/pkg/weather/netatmo/client.go index 87c39c94..25f9c54a 100644 --- a/garden-app/pkg/weather/netatmo/client.go +++ b/garden-app/pkg/weather/netatmo/client.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/mitchellh/mapstructure" ) @@ -196,7 +197,7 @@ func (c *Client) refreshToken() error { expiry, _ := time.Parse(time.RFC3339Nano, c.Config.Authentication.ExpirationDate) // Exit early if token is not expired - if time.Now().Before(expiry) { + if clock.Now().Before(expiry) { return nil } @@ -231,7 +232,7 @@ func (c *Client) refreshToken() error { if err != nil { return fmt.Errorf("unable to unmarshal refresh token response body: %w", err) } - c.Authentication.ExpirationDate = time.Now().Add(time.Duration(c.Authentication.ExpiresIn) * time.Second).Format(time.RFC3339Nano) + c.Authentication.ExpirationDate = clock.Now().Add(time.Duration(c.Authentication.ExpiresIn) * time.Second).Format(time.RFC3339Nano) // Use storage callback to save new authentication details err = c.storageCallback(map[string]interface{}{ diff --git a/garden-app/pkg/weather/netatmo/client_test.go b/garden-app/pkg/weather/netatmo/client_test.go index 8047967b..dbc9c6c3 100644 --- a/garden-app/pkg/weather/netatmo/client_test.go +++ b/garden-app/pkg/weather/netatmo/client_test.go @@ -5,13 +5,14 @@ import ( "testing" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/stretchr/testify/require" - "gopkg.in/dnaeon/go-vcr.v3/cassette" - "gopkg.in/dnaeon/go-vcr.v3/recorder" + "gopkg.in/dnaeon/go-vcr.v4/pkg/cassette" + "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" ) func TestNewClientUsingDeviceName(t *testing.T) { - r, err := recorder.New("testdata/fixtures/GetDeviceIDs") + r, err := recorder.New(recorder.WithCassette("testdata/fixtures/GetDeviceIDs")) if err != nil { t.Fatal(err) } @@ -27,9 +28,9 @@ func TestNewClientUsingDeviceName(t *testing.T) { opts := map[string]any{ "authentication": map[string]any{ - "access_token": "ACCESS_TOKE", + "access_token": "ACCESS_TOKEN", "refresh_token": "REFRESH_TOKEN", - "expiration_date": time.Now().Add(1 * time.Minute).Format(time.RFC3339Nano), + "expiration_date": clock.Now().Add(1 * time.Minute).Format(time.RFC3339Nano), }, "client_id": "CLIENT_ID", "client_secret": "CLIENT_SECRET", @@ -48,6 +49,21 @@ func TestNewClientUsingDeviceName(t *testing.T) { } func TestWeatherRequestMethods(t *testing.T) { + // Modify request from garden-app to use placeholder for date_begin query param + matcher := func(r1 *http.Request, r2 cassette.Request) bool { + query := r1.URL.Query() + if query.Get("date_begin") != "" { + query.Set("date_begin", "DATE_BEGIN") + r1.URL.RawQuery = query.Encode() + } + if query.Get("date_end") != "" { + query.Set("date_end", "DATE_END") + r1.URL.RawQuery = query.Encode() + } + + return cassette.DefaultMatcher(r1, r2) + } + tests := []struct { name string fixture string @@ -57,7 +73,7 @@ func TestWeatherRequestMethods(t *testing.T) { { "GetTotalRain_NoRefresh", "testdata/fixtures/GetTotalRain_NoRefresh", - time.Now().Add(1 * time.Minute), + clock.Now().Add(1 * time.Minute), func(t *testing.T, client *Client) { rain, err := client.GetTotalRain(72 * time.Hour) require.NoError(t, err) @@ -67,7 +83,7 @@ func TestWeatherRequestMethods(t *testing.T) { { "GetTotalRain_Refresh", "testdata/fixtures/GetTotalRain_Refresh", - time.Now().Add(-1 * time.Minute), + clock.Now().Add(-1 * time.Minute), func(t *testing.T, client *Client) { rain, err := client.GetTotalRain(72 * time.Hour) require.NoError(t, err) @@ -78,7 +94,7 @@ func TestWeatherRequestMethods(t *testing.T) { { "GetAverageHighTemperature_NoRefresh", "testdata/fixtures/GetAverageHighTemperature_NoRefresh", - time.Now().Add(1 * time.Minute), + clock.Now().Add(1 * time.Minute), func(t *testing.T, client *Client) { temp, err := client.GetAverageHighTemperature(72 * time.Hour) require.NoError(t, err) @@ -104,7 +120,10 @@ func TestWeatherRequestMethods(t *testing.T) { client, err := NewClient(opts, func(newOpts map[string]interface{}) error { return nil }) require.NoError(t, err) - r, err := recorder.New(tt.fixture) + r, err := recorder.New( + recorder.WithCassette(tt.fixture), + recorder.WithMatcher(matcher), + ) if err != nil { t.Fatal(err) } @@ -116,21 +135,6 @@ func TestWeatherRequestMethods(t *testing.T) { t.Fatal("Recorder should be in ModeRecordOnce") } - // Modify request from garden-app to use placeholder for date_begin query param - r.SetMatcher(func(r1 *http.Request, r2 cassette.Request) bool { - query := r1.URL.Query() - if query.Get("date_begin") != "" { - query.Set("date_begin", "DATE_BEGIN") - r1.URL.RawQuery = query.Encode() - } - if query.Get("date_end") != "" { - query.Set("date_end", "DATE_END") - r1.URL.RawQuery = query.Encode() - } - - return cassette.DefaultMatcher(r1, r2) - }) - client.Client = r.GetDefaultClient() tt.exec(t, client) diff --git a/garden-app/pkg/weather/netatmo/rain.go b/garden-app/pkg/weather/netatmo/rain.go index 8e993cc2..c7e965b7 100644 --- a/garden-app/pkg/weather/netatmo/rain.go +++ b/garden-app/pkg/weather/netatmo/rain.go @@ -2,6 +2,8 @@ package netatmo import ( "time" + + "github.com/calvinmclean/automated-garden/garden-app/clock" ) const minRainInterval = 24 * time.Hour @@ -13,7 +15,7 @@ func (c *Client) GetTotalRain(since time.Duration) (float32, error) { since = minRainInterval } - beginDate := time.Now().Add(-since) + beginDate := clock.Now().Add(-since) rainData, err := c.getMeasure("sum_rain", "1day", beginDate, nil) if err != nil { return 0, err diff --git a/garden-app/pkg/weather/netatmo/temperature.go b/garden-app/pkg/weather/netatmo/temperature.go index b0ac37e6..8366fee3 100644 --- a/garden-app/pkg/weather/netatmo/temperature.go +++ b/garden-app/pkg/weather/netatmo/temperature.go @@ -2,6 +2,8 @@ package netatmo import ( "time" + + "github.com/calvinmclean/automated-garden/garden-app/clock" ) const minTemperatureInterval = 72 * time.Hour @@ -14,7 +16,7 @@ func (c *Client) GetAverageHighTemperature(since time.Duration) (float32, error) since = minTemperatureInterval } - now := time.Now() + now := clock.Now() beginDate := now.Add(-since).Truncate(time.Hour) beginDate = time.Date(beginDate.Year(), beginDate.Month(), beginDate.Day()-1, 23, 59, 59, 0, time.Local) // Since we are looking at daily max temp, get time all the way to very end of yesterday diff --git a/garden-app/pkg/weather/netatmo/testdata/fixtures/GetTotalRain_Refresh.yaml b/garden-app/pkg/weather/netatmo/testdata/fixtures/GetTotalRain_Refresh.yaml index a3a08f3d..44f5865a 100644 --- a/garden-app/pkg/weather/netatmo/testdata/fixtures/GetTotalRain_Refresh.yaml +++ b/garden-app/pkg/weather/netatmo/testdata/fixtures/GetTotalRain_Refresh.yaml @@ -6,7 +6,7 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 180 + content_length: 100 transfer_encoding: [] trailer: {} host: api.netatmo.com diff --git a/garden-app/pkg/zone.go b/garden-app/pkg/zone.go index cf51d928..c99f9b37 100644 --- a/garden-app/pkg/zone.go +++ b/garden-app/pkg/zone.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/babyapi" "github.com/rs/xid" ) @@ -38,7 +39,7 @@ func (z *Zone) String() string { // EndDated returns true if the Zone is end-dated func (z *Zone) EndDated() bool { - return z.EndDate != nil && z.EndDate.Before(time.Now()) + return z.EndDate != nil && z.EndDate.Before(clock.Now()) } func (z *Zone) SetEndDate(now time.Time) { @@ -134,7 +135,7 @@ func (z *Zone) Bind(r *http.Request) error { } z.WaterScheduleIDs = wsIDs - now := time.Now() + now := clock.Now() switch r.Method { case http.MethodPost: z.CreatedAt = &now diff --git a/garden-app/pkg/zone_test.go b/garden-app/pkg/zone_test.go index ec8f5683..293f66a5 100644 --- a/garden-app/pkg/zone_test.go +++ b/garden-app/pkg/zone_test.go @@ -4,14 +4,15 @@ import ( "testing" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/rs/xid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestZoneEndDated(t *testing.T) { - pastDate := time.Now().Add(-1 * time.Minute) - futureDate := time.Now().Add(time.Minute) + pastDate := clock.Now().Add(-1 * time.Minute) + futureDate := clock.Now().Add(time.Minute) tests := []struct { name string endDate *time.Time @@ -35,7 +36,7 @@ func TestZoneEndDated(t *testing.T) { func TestZonePatch(t *testing.T) { zero := uint(0) three := uint(3) - now := time.Now() + now := clock.Now() wsID := xid.New() tests := []struct { name string @@ -87,7 +88,7 @@ func TestZonePatch(t *testing.T) { } t.Run("PatchDoesNotAddEndDate", func(t *testing.T) { - now := time.Now() + now := clock.Now() p := &Zone{} err := p.Patch(&Zone{EndDate: &now}) @@ -99,7 +100,7 @@ func TestZonePatch(t *testing.T) { }) t.Run("PatchRemoveEndDate", func(t *testing.T) { - now := time.Now() + now := clock.Now() p := &Zone{ EndDate: &now, } diff --git a/garden-app/server/api.go b/garden-app/server/api.go index 3c7d4d89..4b94b50f 100644 --- a/garden-app/server/api.go +++ b/garden-app/server/api.go @@ -6,11 +6,15 @@ import ( "fmt" "log/slog" "net/http" + "os" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg/influxdb" "github.com/calvinmclean/automated-garden/garden-app/pkg/mqtt" "github.com/calvinmclean/automated-garden/garden-app/pkg/storage" + "github.com/calvinmclean/automated-garden/garden-app/server/vcr" "github.com/calvinmclean/automated-garden/garden-app/worker" + "github.com/calvinmclean/babyapi" "github.com/calvinmclean/babyapi/html" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -42,9 +46,6 @@ func NewAPI() *API { api.gardens.AddNestedAPI(api.zones) api.API. - AddMiddleware(std.HandlerProvider("", metrics_middleware.New(metrics_middleware.Config{ - Recorder: prommetrics.NewRecorder(prommetrics.Config{Prefix: "garden_app"}), - }))). AddCustomRoute(http.MethodGet, "/metrics", promhttp.Handler()). AddCustomRoute(http.MethodGet, "/", http.RedirectHandler("/gardens", http.StatusFound)). AddNestedAPI(api.gardens). @@ -52,9 +53,28 @@ func NewAPI() *API { AddNestedAPI(api.notificationClients). AddNestedAPI(api.waterSchedules) + cassetteName := os.Getenv("VCR_CASSETTE") + if cassetteName != "" { + EnableMock() + vcr.MustSetupVCR(cassetteName) + } + return api } +// EnableMock prepares mock IDs and clock +func EnableMock() { + enableMockIDs = true + mockIDIndex = 0 + _ = clock.MockTime() +} + +// DisableMock will disable mock IDs and reset the mock clock +func DisableMock() { + enableMockIDs = false + clock.Reset() +} + // Setup will prepare to run by setting up clients and doing any final configurations for the API func (api *API) Setup(cfg Config, validateData bool) error { html.SetFS(templates, "templates/*") @@ -63,6 +83,12 @@ func (api *API) Setup(cfg Config, validateData bool) error { logger := cfg.LogConfig.NewLogger().With("source", "server") slog.SetDefault(logger) + if !cfg.WebConfig.DisableMetrics { + api.API.AddMiddleware(std.HandlerProvider("", metrics_middleware.New(metrics_middleware.Config{ + Recorder: prommetrics.NewRecorder(prommetrics.Config{Prefix: "garden_app"}), + }))) + } + // Initialize Storage Client logger.Info("initializing storage client", "driver", cfg.StorageConfig.Driver) storageClient, err := storage.NewClient(cfg.StorageConfig) diff --git a/garden-app/server/api_test.go b/garden-app/server/api_test.go index 5600354e..54a6c9c2 100644 --- a/garden-app/server/api_test.go +++ b/garden-app/server/api_test.go @@ -7,6 +7,7 @@ import ( "github.com/calvinmclean/automated-garden/garden-app/pkg" "github.com/calvinmclean/automated-garden/garden-app/pkg/storage" "github.com/calvinmclean/automated-garden/garden-app/pkg/weather" + "github.com/calvinmclean/babyapi" "github.com/stretchr/testify/assert" ) diff --git a/garden-app/server/config.go b/garden-app/server/config.go index da9a154f..6b8f7f4c 100644 --- a/garden-app/server/config.go +++ b/garden-app/server/config.go @@ -17,6 +17,7 @@ type Config struct { // WebConfig is used to allow reading the "web_server" section into the main Config struct type WebConfig struct { - Port int `mapstructure:"port"` - ReadOnly bool `mapstructure:"readonly"` + Port int `mapstructure:"port"` + ReadOnly bool `mapstructure:"readonly"` + DisableMetrics bool `mapstructure:"disable_metrics"` } diff --git a/garden-app/server/garden.go b/garden-app/server/garden.go index 0cbc60f0..02799c99 100644 --- a/garden-app/server/garden.go +++ b/garden-app/server/garden.go @@ -59,7 +59,7 @@ func NewGardenAPI() *GardensAPI { switch r.URL.Query().Get("type") { case "create_modal": return api.gardenModalRenderer(r.Context(), &pkg.Garden{ - ID: babyapi.NewID(), + ID: NewID(), }) default: return babyapi.ErrInvalidRequest(fmt.Errorf("invalid component: %s", r.URL.Query().Get("type"))) diff --git a/garden-app/server/garden_test.go b/garden-app/server/garden_test.go index 8199d09d..4885ede2 100644 --- a/garden-app/server/garden_test.go +++ b/garden-app/server/garden_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg" "github.com/calvinmclean/automated-garden/garden-app/pkg/influxdb" "github.com/calvinmclean/automated-garden/garden-app/pkg/mqtt" @@ -68,7 +69,7 @@ func TestGetGarden(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { influxdbClient := new(influxdb.MockClient) - influxdbClient.On("GetLastContact", mock.Anything, "test-garden").Return(time.Now(), nil) + influxdbClient.On("GetLastContact", mock.Anything, "test-garden").Return(clock.Now(), nil) storageClient := setupZoneAndGardenStorage(t) gr := NewGardenAPI() @@ -162,7 +163,7 @@ func TestCreateGarden(t *testing.T) { assert.NoError(t, err) influxdbClient := new(influxdb.MockClient) - influxdbClient.On("GetLastContact", mock.Anything, "test-garden").Return(time.Now(), nil) + influxdbClient.On("GetLastContact", mock.Anything, "test-garden").Return(clock.Now(), nil) if tt.temperatureHumidityError { influxdbClient.On("GetTemperatureAndHumidity", mock.Anything, "test-garden").Return(0.0, 0.0, errors.New("influxdb error")) } else { @@ -262,7 +263,7 @@ func TestUpdateGardenPUT(t *testing.T) { assert.NoError(t, err) influxdbClient := new(influxdb.MockClient) - influxdbClient.On("GetLastContact", mock.Anything, "test-garden").Return(time.Now(), nil) + influxdbClient.On("GetLastContact", mock.Anything, "test-garden").Return(clock.Now(), nil) if tt.temperatureHumidityError { influxdbClient.On("GetTemperatureAndHumidity", mock.Anything, "test-garden").Return(0.0, 0.0, errors.New("influxdb error")) } else { @@ -320,7 +321,7 @@ func TestGetAllGardens(t *testing.T) { } influxdbClient := new(influxdb.MockClient) - influxdbClient.On("GetLastContact", mock.Anything, "test-garden").Return(time.Now(), nil) + influxdbClient.On("GetLastContact", mock.Anything, "test-garden").Return(clock.Now(), nil) gr := NewGardenAPI() err = gr.setup(Config{}, storageClient, influxdbClient, worker.NewWorker(storageClient, nil, nil, slog.Default())) @@ -337,7 +338,7 @@ func TestGetAllGardens(t *testing.T) { } func TestEndDateGarden(t *testing.T) { - now := time.Now() + now := clock.Now() endDatedGarden := createExampleGarden() endDatedGarden.EndDate = &now @@ -486,7 +487,7 @@ func TestUpdateGarden(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { influxdbClient := new(influxdb.MockClient) - influxdbClient.On("GetLastContact", mock.Anything, "test-garden").Return(time.Now(), nil) + influxdbClient.On("GetLastContact", mock.Anything, "test-garden").Return(clock.Now(), nil) storageClient := setupZoneAndGardenStorage(t) err := storageClient.NotificationClientConfigs.Set(context.Background(), notificationClient) @@ -837,7 +838,7 @@ func TestGardenRequest(t *testing.T) { } func TestUpdateGardenRequest(t *testing.T) { - now := time.Now() + now := clock.Now() zero := uint(0) tests := []struct { name string diff --git a/garden-app/server/id.go b/garden-app/server/id.go new file mode 100644 index 00000000..fb423e53 --- /dev/null +++ b/garden-app/server/id.go @@ -0,0 +1,41 @@ +package server + +import ( + "github.com/calvinmclean/automated-garden/garden-app/clock" + "github.com/calvinmclean/babyapi" + + "github.com/rs/xid" +) + +var ( + ids = []string{ + "cqsnecmiuvoqlhrmf2jg", + "cqsnecmiuvoqlhrmf2k0", + "cqsnecmiuvoqlhrmf2kg", + "cqsnecmiuvoqlhrmf2l0", + "cqsnecmiuvoqlhrmf2lg", + "cqsnecmiuvoqlhrmf2m0", + "cqsnecmiuvoqlhrmf2mg", + "cqsnecmiuvoqlhrmf2n0", + "cqsnecmiuvoqlhrmf2ng", + "cqsnecmiuvoqlhrmf2o0", + } + mockIDIndex = 0 + enableMockIDs = false +) + +// NewID creates a new unique xid by default, but mocking can be enabled to choose +// from a consistent list of IDs so results are repeatable +func NewID() babyapi.ID { + if !enableMockIDs { + return babyapi.ID{ID: xid.NewWithTime(clock.Now())} + } + + id, err := xid.FromString(ids[mockIDIndex]) + if err != nil { + panic(err) + } + mockIDIndex++ + + return babyapi.ID{ID: id} +} diff --git a/garden-app/server/templates.go b/garden-app/server/templates.go index bf1f423f..02a5c07b 100644 --- a/garden-app/server/templates.go +++ b/garden-app/server/templates.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg" "github.com/calvinmclean/babyapi" "github.com/calvinmclean/babyapi/html" @@ -54,7 +55,7 @@ func templateFuncs(r *http.Request) map[string]any { }, "ToLower": strings.ToLower, "FormatUpcomingDate": func(date *time.Time) string { - now := time.Now() + now := clock.Now() if date.YearDay() == now.YearDay() && date.Year() == now.Year() { return date.Format("at 3:04PM") } @@ -77,7 +78,7 @@ func templateFuncs(r *http.Request) map[string]any { return c*1.8 + 32 }, "timeNow": func() time.Time { - return time.Now() + return clock.Now() }, "URLContains": func(input string) bool { return strings.Contains(r.URL.Path, input) diff --git a/garden-app/server/templates/garden_modal.html b/garden-app/server/templates/garden_modal.html index 49e5b138..e1e0b46c 100644 --- a/garden-app/server/templates/garden_modal.html +++ b/garden-app/server/templates/garden_modal.html @@ -37,7 +37,7 @@

{{ if .Name }}{{ .Name }}{{ else }}Create Garden{{ en
{{ $noClientSelected := true }} $noClientSelected = eq .NotificationClientID nil - + {{ $g := . }} {{ range $i, $nc := .NotificationClients }} diff --git a/garden-app/server/vcr/testdata/vcr_server/config.yaml b/garden-app/server/vcr/testdata/vcr_server/config.yaml new file mode 100644 index 00000000..9eb85ff8 --- /dev/null +++ b/garden-app/server/vcr/testdata/vcr_server/config.yaml @@ -0,0 +1,20 @@ +web_server: + port: 8080 + disable_metrics: true +mqtt: + broker: "localhost" + port: 1883 + client_id: "garden-app" + water_topic: "{{.Garden}}/command/water" + stop_topic: "{{.Garden}}/command/stop" + stop_all_topic: "{{.Garden}}/command/stop_all" + light_topic: "{{.Garden}}/command/light" +influxdb: + address: "http://localhost:8086" + token: "my-token" + org: "garden" + bucket: "garden" +storage: + driver: "hashmap" + # options: + # filename: "./testdata/vcr_server/gardens.yaml" diff --git a/garden-app/server/vcr/testdata/vcr_server/fixtures/create_and_delete_waterschedule.yaml b/garden-app/server/vcr/testdata/vcr_server/fixtures/create_and_delete_waterschedule.yaml new file mode 100644 index 00000000..7cd372fe --- /dev/null +++ b/garden-app/server/vcr/testdata/vcr_server/fixtures/create_and_delete_waterschedule.yaml @@ -0,0 +1,894 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: / + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - none + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/ + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: |+ + Found. + + headers: + Content-Type: + - text/html; charset=utf-8 + Location: + - /gardens + status: 302 Found + code: 302 + duration: 125ns + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /gardens + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - none + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n
\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 292ns + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /favicon.ico + body: "" + form: {} + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - image + Sec-Fetch-Mode: + - no-cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/favicon.ico + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + 404 page not found + headers: + Content-Type: + - text/plain; charset=utf-8 + X-Content-Type-Options: + - nosniff + status: 404 Not Found + code: 404 + duration: 83ns + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57959' + request_uri: /apple-touch-icon-precomposed.png + body: "" + form: {} + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + User-Agent: + - Safari/19618.2.12.11.6 CFNetwork/1496.0.7 Darwin/23.5.0 + url: http://go-vcr/apple-touch-icon-precomposed.png + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + 404 page not found + headers: + Content-Type: + - text/plain; charset=utf-8 + X-Content-Type-Options: + - nosniff + status: 404 Not Found + code: 404 + duration: 250ns + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57959' + request_uri: /apple-touch-icon.png + body: "" + form: {} + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + User-Agent: + - Safari/19618.2.12.11.6 CFNetwork/1496.0.7 Darwin/23.5.0 + url: http://go-vcr/apple-touch-icon.png + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + 404 page not found + headers: + Content-Type: + - text/plain; charset=utf-8 + X-Content-Type-Options: + - nosniff + status: 404 Not Found + code: 404 + duration: 83ns + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /water_schedules?exclude_weather_data=true + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - same-origin + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules?exclude_weather_data=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n
\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 125ns + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /water_schedules?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 84ns + - id: 7 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /water_schedules/components?type=create_modal + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Hx-Target: + - create-modal-here + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules/components?type=create_modal + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n
\n
\n

Create Water Schedule\n

\n\n
\n \n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n\n \n
\n
\n\n
\n \n
\n
\n \n
\n \n
\n \n \n
\n\n \n\n\n \n \n\n\n
\n
\n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 333ns + - id: 8 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 241 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /water_schedules/cqsnecmiuvoqlhrmf2jg + body: ID=cqsnecmiuvoqlhrmf2jg&Name=New%20Water%20Schedule&Description=New&Duration=1h&Interval=72h&StartTime.Hour=5&StartTime.Minute=0&StartTime.TZ=-07%3A00&ActivePeriod.StartMonth=&ActivePeriod.EndMonth=&NotificationClientID=Notification%20Client + form: + ActivePeriod.EndMonth: + - "" + ActivePeriod.StartMonth: + - "" + Description: + - New + Duration: + - 1h + ID: + - cqsnecmiuvoqlhrmf2jg + Interval: + - 72h + Name: + - New Water Schedule + NotificationClientID: + - Notification Client + StartTime.Hour: + - "5" + StartTime.Minute: + - "0" + StartTime.TZ: + - "-07:00" + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Content-Length: + - "241" + Content-Type: + - application/x-www-form-urlencoded + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Origin: + - http://localhost:8080 + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules/cqsnecmiuvoqlhrmf2jg + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + {"id":"cqsnecmiuvoqlhrmf2jg","duration":"1h0m0s","interval":"72h0m0s","start_date":"2023-08-23T10:00:00Z","start_time":"05:00:00-07:00","name":"New Water Schedule","description":"New","notification_client_id":"Notification Client","next_water":{"time":"2023-08-23T05:00:00-07:00","duration":"1h0m0s"},"links":[{"rel":"self","href":"/water_schedules/cqsnecmiuvoqlhrmf2jg"}]} + headers: + Content-Type: + - application/json + Hx-Trigger: + - newWaterSchedule + status: 200 OK + code: 200 + duration: 41ns + - id: 9 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /water_schedules?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n \n\n\n
\n
\n
\n
\n

\n New Water Schedule\n

\n \n
\n \n \n
\n\n
\n
\n \n \n\n\n
\n Watering for\n 1h\n at 5:00AM\n
\n\n\n\n\n\n \n\n \n
\n

New

\n \n 1h\n \n \n 5:00AM\n \n \n 3 days\n \n \n
\n\n
\n
\n
\n\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 125ns + - id: 10 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /water_schedules/cqsnecmiuvoqlhrmf2jg/components?type=edit_modal + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Hx-Target: + - edit-modal-here + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules/cqsnecmiuvoqlhrmf2jg/components?type=edit_modal + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n
\n
\n

New Water Schedule\n

\n\n
\n \n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n\n \n
\n
\n\n
\n \n
\n
\n \n
\n \n
\n \n \n
\n\n \n\n\n \n \n\n\n
\n \n
\n \n
\n
\n\n \n \n\n\n
\n
\n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 167ns + - id: 11 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 251 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /water_schedules/cqsnecmiuvoqlhrmf2jg + body: ID=cqsnecmiuvoqlhrmf2jg&Name=Old%20Water%20Schedule&Description=New&Duration=1h0m0s&Interval=72h0m0s&StartTime.Hour=05&StartTime.Minute=00&StartTime.TZ=-07%3A00&ActivePeriod.StartMonth=&ActivePeriod.EndMonth=&NotificationClientID=Notification%20Client + form: + ActivePeriod.EndMonth: + - "" + ActivePeriod.StartMonth: + - "" + Description: + - New + Duration: + - 1h0m0s + ID: + - cqsnecmiuvoqlhrmf2jg + Interval: + - 72h0m0s + Name: + - Old Water Schedule + NotificationClientID: + - Notification Client + StartTime.Hour: + - "05" + StartTime.Minute: + - "00" + StartTime.TZ: + - "-07:00" + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Content-Length: + - "251" + Content-Type: + - application/x-www-form-urlencoded + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Origin: + - http://localhost:8080 + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules/cqsnecmiuvoqlhrmf2jg + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + {"id":"cqsnecmiuvoqlhrmf2jg","duration":"1h0m0s","interval":"72h0m0s","start_date":"2023-08-23T10:00:00Z","start_time":"05:00:00-07:00","name":"Old Water Schedule","description":"New","notification_client_id":"Notification Client","next_water":{"time":"2023-08-23T05:00:00-07:00","duration":"1h0m0s"},"links":[{"rel":"self","href":"/water_schedules/cqsnecmiuvoqlhrmf2jg"}]} + headers: + Content-Type: + - application/json + Hx-Trigger: + - newWaterSchedule + status: 200 OK + code: 200 + duration: 41ns + - id: 12 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /water_schedules?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n \n\n\n
\n
\n
\n
\n

\n Old Water Schedule\n

\n \n
\n \n \n
\n\n
\n
\n \n \n\n\n
\n Watering for\n 1h\n at 5:00AM\n
\n\n\n\n\n\n \n\n \n
\n

New

\n \n 1h\n \n \n 5:00AM\n \n \n 3 days\n \n \n
\n\n
\n
\n
\n\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 84ns + - id: 13 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /water_schedules/cqsnecmiuvoqlhrmf2jg/components?type=edit_modal + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Hx-Target: + - edit-modal-here + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules/cqsnecmiuvoqlhrmf2jg/components?type=edit_modal + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n
\n
\n

Old Water Schedule\n

\n\n
\n \n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n\n \n
\n
\n\n
\n \n
\n
\n \n
\n \n
\n \n \n
\n\n \n\n\n \n \n\n\n
\n \n
\n \n
\n
\n\n \n \n\n\n
\n
\n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 125ns + - id: 14 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /gardens + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - same-origin + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n
\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 84ns + - id: 15 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:57956' + request_uri: /gardens/components?type=create_modal + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Hx-Target: + - create-modal-here + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/components?type=create_modal + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n
\n

Create Garden

\n\n
\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n\n \n
\n \n
\n \n \n
\n\n \n\n\n \n \n\n\n
\n
\n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 167ns diff --git a/garden-app/server/vcr/testdata/vcr_server/fixtures/create_garden_simple.yaml b/garden-app/server/vcr/testdata/vcr_server/fixtures/create_garden_simple.yaml new file mode 100644 index 00000000..8da38471 --- /dev/null +++ b/garden-app/server/vcr/testdata/vcr_server/fixtures/create_garden_simple.yaml @@ -0,0 +1,399 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53109' + request_uri: / + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - none + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/ + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: |+ + Found. + + headers: + Content-Type: + - text/html; charset=utf-8 + Location: + - /gardens + status: 302 Found + code: 302 + duration: 83ns + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53109' + request_uri: /gardens + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - none + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n
\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 83ns + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53109' + request_uri: /gardens/components?type=create_modal + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Hx-Target: + - create-modal-here + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/components?type=create_modal + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n
\n

Create Garden

\n\n
\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n\n \n
\n \n
\n \n \n
\n\n \n\n\n \n \n\n\n
\n
\n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 125ns + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 223 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53109' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg + body: "" + form: + CreatedAt: + - "" + ID: + - cqsnecmiuvoqlhrmf2jg + LightSchedule.Duration: + - "" + LightSchedule.StartTime.Hour: + - "" + LightSchedule.StartTime.Minute: + - "" + LightSchedule.StartTime.TZ: + - Z + MaxZones: + - "1" + Name: + - New Garden + NotificationClientID: + - "" + TopicPrefix: + - new-garden + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Content-Length: + - "223" + Content-Type: + - application/x-www-form-urlencoded + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Origin: + - http://localhost:8080 + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + {"name":"New Garden","topic_prefix":"new-garden","id":"cqsnecmiuvoqlhrmf2jg","max_zones":1,"created_at":"2023-08-23T10:00:00Z","health":{"status":"N/A","details":"Post \"http://localhost:8086/api/v2/query?org=garden\": dial tcp [::1]:8086: connect: connection refused"},"num_zones":0,"links":[{"rel":"self","href":"/gardens/cqsnecmiuvoqlhrmf2jg"},{"rel":"zones","href":"/gardens/cqsnecmiuvoqlhrmf2jg/zones"},{"rel":"action","href":"/gardens/cqsnecmiuvoqlhrmf2jg/action"}]} + headers: + Content-Type: + - application/json + Hx-Trigger: + - newGarden + status: 200 OK + code: 200 + duration: 167ns + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53109' + request_uri: /gardens?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n \n
\n
\n
\n
\n

\n New Garden\n

\n \n
\n \n \n
\n\n
\n
\n \n \n\n\n\n N/A\n\n
\n

Post "http://localhost:8086/api/v2/query?org=garden": dial tcp [::1]:8086: connect: connection refused

\n
\n\n \n\n \n 0 Zones \n \n\n \n\n \n
\n
\n
\n
\n \n
\n \n
\n \n
\n
\n\n
\n
\n \n
\n \n
\n

cqsnecmiuvoqlhrmf2jg

\n

Topic prefix: new-garden

\n
\n
\n\n
\n
\n
\n
\n
\n\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 125ns + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53109' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - same-origin + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n

New Garden

\n\n
\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 209ns + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53109' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Hx-Request: + - "true" + Referer: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 125ns diff --git a/garden-app/server/vcr/testdata/vcr_server/fixtures/create_garden_with_light_schedule.yaml b/garden-app/server/vcr/testdata/vcr_server/fixtures/create_garden_with_light_schedule.yaml new file mode 100644 index 00000000..8cd53789 --- /dev/null +++ b/garden-app/server/vcr/testdata/vcr_server/fixtures/create_garden_with_light_schedule.yaml @@ -0,0 +1,644 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53125' + request_uri: / + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - none + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/ + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: |+ + Found. + + headers: + Content-Type: + - text/html; charset=utf-8 + Location: + - /gardens + status: 302 Found + code: 302 + duration: 250ns + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53125' + request_uri: /gardens + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - none + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n
\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 83ns + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53125' + request_uri: /gardens/components?type=create_modal + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Hx-Target: + - create-modal-here + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/components?type=create_modal + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n
\n

Create Garden

\n\n
\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n\n \n
\n \n
\n \n \n
\n\n \n\n\n \n \n\n\n
\n
\n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 166ns + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 228 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53125' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg + body: "" + form: + CreatedAt: + - "" + ID: + - cqsnecmiuvoqlhrmf2jg + LightSchedule.Duration: + - 10h + LightSchedule.StartTime.Hour: + - "8" + LightSchedule.StartTime.Minute: + - "0" + LightSchedule.StartTime.TZ: + - Z + MaxZones: + - "1" + Name: + - New Garden + NotificationClientID: + - "" + TopicPrefix: + - new-garden + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Content-Length: + - "228" + Content-Type: + - application/x-www-form-urlencoded + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Origin: + - http://localhost:8080 + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + {"name":"New Garden","topic_prefix":"new-garden","id":"cqsnecmiuvoqlhrmf2jg","max_zones":1,"created_at":"2023-08-23T10:00:00Z","light_schedule":{"duration":"10h0m0s","start_time":"08:00:00Z"},"next_light_action":{"time":"2023-08-23T18:00:00Z","state":"OFF"},"health":{"status":"N/A","details":"Post \"http://localhost:8086/api/v2/query?org=garden\": dial tcp [::1]:8086: connect: connection refused"},"num_zones":0,"links":[{"rel":"self","href":"/gardens/cqsnecmiuvoqlhrmf2jg"},{"rel":"zones","href":"/gardens/cqsnecmiuvoqlhrmf2jg/zones"},{"rel":"action","href":"/gardens/cqsnecmiuvoqlhrmf2jg/action"}]} + headers: + Content-Type: + - application/json + Hx-Trigger: + - newGarden + status: 200 OK + code: 200 + duration: 167ns + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53125' + request_uri: /gardens?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n \n
\n
\n
\n
\n

\n New Garden\n

\n \n
\n \n \n
\n\n
\n
\n \n \n\n\n\n N/A\n\n
\n

Post "http://localhost:8086/api/v2/query?org=garden": dial tcp [::1]:8086: connect: connection refused

\n
\n\n \n\n \n 0 Zones \n \n\n \n \n

\n \n \n \n \n Light will turn OFF at 6:00PM\n

\n\n

\n \n 10h\n 8:00AM\n \n

\n\n \n\n \n
\n
\n
\n
\n \n
\n \n
\n \n
\n
\n\n
\n
\n \n
\n \n
\n

cqsnecmiuvoqlhrmf2jg

\n

Topic prefix: new-garden

\n
\n
\n\n
\n
\n
\n
\n
\n\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 125ns + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53125' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - same-origin + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n

New Garden

\n\n
\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 125ns + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53125' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Hx-Request: + - "true" + Referer: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 42ns + - id: 7 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53125' + request_uri: /gardens + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Referer: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - same-origin + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n
\n \n \n
\n
\n
\n
\n

\n New Garden\n

\n \n
\n \n \n
\n\n
\n
\n \n \n\n\n\n N/A\n\n
\n

Post "http://localhost:8086/api/v2/query?org=garden": dial tcp [::1]:8086: connect: connection refused

\n
\n\n \n\n \n 0 Zones \n \n\n \n \n

\n \n \n \n \n Light will turn OFF at 6:00PM\n

\n\n

\n \n 10h\n 8:00AM\n \n

\n\n \n\n \n
\n
\n
\n
\n \n
\n \n
\n \n
\n
\n\n
\n
\n \n
\n \n
\n

cqsnecmiuvoqlhrmf2jg

\n

Topic prefix: new-garden

\n
\n
\n\n
\n
\n
\n
\n
\n\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 125ns + - id: 8 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 14 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53125' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/action + body: "" + form: + light.state: + - "ON" + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Content-Length: + - "14" + Content-Type: + - application/x-www-form-urlencoded + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Hx-Target: + - light-action-cqsnecmiuvoqlhrmf2jg-ON + Hx-Trigger: + - light-action-cqsnecmiuvoqlhrmf2jg-ON + Origin: + - http://localhost:8080 + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/action + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + {"status":"Server Error.","error":"unable to execute LightAction: unable to publish LightAction: unable to connect to MQTT broker: network Error : dial tcp [::1]:1883: connect: connection refused"} + headers: + Content-Type: + - application/json + status: 500 Internal Server Error + code: 500 + duration: 167ns + - id: 9 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 15 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53125' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/action + body: "" + form: + light.state: + - "OFF" + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Content-Length: + - "15" + Content-Type: + - application/x-www-form-urlencoded + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Hx-Target: + - light-action-cqsnecmiuvoqlhrmf2jg-OFF + Hx-Trigger: + - light-action-cqsnecmiuvoqlhrmf2jg-OFF + Origin: + - http://localhost:8080 + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/action + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + {"status":"Server Error.","error":"unable to execute LightAction: unable to publish LightAction: unable to connect to MQTT broker: network Error : dial tcp [::1]:1883: connect: connection refused"} + headers: + Content-Type: + - application/json + status: 500 Internal Server Error + code: 500 + duration: 250ns + - id: 10 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 13 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53125' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/action + body: "" + form: + stop.all: + - "true" + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Content-Length: + - "13" + Content-Type: + - application/x-www-form-urlencoded + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Origin: + - http://localhost:8080 + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/action + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + {"status":"Server Error.","error":"unable to execute StopAction: unable to connect to MQTT broker: network Error : dial tcp [::1]:1883: connect: connection refused"} + headers: + Content-Type: + - application/json + status: 500 Internal Server Error + code: 500 + duration: 83ns diff --git a/garden-app/server/vcr/testdata/vcr_server/fixtures/create_garden_ws_zone.yaml b/garden-app/server/vcr/testdata/vcr_server/fixtures/create_garden_ws_zone.yaml new file mode 100644 index 00000000..9bb3e7a1 --- /dev/null +++ b/garden-app/server/vcr/testdata/vcr_server/fixtures/create_garden_ws_zone.yaml @@ -0,0 +1,1559 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: / + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - none + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/ + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: |+ + Found. + + headers: + Content-Type: + - text/html; charset=utf-8 + Location: + - /gardens + status: 302 Found + code: 302 + duration: 250ns + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - none + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n
\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 83ns + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/components?type=create_modal + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Hx-Target: + - create-modal-here + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/components?type=create_modal + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n
\n

Create Garden

\n\n
\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n\n \n
\n \n
\n \n \n
\n\n \n\n\n \n \n\n\n
\n
\n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 458ns + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 223 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg + body: "" + form: + CreatedAt: + - "" + ID: + - cqsnecmiuvoqlhrmf2jg + LightSchedule.Duration: + - "" + LightSchedule.StartTime.Hour: + - "" + LightSchedule.StartTime.Minute: + - "" + LightSchedule.StartTime.TZ: + - Z + MaxZones: + - "1" + Name: + - New Garden + NotificationClientID: + - "" + TopicPrefix: + - new-garden + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Content-Length: + - "223" + Content-Type: + - application/x-www-form-urlencoded + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Origin: + - http://localhost:8080 + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + {"name":"New Garden","topic_prefix":"new-garden","id":"cqsnecmiuvoqlhrmf2jg","max_zones":1,"created_at":"2023-08-23T10:00:00Z","health":{"status":"N/A","details":"Post \"http://localhost:8086/api/v2/query?org=garden\": dial tcp [::1]:8086: connect: connection refused"},"num_zones":0,"links":[{"rel":"self","href":"/gardens/cqsnecmiuvoqlhrmf2jg"},{"rel":"zones","href":"/gardens/cqsnecmiuvoqlhrmf2jg/zones"},{"rel":"action","href":"/gardens/cqsnecmiuvoqlhrmf2jg/action"}]} + headers: + Content-Type: + - application/json + Hx-Trigger: + - newGarden + status: 200 OK + code: 200 + duration: 250ns + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n \n
\n
\n
\n
\n

\n New Garden\n

\n \n
\n \n \n
\n\n
\n
\n \n \n\n\n\n N/A\n\n
\n

Post "http://localhost:8086/api/v2/query?org=garden": dial tcp [::1]:8086: connect: connection refused

\n
\n\n \n\n \n 0 Zones \n \n\n \n\n \n
\n
\n
\n
\n \n
\n \n
\n \n
\n
\n\n
\n
\n \n
\n \n
\n

cqsnecmiuvoqlhrmf2jg

\n

Topic prefix: new-garden

\n
\n
\n\n
\n
\n
\n
\n
\n\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 250ns + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/components?type=edit_modal + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Hx-Target: + - edit-modal-here + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/components?type=edit_modal + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n
\n

New Garden

\n\n
\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n\n \n
\n \n
\n \n \n
\n\n \n\n\n \n \n\n\n
\n \n
\n \n
\n
\n\n \n \n\n\n
\n
\n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 292ns + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 247 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg + body: "" + form: + CreatedAt: + - "2023-08-23T10:00:00Z" + ID: + - cqsnecmiuvoqlhrmf2jg + LightSchedule.Duration: + - "" + LightSchedule.StartTime.Hour: + - "" + LightSchedule.StartTime.Minute: + - "" + LightSchedule.StartTime.TZ: + - Z + MaxZones: + - "2" + Name: + - New Garden + NotificationClientID: + - "" + TopicPrefix: + - new-garden + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Content-Length: + - "247" + Content-Type: + - application/x-www-form-urlencoded + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Origin: + - http://localhost:8080 + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + {"name":"New Garden","topic_prefix":"new-garden","id":"cqsnecmiuvoqlhrmf2jg","max_zones":2,"created_at":"2023-08-23T10:00:00Z","health":{"status":"N/A","details":"Post \"http://localhost:8086/api/v2/query?org=garden\": dial tcp [::1]:8086: connect: connection refused"},"num_zones":0,"links":[{"rel":"self","href":"/gardens/cqsnecmiuvoqlhrmf2jg"},{"rel":"zones","href":"/gardens/cqsnecmiuvoqlhrmf2jg/zones"},{"rel":"action","href":"/gardens/cqsnecmiuvoqlhrmf2jg/action"}]} + headers: + Content-Type: + - application/json + Hx-Trigger: + - newGarden + status: 200 OK + code: 200 + duration: 416ns + - id: 7 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens + Hx-Request: + - "true" + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n \n
\n
\n
\n
\n

\n New Garden\n

\n \n
\n \n \n
\n\n
\n
\n \n \n\n\n\n N/A\n\n
\n

Post "http://localhost:8086/api/v2/query?org=garden": dial tcp [::1]:8086: connect: connection refused

\n
\n\n \n\n \n 0 Zones \n \n\n \n\n \n
\n
\n
\n
\n \n
\n \n
\n \n
\n
\n\n
\n
\n \n
\n \n
\n

cqsnecmiuvoqlhrmf2jg

\n

Topic prefix: new-garden

\n
\n
\n\n
\n
\n
\n
\n
\n\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 375ns + - id: 8 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /water_schedules?exclude_weather_data=true + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - same-origin + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules?exclude_weather_data=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n
\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 458ns + - id: 9 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /water_schedules?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 292ns + - id: 10 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - same-origin + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n
\n \n \n
\n
\n
\n
\n

\n New Garden\n

\n \n
\n \n \n
\n\n
\n
\n \n \n\n\n\n N/A\n\n
\n

Post "http://localhost:8086/api/v2/query?org=garden": dial tcp [::1]:8086: connect: connection refused

\n
\n\n \n\n \n 0 Zones \n \n\n \n\n \n
\n
\n
\n
\n \n
\n \n
\n \n
\n
\n\n
\n
\n \n
\n \n
\n

cqsnecmiuvoqlhrmf2jg

\n

Topic prefix: new-garden

\n
\n
\n\n
\n
\n
\n
\n
\n\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 291ns + - id: 11 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - same-origin + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n

New Garden

\n\n
\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 500ns + - id: 12 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Hx-Request: + - "true" + Referer: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 41ns + - id: 13 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones/components?type=create_modal + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Hx-Request: + - "true" + Hx-Target: + - create-modal-here + Referer: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones/components?type=create_modal + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n
\n

\n Create Zone\n

\n\n
\n\n \n \n\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n \n
\n \n \n
\n\n
\n \n\n\n \n \n\n\n
\n
\n
\n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 375ns + - id: 14 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 102 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones/cqsnecmiuvoqlhrmf2k0 + body: "" + form: + CreatedAt: + - "" + Details.Description: + - Zone! + Details.Notes: + - "" + ID: + - cqsnecmiuvoqlhrmf2k0 + Name: + - New Zone + Position: + - "0" + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Content-Length: + - "102" + Content-Type: + - application/x-www-form-urlencoded + Hx-Current-Url: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Hx-Request: + - "true" + Origin: + - http://localhost:8080 + Referer: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones/cqsnecmiuvoqlhrmf2k0 + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n

New Zone

\n
\n \n
\n
\n
\n
\n

Actions

\n
\n \n \n
\n
\n
\n
\n
\n
\n

\n \n Water Schedule\n \n

\n \n\n\n\n\n
\n

no active WaterSchedules

\n
\n\n\n\n
\n
\n
\n
\n

Details

\n \n

Zone!

\n

\n \n
\n
\n
\n
\n\n \n
\n
\n
\n

Water History

\n
\n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n
TimeDuration
Post "http://localhost:8086/api/v2/query?org=garden": dial tcp [::1]:8086: connect: connection refused
\n
\n
\n\n
\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + Hx-Trigger: + - newZone + status: 200 OK + code: 200 + duration: 292ns + - id: 15 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Hx-Request: + - "true" + Referer: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n \n
\n
\n
\n
\n

\n New Zone\n

\n \n
\n \n \n
\n\n
\n
\n \n\n\n\n\n
\n

no active WaterSchedules

\n
\n\n\n\n
\n
\n
\n
\n \n
\n
\n \n
\n \n
\n
\n\n
\n
\n \n
\n
\n

cqsnecmiuvoqlhrmf2k0

\n

Position: 0

\n

Water Schedules: []

\n
\n
\n\n
\n
\n
\n
\n
\n\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 83ns + - id: 16 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /water_schedules?exclude_weather_data=true + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Referer: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - same-origin + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules?exclude_weather_data=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n
\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 291ns + - id: 17 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /water_schedules?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 42ns + - id: 18 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /water_schedules/components?type=create_modal + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Hx-Target: + - create-modal-here + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules/components?type=create_modal + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n
\n
\n

Create Water Schedule\n

\n\n
\n \n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n\n \n
\n
\n\n
\n \n
\n
\n \n
\n \n
\n \n \n
\n\n \n\n\n \n \n\n\n
\n
\n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 333ns + - id: 19 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 223 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /water_schedules/cqsnecmiuvoqlhrmf2kg + body: "" + form: + ActivePeriod.EndMonth: + - "" + ActivePeriod.StartMonth: + - "" + Description: + - daily + Duration: + - 10m + ID: + - cqsnecmiuvoqlhrmf2kg + Interval: + - 24h + Name: + - New WS + NotificationClientID: + - Notification Client + StartTime.Hour: + - "7" + StartTime.Minute: + - "0" + StartTime.TZ: + - Z + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Content-Length: + - "223" + Content-Type: + - application/x-www-form-urlencoded + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Origin: + - http://localhost:8080 + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules/cqsnecmiuvoqlhrmf2kg + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: | + {"id":"cqsnecmiuvoqlhrmf2kg","duration":"10m0s","interval":"24h0m0s","start_date":"2023-08-23T10:00:00Z","start_time":"07:00:00Z","name":"New WS","description":"daily","notification_client_id":"Notification Client","next_water":{"time":"2023-08-24T07:00:00Z","duration":"10m0s"},"links":[{"rel":"self","href":"/water_schedules/cqsnecmiuvoqlhrmf2kg"}]} + headers: + Content-Type: + - application/json + Hx-Trigger: + - newWaterSchedule + status: 200 OK + code: 200 + duration: 542ns + - id: 20 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /water_schedules?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Hx-Request: + - "true" + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/water_schedules?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n \n\n\n
\n
\n
\n
\n

\n New WS\n

\n \n
\n \n \n
\n\n
\n
\n \n \n\n\n
\n Watering for\n 10m\n on Thursday, 24 Aug at 7:00AM\n
\n\n\n\n\n\n \n\n \n
\n

daily

\n \n 10m\n \n \n 7:00AM\n \n \n 1 days\n \n \n
\n\n
\n
\n
\n\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 500ns + - id: 21 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Referer: + - http://localhost:8080/water_schedules?exclude_weather_data=true + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - same-origin + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n
\n \n \n
\n
\n
\n
\n

\n New Garden\n

\n \n
\n \n \n
\n\n
\n
\n \n \n\n\n\n N/A\n\n
\n

Post "http://localhost:8086/api/v2/query?org=garden": dial tcp [::1]:8086: connect: connection refused

\n
\n\n \n\n \n 1 Zones \n \n\n \n\n \n
\n
\n
\n
\n \n
\n \n
\n \n
\n
\n\n
\n
\n \n
\n \n
\n

cqsnecmiuvoqlhrmf2jg

\n

Topic prefix: new-garden

\n
\n
\n\n
\n
\n
\n
\n
\n\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 334ns + - id: 22 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + body: "" + form: {} + headers: + Accept: + - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Referer: + - http://localhost:8080/gardens + Sec-Fetch-Dest: + - document + Sec-Fetch-Mode: + - navigate + Sec-Fetch-Site: + - same-origin + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n

New Garden

\n\n
\n \n \n
\n
\n
\n
\n

\n New Zone\n

\n \n
\n \n \n
\n\n
\n
\n \n\n
\n
\n
\n\n\n
\n
\n
\n
\n \n
\n
\n \n
\n \n
\n
\n\n
\n
\n \n
\n
\n

cqsnecmiuvoqlhrmf2k0

\n

Position: 0

\n

Water Schedules: []

\n
\n
\n\n
\n
\n
\n
\n
\n\n \n
\n\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 83ns + - id: 23 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Hx-Request: + - "true" + Referer: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n \n
\n
\n
\n
\n

\n New Zone\n

\n \n
\n \n \n
\n\n
\n
\n \n\n\n\n\n
\n

no active WaterSchedules

\n
\n\n\n\n
\n
\n
\n
\n \n
\n
\n \n
\n \n
\n
\n\n
\n
\n \n
\n
\n

cqsnecmiuvoqlhrmf2k0

\n

Position: 0

\n

Water Schedules: []

\n
\n
\n\n
\n
\n
\n
\n
\n\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 83ns + - id: 24 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones/cqsnecmiuvoqlhrmf2k0/components?type=edit_modal + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Hx-Request: + - "true" + Hx-Target: + - edit-modal-here + Referer: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones/cqsnecmiuvoqlhrmf2k0/components?type=edit_modal + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n
\n

\n Edit New Zone\n

\n\n
\n\n \n \n\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n \n
\n \n \n \n \n
\n \n
\n \n
\n\n
\n \n\n\n \n \n\n\n
\n \n
\n \n
\n
\n\n \n \n\n\n
\n
\n
\n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 375ns + - id: 25 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 182 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones/cqsnecmiuvoqlhrmf2k0 + body: "" + form: + CreatedAt: + - "2023-08-23T10:00:00Z" + Details.Description: + - Now with watering + Details.Notes: + - "" + ID: + - cqsnecmiuvoqlhrmf2k0 + Name: + - New Zone + Position: + - "0" + WaterScheduleIDs.0: + - cqsnecmiuvoqlhrmf2kg + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Content-Length: + - "182" + Content-Type: + - application/x-www-form-urlencoded + Hx-Current-Url: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Hx-Request: + - "true" + Origin: + - http://localhost:8080 + Referer: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones/cqsnecmiuvoqlhrmf2k0 + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n\n\n\n\n\n \n \n Garden App\n \n \n \n \n \n\n\n\n\n\n
\n \n \n\n

New Zone

\n
\n \n
\n
\n
\n
\n

Actions

\n
\n \n \n
\n
\n
\n
\n
\n
\n

\n \n \n Water Schedule\n \n \n

\n \n\n\n
\n Watering for\n 10m\n on Thursday, 24 Aug at 7:00AM\n
\n\n\n\n\n\n
\n
\n
\n
\n

Details

\n \n

Now with watering

\n

\n \n
\n
\n
\n
\n\n \n
\n
\n
\n

Water History

\n
\n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n
TimeDuration
Post "http://localhost:8086/api/v2/query?org=garden": dial tcp [::1]:8086: connect: connection refused
\n
\n
\n\n
\n\n\n\n\n\n" + headers: + Content-Type: + - text/html; charset=utf-8 + Hx-Trigger: + - newZone + status: 200 OK + code: 200 + duration: 459ns + - id: 26 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: localhost:8080 + remote_addr: '[::1]:53584' + request_uri: /gardens/cqsnecmiuvoqlhrmf2jg/zones?refresh=true + body: "" + form: {} + headers: + Accept: + - text/html + Accept-Encoding: + - gzip, deflate + Accept-Language: + - en-US,en;q=0.9 + Connection: + - keep-alive + Hx-Current-Url: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Hx-Request: + - "true" + Referer: + - http://localhost:8080/gardens/cqsnecmiuvoqlhrmf2jg/zones?exclude_weather_data=true + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 + url: http://go-vcr/gardens/cqsnecmiuvoqlhrmf2jg/zones?refresh=true + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: -1 + uncompressed: false + body: "\n
\n \n \n
\n
\n
\n
\n

\n New Zone\n

\n \n
\n \n \n
\n\n
\n
\n \n\n\n
\n Watering for\n 10m\n on Thursday, 24 Aug at 7:00AM\n
\n\n\n\n\n\n
\n
\n
\n
\n \n
\n
\n \n
\n \n
\n
\n\n
\n
\n \n
\n
\n

cqsnecmiuvoqlhrmf2k0

\n

Position: 0

\n

Water Schedules: [cqsnecmiuvoqlhrmf2kg]

\n
\n
\n\n
\n
\n
\n
\n
\n\n \n
\n" + headers: + Content-Type: + - text/html; charset=utf-8 + status: 200 OK + code: 200 + duration: 292ns diff --git a/garden-app/server/vcr/vcr.go b/garden-app/server/vcr/vcr.go new file mode 100644 index 00000000..2c0d949d --- /dev/null +++ b/garden-app/server/vcr/vcr.go @@ -0,0 +1,35 @@ +package vcr + +import ( + "github.com/calvinmclean/babyapi" + + "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" +) + +var rec *recorder.Recorder + +func StopRecorder() { + if rec == nil { + return + } + + err := rec.Stop() + if err != nil { + panic(err) + } +} + +// MustSetupVCR will create a new Recorder and add it to babyapi.DefaultMiddleware. +// Panics if there is an error +func MustSetupVCR(cassetteName string) { + var err error + rec, err = recorder.New( + recorder.WithCassette(cassetteName), + recorder.WithMode(recorder.ModeRecordOnly), + ) + if err != nil { + panic(err) + } + + babyapi.DefaultMiddleware = append(babyapi.DefaultMiddleware, rec.HTTPMiddleware) +} diff --git a/garden-app/server/vcr/vcr_test.go b/garden-app/server/vcr/vcr_test.go new file mode 100644 index 00000000..765d730b --- /dev/null +++ b/garden-app/server/vcr/vcr_test.go @@ -0,0 +1,52 @@ +package vcr_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/calvinmclean/automated-garden/garden-app/server" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "gopkg.in/dnaeon/go-vcr.v4/pkg/cassette" +) + +func TestReplay(t *testing.T) { + dir := "testdata/vcr_server/fixtures" + entries, err := os.ReadDir(dir) + require.NoError(t, err) + + t.Cleanup(server.DisableMock) + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + cassetteName := strings.TrimSuffix(entry.Name(), ".yaml") + t.Run(cassetteName, func(t *testing.T) { + api := server.NewAPI() + server.EnableMock() + + var config server.Config + t.Run("SetupConfig", func(t *testing.T) { + viper.SetConfigFile("./testdata/vcr_server/config.yaml") + + err := viper.ReadInConfig() + require.NoError(t, err) + + err = viper.Unmarshal(&config) + require.NoError(t, err) + }) + + err := api.Setup(config, true) + require.NoError(t, err) + + r, err := api.Router() + require.NoError(t, err) + + cassette.TestServerReplay(t, filepath.Join(dir, cassetteName), r) + }) + } +} diff --git a/garden-app/server/water_notification_handler_test.go b/garden-app/server/water_notification_handler_test.go index df589129..b661d88f 100644 --- a/garden-app/server/water_notification_handler_test.go +++ b/garden-app/server/water_notification_handler_test.go @@ -12,8 +12,8 @@ import ( "github.com/calvinmclean/automated-garden/garden-app/pkg/storage" "github.com/calvinmclean/babyapi" "github.com/stretchr/testify/require" - "gopkg.in/dnaeon/go-vcr.v3/cassette" - "gopkg.in/dnaeon/go-vcr.v3/recorder" + "gopkg.in/dnaeon/go-vcr.v4/pkg/cassette" + "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" ) func TestParseWaterMessage(t *testing.T) { @@ -115,7 +115,7 @@ func TestHandleMessage(t *testing.T) { }) t.Run("ErrorUsingPushover", func(t *testing.T) { - r, err := recorder.New("testdata/fixtures/pushover_fail") + r, err := recorder.New(recorder.WithCassette("testdata/fixtures/pushover_fail")) if err != nil { t.Fatal(err) } @@ -136,7 +136,18 @@ func TestHandleMessage(t *testing.T) { }) t.Run("Success", func(t *testing.T) { - r, err := recorder.New("testdata/fixtures/pushover_success") + numMessages := 0 + + r, err := recorder.New( + recorder.WithCassette("testdata/fixtures/pushover_success"), + recorder.WithHook(func(i *cassette.Interaction) error { + // Use hook to count number of message requests + if i.Request.URL == "https://api.pushover.net/1/messages.json" { + numMessages++ + } + return nil + }, recorder.BeforeResponseReplayHook), + ) if err != nil { t.Fatal(err) } @@ -148,14 +159,6 @@ func TestHandleMessage(t *testing.T) { t.Fatal("Recorder should be in ModeRecordOnce") } - numMessages := 0 - r.AddHook(func(i *cassette.Interaction) error { - if i.Request.URL == "https://api.pushover.net/1/messages.json" { - numMessages++ - } - return nil - }, recorder.BeforeResponseReplayHook) - // github.com/gregdel/pushover uses http.DefaultClient http.DefaultClient = r.GetDefaultClient() diff --git a/garden-app/server/water_schedule.go b/garden-app/server/water_schedule.go index 190c4847..4c6f7b78 100644 --- a/garden-app/server/water_schedule.go +++ b/garden-app/server/water_schedule.go @@ -84,7 +84,7 @@ func NewWaterSchedulesAPI() *WaterSchedulesAPI { switch r.URL.Query().Get("type") { case "create_modal": return api.waterScheduleModalRenderer(r.Context(), &pkg.WaterSchedule{ - ID: babyapi.NewID(), + ID: NewID(), }) default: return babyapi.ErrInvalidRequest(fmt.Errorf("invalid component: %s", r.URL.Query().Get("type"))) diff --git a/garden-app/server/water_schedule_test.go b/garden-app/server/water_schedule_test.go index 0f1cb303..b28b3c3f 100644 --- a/garden-app/server/water_schedule_test.go +++ b/garden-app/server/water_schedule_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg" "github.com/calvinmclean/automated-garden/garden-app/pkg/influxdb" "github.com/calvinmclean/automated-garden/garden-app/pkg/storage" @@ -285,7 +286,7 @@ func TestUpdateWaterSchedule(t *testing.T) { } func TestEndDateWaterSchedule(t *testing.T) { - now := time.Now() + now := clock.Now() endDatedWaterSchedule := createExampleWaterSchedule() endDatedWaterSchedule.EndDate = &now endDatedWaterSchedule.ID = babyapi.ID{ID: id2} @@ -361,7 +362,7 @@ func TestGetAllWaterSchedules(t *testing.T) { waterSchedule := createExampleWaterSchedule() endDatedWaterSchedule := createExampleWaterSchedule() endDatedWaterSchedule.ID = babyapi.NewID() - now := time.Now() + now := clock.Now() endDatedWaterSchedule.EndDate = &now tests := []struct { @@ -564,7 +565,7 @@ func TestUpdateWaterSchedulePUT(t *testing.T) { } func TestWaterScheduleRequest(t *testing.T) { - now := time.Now() + now := clock.Now() tests := []struct { name string pr *pkg.WaterSchedule @@ -777,7 +778,7 @@ func TestWaterScheduleRequest(t *testing.T) { } func TestUpdateWaterScheduleRequest(t *testing.T) { - now := time.Now() + now := clock.Now() tests := []struct { name string pr *pkg.WaterSchedule diff --git a/garden-app/server/weather_clients.go b/garden-app/server/weather_clients.go index 76172e4e..4bd3b039 100644 --- a/garden-app/server/weather_clients.go +++ b/garden-app/server/weather_clients.go @@ -59,7 +59,7 @@ func NewWeatherClientsAPI() *WeatherClientsAPI { switch r.URL.Query().Get("type") { case "create_modal": return weatherClientModalTemplate.Renderer(&weather.Config{ - ID: babyapi.NewID(), + ID: NewID(), }) default: return babyapi.ErrInvalidRequest(fmt.Errorf("invalid component: %s", r.URL.Query().Get("type"))) diff --git a/garden-app/server/zone.go b/garden-app/server/zone.go index bda68b76..4cda0b59 100644 --- a/garden-app/server/zone.go +++ b/garden-app/server/zone.go @@ -60,7 +60,7 @@ func NewZonesAPI() *ZonesAPI { switch r.URL.Query().Get("type") { case "create_modal": modal, apiErr := api.createModal(r, &pkg.Zone{ - ID: babyapi.NewID(), + ID: NewID(), }) if apiErr != nil { return apiErr diff --git a/garden-app/server/zone_test.go b/garden-app/server/zone_test.go index 0eed49d1..6e5ad35f 100644 --- a/garden-app/server/zone_test.go +++ b/garden-app/server/zone_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg" "github.com/calvinmclean/automated-garden/garden-app/pkg/influxdb" "github.com/calvinmclean/automated-garden/garden-app/pkg/mqtt" @@ -481,7 +482,7 @@ func TestUpdateZone(t *testing.T) { } func TestEndDateZone(t *testing.T) { - now := time.Now() + now := clock.Now() endDatedZone := createExampleZone() endDatedZone.EndDate = &now @@ -597,7 +598,7 @@ func TestCreateZone(t *testing.T) { gardenWithZone.MaxZones = &one // Predict NextWaterTime so I can test it better - now := time.Now() + now := clock.Now() expectedNextWaterTime := time.Date(now.Year(), now.Month(), now.Day(), createdAt.Hour(), createdAt.Minute(), createdAt.Second(), createdAt.Nanosecond(), createdAt.Location()) if now.After(expectedNextWaterTime) { expectedNextWaterTime = expectedNextWaterTime.Add(24 * time.Hour) @@ -1071,7 +1072,7 @@ func TestZoneRequest(t *testing.T) { func TestUpdateZoneRequest(t *testing.T) { pp := uint(0) - now := time.Now() + now := clock.Now() tests := []struct { name string z *pkg.Zone diff --git a/garden-app/worker/garden_action_test.go b/garden-app/worker/garden_action_test.go index 4024d4a9..72b96e54 100644 --- a/garden-app/worker/garden_action_test.go +++ b/garden-app/worker/garden_action_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg" "github.com/calvinmclean/automated-garden/garden-app/pkg/action" "github.com/calvinmclean/automated-garden/garden-app/pkg/influxdb" @@ -106,7 +107,7 @@ func TestGardenAction(t *testing.T) { } func TestLightActionExecute(t *testing.T) { - now := time.Now() + now := clock.Now() startTime, _ := pkg.StartTimeFromString("23:00:00-07:00") garden := &pkg.Garden{ ID: babyapi.NewID(), diff --git a/garden-app/worker/scheduler.go b/garden-app/worker/scheduler.go index e9f5fc2c..c5ff200d 100644 --- a/garden-app/worker/scheduler.go +++ b/garden-app/worker/scheduler.go @@ -8,6 +8,7 @@ import ( "sort" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg" "github.com/calvinmclean/automated-garden/garden-app/pkg/action" "github.com/go-co-op/gocron" @@ -60,7 +61,7 @@ func (w *Worker) ScheduleWaterAction(waterSchedule *pkg.WaterSchedule) error { return errors.New("WaterSchedule not found") } - if !ws.IsActive(time.Now()) { + if !ws.IsActive(clock.Now()) { jobLogger.Info("skipping WaterSchedule because current time is outside of ActivePeriod", "active_period", *ws.ActivePeriod) return nil } @@ -183,7 +184,7 @@ func (w *Worker) ScheduleLightActions(g *pkg.Garden) error { lightTime := g.LightSchedule.StartTime.Time.UTC() - now := time.Now() + now := clock.Now() onStartDate := timeAtDate(&now, lightTime) offStartDate := onStartDate.Add(g.LightSchedule.Duration.Duration) @@ -217,7 +218,7 @@ func (w *Worker) ScheduleLightActions(g *pkg.Garden) error { if g.LightSchedule.AdhocOnTime != nil { logger.Debug("garden has adhoc ON time", "adhoc_on_time", g.LightSchedule.AdhocOnTime) // If AdhocOnTime is in the past, reset it and return - if g.LightSchedule.AdhocOnTime.Before(time.Now().UTC()) { + if g.LightSchedule.AdhocOnTime.Before(clock.Now().UTC()) { logger.Debug("adhoc ON time is in the past and is being removed") g.LightSchedule.AdhocOnTime = nil return w.storageClient.Gardens.Set(context.Background(), g) @@ -305,7 +306,7 @@ func (w *Worker) ScheduleLightDelay(g *pkg.Garden, input *action.LightAction) er // No need to change any schedules if nextOffTime.Before(*nextOnTime) { logger.Debug("next OFF time is before next ON time; setting schedule to turn light back on", "duration", input.ForDuration.Duration) - now := time.Now().UTC() + now := clock.Now().UTC() // Don't allow a delayDuration that will occur after nextOffTime if nextOffTime.Before(now.Add(input.ForDuration.Duration)) { @@ -500,7 +501,7 @@ func (w *Worker) executeLightActionInScheduledJob(g *pkg.Garden, input *action.L } func timeAtDate(date *time.Time, startTime time.Time) time.Time { - actualDate := time.Now() + actualDate := clock.Now() if date != nil { actualDate = *date } diff --git a/garden-app/worker/scheduler_test.go b/garden-app/worker/scheduler_test.go index 6ee082ab..7a830c9e 100644 --- a/garden-app/worker/scheduler_test.go +++ b/garden-app/worker/scheduler_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg" "github.com/calvinmclean/automated-garden/garden-app/pkg/action" "github.com/calvinmclean/automated-garden/garden-app/pkg/influxdb" @@ -95,7 +96,7 @@ func TestScheduleWaterActionStorageError(t *testing.T) { ws := createExampleWaterSchedule() // Set StartTime to the near future - startTime := pkg.NewStartTime(time.Now().Add(250 * time.Millisecond)) + startTime := pkg.NewStartTime(clock.Now().Add(250 * time.Millisecond)) ws.StartTime = startTime err = worker.ScheduleWaterAction(ws) @@ -142,14 +143,14 @@ func TestScheduleWaterAction(t *testing.T) { wsNotActive := createExampleWaterSchedule() wsNotActive.ID = babyapi.NewID() - currentTime := time.Now() + currentTime := clock.Now() wsNotActive.ActivePeriod = &pkg.ActivePeriod{ StartMonth: currentTime.AddDate(0, 1, 0).String(), EndMonth: currentTime.AddDate(0, 2, 0).String(), } // Set StartTime to the near future - startTime := pkg.NewStartTime(time.Now().Add(1 * time.Second)) + startTime := pkg.NewStartTime(clock.Now().Add(1 * time.Second)) ws.StartTime = startTime wsNotInStorage.StartTime = startTime wsNotActive.StartTime = startTime @@ -184,12 +185,12 @@ func TestScheduleWaterActionGardenHealthNotification(t *testing.T) { }{ { "GardenUp", - time.Now(), + clock.Now(), []fake.Message{}, }, { "GardenDown", - time.Now().Add(-10 * time.Minute), + clock.Now().Add(-10 * time.Minute), []fake.Message{ { Title: "test-garden: DOWN", @@ -242,7 +243,7 @@ func TestScheduleWaterActionGardenHealthNotification(t *testing.T) { ws := createExampleWaterSchedule() ws.Name = "MyWaterSchedule" // Set StartTime to the near future - ws.StartTime = pkg.NewStartTime(time.Now().Add(1 * time.Second)) + ws.StartTime = pkg.NewStartTime(clock.Now().Add(1 * time.Second)) ncID := notificationClient.GetID() ws.NotificationClientID = &ncID @@ -314,7 +315,7 @@ func TestScheduleWaterActionWithErrorNotification(t *testing.T) { influxdbClient := new(influxdb.MockClient) if tt.enableNotification { - influxdbClient.On("GetLastContact", mock.Anything, mock.Anything).Return(time.Now(), nil) + influxdbClient.On("GetLastContact", mock.Anything, mock.Anything).Return(clock.Now(), nil) } influxdbClient.On("Close").Return() @@ -324,7 +325,7 @@ func TestScheduleWaterActionWithErrorNotification(t *testing.T) { ws := createExampleWaterSchedule() ws.Name = "MyWaterSchedule" // Set StartTime to the near future - ws.StartTime = pkg.NewStartTime(time.Now().Add(1 * time.Second)) + ws.StartTime = pkg.NewStartTime(clock.Now().Add(1 * time.Second)) if tt.enableNotification { ncID := notificationClient.GetID() ws.NotificationClientID = &ncID @@ -364,7 +365,7 @@ func TestResetNextWaterTime(t *testing.T) { ws := createExampleWaterSchedule() // Set Zone's WaterSchedule.StartTime to a time that won't cause it to run - startTime := pkg.NewStartTime(time.Now().Add(-1 * time.Hour)) + startTime := pkg.NewStartTime(clock.Now().Add(-1 * time.Hour)) ws.StartTime = startTime err = worker.ScheduleWaterAction(ws) assert.NoError(t, err) @@ -385,7 +386,9 @@ func TestResetNextWaterTime(t *testing.T) { } func TestGetNextWaterTime(t *testing.T) { - now := time.Now() + mockClock := clock.MockTime() + now := mockClock.Now() + t.Cleanup(clock.Reset) tests := []struct { name string @@ -490,7 +493,7 @@ func TestGetNextWaterTimeWithInterval(t *testing.T) { // Set time to near future so it can execute and we can see the next interval delay := 100 * time.Millisecond - now := time.Now() + now := clock.Now() startTime := now.Add(delay) ws := createExampleWaterSchedule() @@ -530,7 +533,7 @@ func TestScheduleLightActions(t *testing.T) { worker.StartAsync() defer worker.Stop() - now := time.Now().UTC() + now := clock.Now().UTC() later := now.Add(1 * time.Hour).Truncate(time.Second) g := createExampleGarden() g.LightSchedule.AdhocOnTime = &later @@ -552,7 +555,7 @@ func TestScheduleLightActions(t *testing.T) { worker.StartAsync() defer worker.Stop() - now := time.Now().UTC() + now := clock.Now().UTC() past := now.Add(-1 * time.Hour) g := createExampleGarden() g.LightSchedule.AdhocOnTime = &past @@ -653,7 +656,7 @@ func TestScheduleLightActions(t *testing.T) { mqttClient.On("Disconnect", uint(100)).Return() influxdbClient := new(influxdb.MockClient) - influxdbClient.On("GetLastContact", mock.Anything, mock.Anything).Return(time.Now(), nil) + influxdbClient.On("GetLastContact", mock.Anything, mock.Anything).Return(clock.Now(), nil) influxdbClient.On("Close").Return() notificationClient := ¬ifications.Client{ @@ -670,7 +673,7 @@ func TestScheduleLightActions(t *testing.T) { defer worker.Stop() // Create new LightSchedule that turns on in 1 second for only 1 second - now := time.Now().UTC() + now := clock.Now().UTC() later := now.Add(1 * time.Second).Truncate(time.Second) g := createExampleGarden() g.LightSchedule.StartTime = pkg.NewStartTime(later) @@ -702,14 +705,14 @@ func TestScheduleLightActions(t *testing.T) { }{ { "GardenUp", - time.Now(), + clock.Now(), []fake.Message{ {Title: "test-garden: Light ON", Message: "Successfully executed LightAction"}, }, }, { "GardenDown", - time.Now().Add(-10 * time.Minute), + clock.Now().Add(-10 * time.Minute), []fake.Message{ { Title: "test-garden: DOWN", @@ -752,7 +755,7 @@ func TestScheduleLightActions(t *testing.T) { defer worker.Stop() // Create new LightSchedule that turns on in 1 second for only 1 second - now := time.Now().UTC() + now := clock.Now().UTC() later := now.Add(1 * time.Second).Truncate(time.Second) g := createExampleGarden() g.LightSchedule.StartTime = pkg.NewStartTime(later) @@ -786,7 +789,7 @@ func TestScheduleLightDelay(t *testing.T) { func() *pkg.Garden { g := createExampleGarden() // Set start time to a bit ago so the light is considered to be ON - g.LightSchedule.StartTime = pkg.NewStartTime(time.Now().Add(-1 * time.Minute)) + g.LightSchedule.StartTime = pkg.NewStartTime(clock.Now().Add(-1 * time.Minute)) return g }(), []*action.LightAction{ @@ -803,7 +806,7 @@ func TestScheduleLightDelay(t *testing.T) { func() *pkg.Garden { g := createExampleGarden() // Set start time to a bit ago so the light is considered to be ON - g.LightSchedule.StartTime = pkg.NewStartTime(time.Now().Add(-1 * time.Minute)) + g.LightSchedule.StartTime = pkg.NewStartTime(clock.Now().Add(-1 * time.Minute)) return g }(), []*action.LightAction{ @@ -824,7 +827,7 @@ func TestScheduleLightDelay(t *testing.T) { func() *pkg.Garden { g := createExampleGarden() // Set start time to the future so the light is considered to be OFF - g.LightSchedule.StartTime = pkg.NewStartTime(time.Now().Add(5 * time.Minute)) + g.LightSchedule.StartTime = pkg.NewStartTime(clock.Now().Add(5 * time.Minute)) return g }(), []*action.LightAction{ @@ -841,7 +844,7 @@ func TestScheduleLightDelay(t *testing.T) { func() *pkg.Garden { g := createExampleGarden() // Set start time to the future so the light is considered to be OFF - g.LightSchedule.StartTime = pkg.NewStartTime(time.Now().Add(5 * time.Minute)) + g.LightSchedule.StartTime = pkg.NewStartTime(clock.Now().Add(5 * time.Minute)) return g }(), []*action.LightAction{ @@ -875,7 +878,7 @@ func TestScheduleLightDelay(t *testing.T) { assert.NoError(t, err) // Now request delay - now := time.Now() + now := clock.Now() for _, action := range tt.actions { err = worker.ScheduleLightDelay(tt.garden, action) assert.NoError(t, err) @@ -915,7 +918,7 @@ func TestScheduleLightDelay(t *testing.T) { g := createExampleGarden() // Set StartTime and Duration so NextOffTime is soon - g.LightSchedule.StartTime = pkg.NewStartTime(time.Now().Add(-1 * time.Hour)) + g.LightSchedule.StartTime = pkg.NewStartTime(clock.Now().Add(-1 * time.Hour)) g.LightSchedule.Duration = &pkg.Duration{Duration: 1*time.Hour + 5*time.Minute} err = worker.ScheduleLightActions(g) @@ -926,12 +929,8 @@ func TestScheduleLightDelay(t *testing.T) { State: pkg.LightStateOff, ForDuration: &pkg.Duration{Duration: 30 * time.Minute}, }) - if err == nil { - t.Errorf("Expected error but got nil") - } - if err.Error() != "unable to schedule delay that extends past the light turning back on" { - t.Errorf("Unexpected error string: %v", err) - } + require.Error(t, err) + require.Equal(t, "unable to schedule delay that extends past the light turning back on", err.Error()) }) t.Run("ErrorDelayingLongerThanLightDuration", func(t *testing.T) { @@ -1010,7 +1009,7 @@ func TestRemoveJobsByID(t *testing.T) { ws := createExampleWaterSchedule() // Set Zone's WaterSchedule.StartTime to a time that won't cause it to run - startTime := pkg.NewStartTime(time.Now().Add(-1 * time.Hour)) + startTime := pkg.NewStartTime(clock.Now().Add(-1 * time.Hour)) ws.StartTime = startTime err = worker.ScheduleWaterAction(ws) assert.NoError(t, err) @@ -1033,7 +1032,7 @@ func TestGetNextWaterScheduleWithMultiple(t *testing.T) { worker := NewWorker(nil, nil, nil, slog.Default()) worker.scheduler.StartAsync() - now := time.Now() + now := clock.Now() addTime := func(add time.Duration) *pkg.StartTime { return pkg.NewStartTime(now.Add(add)) } diff --git a/garden-app/worker/worker.go b/garden-app/worker/worker.go index 4ff216a2..fe01d1ab 100644 --- a/garden-app/worker/worker.go +++ b/garden-app/worker/worker.go @@ -2,8 +2,10 @@ package worker import ( "log/slog" + "sync" "time" + "github.com/calvinmclean/automated-garden/garden-app/clock" "github.com/calvinmclean/automated-garden/garden-app/pkg/influxdb" "github.com/calvinmclean/automated-garden/garden-app/pkg/mqtt" "github.com/calvinmclean/automated-garden/garden-app/pkg/storage" @@ -24,6 +26,15 @@ var ( }, []string{"type", "id"}) ) +func init() { + sync.OnceFunc(func() { + prometheus.MustRegister( + scheduleJobsGauge, + schedulerErrors, + ) + })() +} + // Worker contains the necessary clients to schedule and execute actions type Worker struct { storageClient *storage.Client @@ -40,11 +51,13 @@ func NewWorker( mqttClient mqtt.Client, logger *slog.Logger, ) *Worker { + scheduler := gocron.NewScheduler(time.UTC) + scheduler.CustomTime(clock.DefaultClock) return &Worker{ storageClient: storageClient, influxdbClient: influxdbClient, mqttClient: mqttClient, - scheduler: gocron.NewScheduler(time.UTC), + scheduler: scheduler, logger: logger.With("source", "worker"), } } @@ -52,10 +65,6 @@ func NewWorker( // StartAsync starts the Worker's background jobs func (w *Worker) StartAsync() { w.scheduler.StartAsync() - prometheus.MustRegister( - scheduleJobsGauge, - schedulerErrors, - ) } // Stop stops the Worker's background jobs