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