From cd17d3ae7f2a86c5a239e896fca0d7a48cdf2ff2 Mon Sep 17 00:00:00 2001 From: Alex Savin Date: Tue, 9 Sep 2025 21:40:33 -0400 Subject: [PATCH] Add KeepAlive duration to IaR struct and update related functionality; remove obsolete test file --- client/client.go | 9 +- client/client_api_test.go | 179 -------------------------------------- client/client_test.go | 179 +++++++++++++++++++++++++++++++++++++- client/response.go | 2 +- main.go | 6 +- 5 files changed, 188 insertions(+), 187 deletions(-) delete mode 100644 client/client_api_test.go diff --git a/client/client.go b/client/client.go index dda7966..26271a6 100644 --- a/client/client.go +++ b/client/client.go @@ -41,6 +41,7 @@ type IaR struct { PagerGroupID string PagerGroupName string Type string + KeepAlive time.Duration } // New function creates a New MySubaru API client @@ -52,6 +53,7 @@ func New(credentials *config.Credentials, logger *slog.Logger) (*Client, error) PagerGroupID: "", PagerGroupName: "", Type: "", + KeepAlive: 15 * time.Minute, }, isAlive: false, logger: logger, @@ -86,6 +88,7 @@ func (c *Client) KeepAlive() error { c.IAR.PagerGroupID = pg.ID c.IAR.PagerGroupName = pg.Name c.IAR.Type = pg.Type + c.IAR.KeepAlive = time.Duration(resp.Data.GetTTDSetting.KeyValue) * time.Second } return nil } @@ -118,11 +121,11 @@ func (c *Client) Alert(audioBase64 string) error { } // ServeKeepAlive starts a goroutine that sends keep-alive requests at the specified interval. -func (c *Client) ServeKeepAlive(ctx context.Context, interval time.Duration) { - ticker := time.NewTicker(interval) +func (c *Client) ServeKeepAlive(ctx context.Context) { + ticker := time.NewTicker(c.IAR.KeepAlive) defer ticker.Stop() - slog.Info("Starting keep-alive sender", "interval", interval) + slog.Info("Starting keep-alive sender", slog.Duration("interval", c.IAR.KeepAlive)) for { select { diff --git a/client/client_api_test.go b/client/client_api_test.go deleted file mode 100644 index 711ee67..0000000 --- a/client/client_api_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package client_test - -import ( - "net/http" - "net/http/httptest" - "testing" - - "git.savin.nyc/alex/go-iar-notificator/client" -) - -const keepAliveResponse = `{ - "data": { - "getMutualPagerGroupsList": [ - { - "_id": "672783715f6ed07deb857afb", - "subscriberId": 536873008, - "name": "NPFD - 672783715f6ed07deb857afb", - "type": "ab tone", - "tone_tolerance": 0.02, - "gaplength": 0, - "ignore_after": 60, - "record_delay": 2, - "record_seconds": 15, - "release_time": 3, - "playback_during_record": 0, - "post_email_command": "", - "alert_command": "", - "atone": 1656.4, - "btone": 656.1, - "atonelength": 0.9, - "btonelength": 3, - "longtone": null, - "longtonelength": null, - "createDate": "2024-11-03T14:06:41.765Z" - } - ], - "getTTDSetting": { - "keySetting": "pollingSeconds", - "keyValue": "900" - } - } -}` - -const preAlertResponse = `{ - "data": { - "addAlert": { - "_id": "68bb90e0b91373858dd8f214", - "textAlert": "NPFD Received at 21:39:44 09/05/2025", - "pagerGroup": "NPFD", - "subscriberId": 536873008 - } - } -}` - -const alertResponse = `{ - "data": { - "addAlert": { - "_id": "68bb90e0b91373858dd8f214", - "textAlert": "NPFD Received at 21:40:05 09/05/2025", - "pagerGroup": "NPFD", - "audioUrl": "https://storage.iamresponding.com/v3/xzPAzU4ZEXlRtMLtj2NlquvalB26xQfS.mp3", - "subscriberId": 536873008 - } - } -}` - -// patchClientHTTP replaces the HTTPClient's transport with a test server -func patchClientHTTP(c *client.Client, handler http.HandlerFunc) { - ts := httptest.NewServer(handler) - c.HTTPClient.SetBaseURL(ts.URL) -} - -func TestClient_KeepAlive_Success(t *testing.T) { - logger := mockLogger() - creds := mockCredentials() - c, err := client.New(creds, logger) - if err != nil { - t.Fatalf("New() error: %v", err) - } - - patchClientHTTP(c, func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write([]byte(keepAliveResponse)) - }) - - err = c.KeepAlive() - if err != nil { - t.Errorf("KeepAlive() error = %v, want nil", err) - } - if !c.IsAlive() { - t.Error("expected client to be alive after successful KeepAlive") - } - // Check if IAR is set - if c.IAR.PagerGroupID != "672783715f6ed07deb857afb" { - t.Errorf("expected PagerGroupID to be set, got %s", c.IAR.PagerGroupID) - } -} - -func TestClient_KeepAlive_Failure(t *testing.T) { - logger := mockLogger() - creds := mockCredentials() - c, err := client.New(creds, logger) - if err != nil { - t.Fatalf("New() error: %v", err) - } - - patchClientHTTP(c, func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(`{"error": "fail"}`)) - }) - - err = c.KeepAlive() - if err == nil { - t.Error("expected error from KeepAlive, got nil") - } - if c.IsAlive() { - t.Error("expected client to not be alive after failed KeepAlive") - } -} - -func TestClient_PreAlert_Success(t *testing.T) { - logger := mockLogger() - creds := mockCredentials() - c, err := client.New(creds, logger) - if err != nil { - t.Fatalf("New() error: %v", err) - } - // Set IAR - c.IAR.PagerGroupID = "test_group" - - patchClientHTTP(c, func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write([]byte(preAlertResponse)) - }) - - alertID, err := c.PreAlert() - if err != nil { - t.Errorf("PreAlert() error = %v, want nil", err) - } - expectedID := "68bb90e0b91373858dd8f214" - if alertID != expectedID { - t.Errorf("expected alertID %s, got %s", expectedID, alertID) - } -} - -func TestClient_Alert_Success(t *testing.T) { - logger := mockLogger() - creds := mockCredentials() - c, err := client.New(creds, logger) - if err != nil { - t.Fatalf("New() error: %v", err) - } - // Set IAR - c.IAR.PagerGroupID = "test_group" - - callCount := 0 - patchClientHTTP(c, func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if callCount == 0 { - // First call is preAlert - w.Write([]byte(preAlertResponse)) - } else { - // Second call is Alert - w.Write([]byte(alertResponse)) - } - callCount++ - }) - - err = c.Alert("base64_audio") - if err != nil { - t.Errorf("Alert() error = %v, want nil", err) - } - if callCount != 2 { - t.Errorf("expected 2 HTTP calls, got %d", callCount) - } -} diff --git a/client/client_test.go b/client/client_test.go index aef4e67..77efda8 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -4,6 +4,8 @@ import ( "context" "io" "log/slog" + "net/http" + "net/http/httptest" "testing" "time" @@ -11,6 +13,62 @@ import ( "git.savin.nyc/alex/go-iar-notificator/config" ) +const keepAliveResponse = `{ + "data": { + "getMutualPagerGroupsList": [ + { + "_id": "672783715f6ed07deb857afb", + "subscriberId": 536873008, + "name": "NPFD - 672783715f6ed07deb857afb", + "type": "ab tone", + "tone_tolerance": 0.02, + "gaplength": 0, + "ignore_after": 60, + "record_delay": 2, + "record_seconds": 15, + "release_time": 3, + "playback_during_record": 0, + "post_email_command": "", + "alert_command": "", + "atone": 1656.4, + "btone": 656.1, + "atonelength": 0.9, + "btonelength": 3, + "longtone": null, + "longtonelength": null, + "createDate": "2024-11-03T14:06:41.765Z" + } + ], + "getTTDSetting": { + "keySetting": "pollingSeconds", + "keyValue": "900" + } + } +}` + +const preAlertResponse = `{ + "data": { + "addAlert": { + "_id": "68bb90e0b91373858dd8f214", + "textAlert": "NPFD Received at 21:39:44 09/05/2025", + "pagerGroup": "NPFD", + "subscriberId": 536873008 + } + } +}` + +const alertResponse = `{ + "data": { + "addAlert": { + "_id": "68bb90e0b91373858dd8f214", + "textAlert": "NPFD Received at 21:40:05 09/05/2025", + "pagerGroup": "NPFD", + "audioUrl": "https://storage.iamresponding.com/v3/xzPAzU4ZEXlRtMLtj2NlquvalB26xQfS.mp3", + "subscriberId": 536873008 + } + } +}` + // mockLogger returns a no-op slog.Logger for testing func mockLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) @@ -50,9 +108,128 @@ func TestServeKeepAliveCancel(t *testing.T) { creds := mockCredentials() c, _ := client.New(creds, logger) ctx, cancel := context.WithCancel(context.Background()) - go c.ServeKeepAlive(ctx, 10*time.Millisecond) + go c.ServeKeepAlive(ctx) // Let it run briefly time.Sleep(20 * time.Millisecond) cancel() // No panic or deadlock expected } + +// patchClientHTTP replaces the HTTPClient's transport with a test server +func patchClientHTTP(c *client.Client, handler http.HandlerFunc) { + ts := httptest.NewServer(handler) + c.HTTPClient.SetBaseURL(ts.URL) +} + +func TestClient_KeepAlive_Success(t *testing.T) { + logger := mockLogger() + creds := mockCredentials() + c, err := client.New(creds, logger) + if err != nil { + t.Fatalf("New() error: %v", err) + } + + patchClientHTTP(c, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(keepAliveResponse)) + }) + + err = c.KeepAlive() + if err != nil { + t.Errorf("KeepAlive() error = %v, want nil", err) + } + if !c.IsAlive() { + t.Error("expected client to be alive after successful KeepAlive") + } + // Check if IAR is set + if c.IAR.PagerGroupID != "672783715f6ed07deb857afb" { + t.Errorf("expected PagerGroupID to be set, got %s", c.IAR.PagerGroupID) + } + // Check if KeepAlive duration is set from API response (900 seconds = 15 minutes) + expectedDuration := 900 * time.Second + if c.IAR.KeepAlive != expectedDuration { + t.Errorf("expected KeepAlive duration to be %v, got %v", expectedDuration, c.IAR.KeepAlive) + } +} + +func TestClient_KeepAlive_Failure(t *testing.T) { + logger := mockLogger() + creds := mockCredentials() + c, err := client.New(creds, logger) + if err != nil { + t.Fatalf("New() error: %v", err) + } + + patchClientHTTP(c, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"error": "fail"}`)) + }) + + err = c.KeepAlive() + if err == nil { + t.Error("expected error from KeepAlive, got nil") + } + if c.IsAlive() { + t.Error("expected client to not be alive after failed KeepAlive") + } +} + +func TestClient_PreAlert_Success(t *testing.T) { + logger := mockLogger() + creds := mockCredentials() + c, err := client.New(creds, logger) + if err != nil { + t.Fatalf("New() error: %v", err) + } + // Set IAR + c.IAR.PagerGroupID = "test_group" + + patchClientHTTP(c, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(preAlertResponse)) + }) + + alertID, err := c.PreAlert() + if err != nil { + t.Errorf("PreAlert() error = %v, want nil", err) + } + expectedID := "68bb90e0b91373858dd8f214" + if alertID != expectedID { + t.Errorf("expected alertID %s, got %s", expectedID, alertID) + } +} + +func TestClient_Alert_Success(t *testing.T) { + logger := mockLogger() + creds := mockCredentials() + c, err := client.New(creds, logger) + if err != nil { + t.Fatalf("New() error: %v", err) + } + // Set IAR + c.IAR.PagerGroupID = "test_group" + + callCount := 0 + patchClientHTTP(c, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if callCount == 0 { + // First call is preAlert + w.Write([]byte(preAlertResponse)) + } else { + // Second call is Alert + w.Write([]byte(alertResponse)) + } + callCount++ + }) + + err = c.Alert("base64_audio") + if err != nil { + t.Errorf("Alert() error = %v, want nil", err) + } + if callCount != 2 { + t.Errorf("expected 2 HTTP calls, got %d", callCount) + } +} diff --git a/client/response.go b/client/response.go index 4479f2d..3d20288 100644 --- a/client/response.go +++ b/client/response.go @@ -30,7 +30,7 @@ type PagerGroup struct { type TTDSetting struct { KeySetting string `json:"keySetting"` - KeyValue string `json:"keyValue"` + KeyValue int `json:"keyValue,string"` } type AddAlert struct { diff --git a/main.go b/main.go index 815e0a2..b67332b 100644 --- a/main.go +++ b/main.go @@ -70,14 +70,14 @@ func main() { // Start directory watcher in a goroutine go watcher.WatchDirectory(ctx, config.Directory, eb, logger) - // Start keep-alive sender in a goroutine - go iar.ServeKeepAlive(ctx, 15*time.Minute) - err = iar.KeepAlive() if err != nil { slog.Error("Initial keep-alive failed", "error", err) } + // Start keep-alive sender in a goroutine + go iar.ServeKeepAlive(ctx) + // Handle OS signals for shutdown sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)