From 6d03a39db9464e86ca44ae77814364632a983ec6 Mon Sep 17 00:00:00 2001 From: marcon-effe Date: Sat, 4 Apr 2026 08:58:15 +0000 Subject: [PATCH 1/2] fix: refactor tests for improved readability and consistency --- cmd/anomalies_test.go | 4 +- cmd/commands_test.go | 97 ++++++---------- cmd/request_mapping_test.go | 112 +++++++++---------- cmd/root.go | 1 - cmd/shell_test.go | 8 +- cmd/spinner_test.go | 6 +- internal/client/client_test.go | 197 ++++++++++++++++++--------------- 7 files changed, 202 insertions(+), 223 deletions(-) diff --git a/cmd/anomalies_test.go b/cmd/anomalies_test.go index fd8de60..b8b2e49 100644 --- a/cmd/anomalies_test.go +++ b/cmd/anomalies_test.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/cobra" ) -func TestMustMarkRequired_Success(t *testing.T) { +func TestMustMarkRequiredSuccess(t *testing.T) { cmd := &cobra.Command{Use: "test"} cmd.Flags().String("duration", "", "") @@ -21,7 +21,7 @@ func TestMustMarkRequired_Success(t *testing.T) { } } -func TestMustMarkRequired_ErrorTriggersExit(t *testing.T) { +func TestMustMarkRequiredErrorTriggersExit(t *testing.T) { cmd := &cobra.Command{Use: "test"} called := false code := 0 diff --git a/cmd/commands_test.go b/cmd/commands_test.go index 5ae1cc7..d443827 100644 --- a/cmd/commands_test.go +++ b/cmd/commands_test.go @@ -124,32 +124,32 @@ func TestCommandTree(t *testing.T) { // ── Required-flag validation ────────────────────────────────────────────────── -func TestGatewaysCreate_MissingRequiredFlags(t *testing.T) { +func TestGatewaysCreateMissingRequiredFlags(t *testing.T) { // factory-id, factory-key, serial are all required if err := runCmd("gateways", "create"); err == nil { t.Error("expected error when required flags are missing") } } -func TestGatewaysBulk_MissingCount(t *testing.T) { +func TestGatewaysBulkMissingCount(t *testing.T) { if err := runCmd("gateways", "bulk", testFlagFactoryID, "f", testFlagFactoryKey, "k"); err == nil { t.Error("expected error when --count is missing") } } -func TestSensorsAdd_MissingFlags(t *testing.T) { +func TestSensorsAddMissingFlags(t *testing.T) { if err := runCmd("sensors", "add", "5"); err == nil { t.Error("expected error when sensor flags are missing") } } -func TestAnomaliesDisconnect_MissingDuration(t *testing.T) { +func TestAnomaliesDisconnectMissingDuration(t *testing.T) { if err := runCmd("anomalies", "disconnect", testGatewayUUID); err == nil { t.Error("expected error when --duration is missing") } } -func TestAnomaliesNetworkDegradation_MissingDuration(t *testing.T) { +func TestAnomaliesNetworkDegradationMissingDuration(t *testing.T) { if err := runCmd("anomalies", cmdNetDegradation, testGatewayUUID); err == nil { t.Error("expected error when --duration is missing") } @@ -157,19 +157,19 @@ func TestAnomaliesNetworkDegradation_MissingDuration(t *testing.T) { // ── Argument-count validation ───────────────────────────────────────────────── -func TestGatewaysGet_NoArgs(t *testing.T) { +func TestGatewaysGetNoArgs(t *testing.T) { if err := runCmd("gateways", "get"); err == nil { t.Error("expected error when uuid arg is missing") } } -func TestGatewaysDelete_NoArgs(t *testing.T) { +func TestGatewaysDeleteNoArgs(t *testing.T) { if err := runCmd("gateways", "delete"); err == nil { t.Error("expected error when uuid arg is missing") } } -func TestSensorsAdd_NonNumericID(t *testing.T) { +func TestSensorsAddNonNumericID(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { // Should never reach the server — arg parsing fails first. t.Error("server should not have been called") @@ -180,13 +180,13 @@ func TestSensorsAdd_NonNumericID(t *testing.T) { } } -func TestSensorsDelete_NonNumericID(t *testing.T) { +func TestSensorsDeleteNonNumericID(t *testing.T) { if err := runCmd("sensors", "delete", "abc"); err == nil { t.Error("expected error for non-numeric sensor ID") } } -func TestAnomaliesOutlier_NonNumericID(t *testing.T) { +func TestAnomaliesOutlierNonNumericID(t *testing.T) { if err := runCmd("anomalies", "outlier", "not-a-number"); err == nil { t.Error("expected error for non-numeric sensor ID") } @@ -194,7 +194,7 @@ func TestAnomaliesOutlier_NonNumericID(t *testing.T) { // ── Integration: full execution against mock server ─────────────────────────── -func TestGatewaysList_Integration(t *testing.T) { +func TestGatewaysListIntegration(t *testing.T) { gateways := []map[string]any{ {"id": 1, "managementGatewayId": testGatewayUUID, "status": "online", "model": "X", "serialNumber": "SN1", "sendFrequencyMs": 1000, "tenantId": "t1"}, } @@ -210,7 +210,7 @@ func TestGatewaysList_Integration(t *testing.T) { } } -func TestGatewaysList_ServerError(t *testing.T) { +func TestGatewaysListServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, "boom", http.StatusInternalServerError) }) @@ -219,7 +219,7 @@ func TestGatewaysList_ServerError(t *testing.T) { } } -func TestGatewaysStart_Integration(t *testing.T) { +func TestGatewaysStartIntegration(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/sim/gateways/uuid-1/start" { t.Errorf(fmtUnexpectedPath, r.URL.Path) @@ -231,7 +231,7 @@ func TestGatewaysStart_Integration(t *testing.T) { } } -func TestGatewaysStop_Integration(t *testing.T) { +func TestGatewaysStopIntegration(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/sim/gateways/uuid-1/stop" { t.Errorf(fmtUnexpectedPath, r.URL.Path) @@ -243,7 +243,7 @@ func TestGatewaysStop_Integration(t *testing.T) { } } -func TestGatewaysDelete_Integration(t *testing.T) { +func TestGatewaysDeleteIntegration(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete || r.URL.Path != "/sim/gateways/uuid-1" { t.Errorf(fmtUnexpectedRequest, r.Method, r.URL.Path) @@ -255,7 +255,7 @@ func TestGatewaysDelete_Integration(t *testing.T) { } } -func TestSensorsList_Integration(t *testing.T) { +func TestSensorsListIntegration(t *testing.T) { sensors := []map[string]any{ {"id": 1, "gatewayId": 5, "sensorId": "s-uuid-1", "type": "temperature", "minRange": 0, "maxRange": 100, "algorithm": "sine_wave"}, } @@ -270,7 +270,7 @@ func TestSensorsList_Integration(t *testing.T) { } } -func TestSensorsDelete_Integration(t *testing.T) { +func TestSensorsDeleteIntegration(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete || r.URL.Path != "/sim/sensors/99" { t.Errorf(fmtUnexpectedRequest, r.Method, r.URL.Path) @@ -282,7 +282,7 @@ func TestSensorsDelete_Integration(t *testing.T) { } } -func TestAnomaliesDisconnect_Integration(t *testing.T) { +func TestAnomaliesDisconnectIntegration(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/sim/gateways/uuid-1/anomaly/disconnect" { t.Errorf(fmtUnexpectedPath, r.URL.Path) @@ -299,7 +299,7 @@ func TestAnomaliesDisconnect_Integration(t *testing.T) { } } -func TestAnomaliesNetworkDegradation_Integration(t *testing.T) { +func TestAnomaliesNetworkDegradationIntegration(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/sim/gateways/uuid-1/anomaly/network-degradation" { t.Errorf(fmtUnexpectedPath, r.URL.Path) @@ -319,7 +319,7 @@ func TestAnomaliesNetworkDegradation_Integration(t *testing.T) { } } -func TestAnomaliesOutlier_Integration(t *testing.T) { +func TestAnomaliesOutlierIntegration(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/sim/sensors/42/anomaly/outlier" { t.Errorf(fmtUnexpectedPath, r.URL.Path) @@ -338,7 +338,7 @@ func TestAnomaliesOutlier_Integration(t *testing.T) { // ── Error paths (spinner.Fail + return err) ─────────────────────────────────── -func TestGatewaysList_EmptyResult(t *testing.T) { +func TestGatewaysListEmptyResult(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, []any{}) }) @@ -347,7 +347,7 @@ func TestGatewaysList_EmptyResult(t *testing.T) { } } -func TestGatewaysGet_ServerError(t *testing.T) { +func TestGatewaysGetServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) }) @@ -356,7 +356,7 @@ func TestGatewaysGet_ServerError(t *testing.T) { } } -func TestGatewaysCreate_ServerError(t *testing.T) { +func TestGatewaysCreateServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, "bad request", http.StatusBadRequest) }) @@ -366,7 +366,7 @@ func TestGatewaysCreate_ServerError(t *testing.T) { } } -func TestGatewaysBulk_ServerError(t *testing.T) { +func TestGatewaysBulkServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, "server error", http.StatusInternalServerError) }) @@ -376,7 +376,7 @@ func TestGatewaysBulk_ServerError(t *testing.T) { } } -func TestGatewaysBulk_PartialErrors(t *testing.T) { +func TestGatewaysBulkPartialErrors(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusMultiStatus, map[string]any{ "gateways": []any{map[string]any{"id": 1}}, @@ -389,7 +389,7 @@ func TestGatewaysBulk_PartialErrors(t *testing.T) { } } -func TestGatewaysStart_ServerError(t *testing.T) { +func TestGatewaysStartServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, "conflict", http.StatusConflict) }) @@ -398,7 +398,7 @@ func TestGatewaysStart_ServerError(t *testing.T) { } } -func TestGatewaysStop_ServerError(t *testing.T) { +func TestGatewaysStopServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) }) @@ -407,7 +407,7 @@ func TestGatewaysStop_ServerError(t *testing.T) { } } -func TestGatewaysDelete_ServerError(t *testing.T) { +func TestGatewaysDeleteServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) }) @@ -416,7 +416,7 @@ func TestGatewaysDelete_ServerError(t *testing.T) { } } -func TestSensorsAdd_ServerError(t *testing.T) { +func TestSensorsAddServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) }) @@ -426,7 +426,7 @@ func TestSensorsAdd_ServerError(t *testing.T) { } } -func TestSensorsList_EmptyResult(t *testing.T) { +func TestSensorsListEmptyResult(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, []any{}) }) @@ -435,7 +435,7 @@ func TestSensorsList_EmptyResult(t *testing.T) { } } -func TestSensorsList_ServerError(t *testing.T) { +func TestSensorsListServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) }) @@ -444,7 +444,7 @@ func TestSensorsList_ServerError(t *testing.T) { } } -func TestSensorsDelete_ServerError(t *testing.T) { +func TestSensorsDeleteServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) }) @@ -453,7 +453,7 @@ func TestSensorsDelete_ServerError(t *testing.T) { } } -func TestAnomaliesDisconnect_ServerError(t *testing.T) { +func TestAnomaliesDisconnectServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) }) @@ -462,7 +462,7 @@ func TestAnomaliesDisconnect_ServerError(t *testing.T) { } } -func TestAnomaliesNetworkDegradation_ServerError(t *testing.T) { +func TestAnomaliesNetworkDegradationServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) }) @@ -471,7 +471,7 @@ func TestAnomaliesNetworkDegradation_ServerError(t *testing.T) { } } -func TestAnomaliesOutlier_ServerError(t *testing.T) { +func TestAnomaliesOutlierServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) }) @@ -631,31 +631,6 @@ func TestShellExitImmediately(t *testing.T) { } } -func TestPrintPromptRawOutput(t *testing.T) { - originalRawOutput := pterm.RawOutput - pterm.RawOutput = true - t.Cleanup(func() { - pterm.RawOutput = originalRawOutput - }) - - out := captureStdout(t, printPrompt) - if out != "sim-cli> " { - t.Fatalf("prompt = %q, want %q", out, "sim-cli> ") - } -} - -func TestPrintWelcomeBannerRawModeDoesNotCrash(t *testing.T) { - originalRawOutput := pterm.RawOutput - pterm.RawOutput = true - t.Cleanup(func() { - pterm.RawOutput = originalRawOutput - }) - - // In non-TTY/raw mode the renderer may bypass direct stdout writes, - // but the banner must still render without panicking. - printWelcomeBanner() -} - func TestStatusStyleVariants(t *testing.T) { originalRawOutput := pterm.RawOutput pterm.RawOutput = false @@ -674,7 +649,7 @@ func TestStatusStyleVariants(t *testing.T) { } } -func TestPrintSensorTable_EmptySlice_NoOutput(t *testing.T) { +func TestPrintSensorTableEmptySliceNoOutput(t *testing.T) { out := captureStdout(t, func() { printSensorTable([]client.Sensor{}) }) diff --git a/cmd/request_mapping_test.go b/cmd/request_mapping_test.go index 6d2b49e..e36039e 100644 --- a/cmd/request_mapping_test.go +++ b/cmd/request_mapping_test.go @@ -11,6 +11,11 @@ import ( "testing" ) +const ( + fmtUnexpectedError = "unexpected error: %v" + flagSerialArg = "--serial" +) + // readBody parses the request body into a generic map for field inspection. func readBody(t *testing.T, r *http.Request) map[string]any { t.Helper() @@ -47,28 +52,28 @@ func checkAbsent(t *testing.T, body map[string]any, key string) { } } -// ── gateways create ─────────────────────────────────────────────────────────── +// -- gateways create ---------------------------------------------------------- -func TestGatewaysCreate_FlagToJSONMapping(t *testing.T) { +func TestGatewaysCreateFlagToJSONMapping(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/sim/gateways" { - t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + t.Errorf(fmtUnexpectedRequest, r.Method, r.URL.Path) } body := readBody(t, r) - checkKey(t, body, "factoryId", "fac-42") // --factory-id - checkKey(t, body, "factoryKey", "secret-key") // --factory-key - checkKey(t, body, "serialNumber", "SN-XYZ") // --serial - checkKey(t, body, "model", "GW-PRO") // --model - checkKey(t, body, "firmwareVersion", "3.0.1") // --firmware - checkKey(t, body, "sendFrequencyMs", float64(250)) // --freq + checkKey(t, body, "factoryId", "fac-42") + checkKey(t, body, "factoryKey", "secret-key") + checkKey(t, body, "serialNumber", "SN-XYZ") + checkKey(t, body, "model", "GW-PRO") + checkKey(t, body, "firmwareVersion", "3.0.1") + checkKey(t, body, "sendFrequencyMs", float64(250)) writeJSON(w, http.StatusCreated, map[string]any{"id": 1}) }) err := runCmd("gateways", "create", - "--factory-id", "fac-42", - "--factory-key", "secret-key", - "--serial", "SN-XYZ", + testFlagFactoryID, "fac-42", + testFlagFactoryKey, "secret-key", + flagSerialArg, "SN-XYZ", "--model", "GW-PRO", "--firmware", "3.0.1", "--freq", "250", @@ -78,50 +83,47 @@ func TestGatewaysCreate_FlagToJSONMapping(t *testing.T) { } } -func TestGatewaysCreate_DefaultFreq_Is1000(t *testing.T) { +func TestGatewaysCreateDefaultFreqIs1000(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { body := readBody(t, r) - // --freq defaults to 1000 ms when not provided checkKey(t, body, "sendFrequencyMs", float64(1000)) writeJSON(w, http.StatusCreated, map[string]any{"id": 1}) }) err := runCmd("gateways", "create", - "--factory-id", "f", - "--factory-key", "k", - "--serial", "SN", + testFlagFactoryID, "f", + testFlagFactoryKey, "k", + flagSerialArg, "SN", ) if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(fmtUnexpectedError, err) } } -func TestGatewaysCreate_OptionalFields_OmittedWhenNotProvided(t *testing.T) { +func TestGatewaysCreateOptionalFieldsOmittedWhenNotProvided(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { body := readBody(t, r) - // model and firmware are optional; when not passed they must be absent - // (omitempty), not sent as empty strings checkAbsent(t, body, "model") checkAbsent(t, body, "firmwareVersion") writeJSON(w, http.StatusCreated, map[string]any{"id": 1}) }) err := runCmd("gateways", "create", - "--factory-id", "f", - "--factory-key", "k", - "--serial", "SN", + testFlagFactoryID, "f", + testFlagFactoryKey, "k", + flagSerialArg, "SN", ) if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(fmtUnexpectedError, err) } } -// ── gateways bulk ───────────────────────────────────────────────────────────── +// -- gateways bulk ------------------------------------------------------------ -func TestGatewaysBulk_FlagToJSONMapping(t *testing.T) { +func TestGatewaysBulkFlagToJSONMapping(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/sim/gateways/bulk" { - t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + t.Errorf(fmtUnexpectedRequest, r.Method, r.URL.Path) } body := readBody(t, r) checkKey(t, body, "count", float64(5)) @@ -139,8 +141,8 @@ func TestGatewaysBulk_FlagToJSONMapping(t *testing.T) { err := runCmd("gateways", "bulk", "--count", "5", - "--factory-id", "fac-bulk", - "--factory-key", "key-bulk", + testFlagFactoryID, "fac-bulk", + testFlagFactoryKey, "key-bulk", "--model", "GW-MINI", "--firmware", "1.2.3", "--freq", "500", @@ -150,12 +152,12 @@ func TestGatewaysBulk_FlagToJSONMapping(t *testing.T) { } } -// ── gateways get ───────────────────────────────────────────────────────────── +// -- gateways get ------------------------------------------------------------- -func TestGatewaysGet_Integration(t *testing.T) { +func TestGatewaysGetIntegration(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet || r.URL.Path != "/sim/gateways/uuid-get-1" { - t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + t.Errorf(fmtUnexpectedRequest, r.Method, r.URL.Path) } writeJSON(w, http.StatusOK, map[string]any{ "id": 42, @@ -176,7 +178,7 @@ func TestGatewaysGet_Integration(t *testing.T) { } } -func TestGatewaysGet_NotFound(t *testing.T) { +func TestGatewaysGetNotFound(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, "not found", http.StatusNotFound) }) @@ -185,27 +187,20 @@ func TestGatewaysGet_NotFound(t *testing.T) { } } -// ── sensors add ─────────────────────────────────────────────────────────────── -// -// CRITICAL: --min/--max CLI flags must map to "minRange"/"maxRange" in JSON. -// If this mapping breaks the backend will reject the request or use wrong defaults. +// -- sensors add -------------------------------------------------------------- -func TestSensorsAdd_FlagToJSONMapping(t *testing.T) { +func TestSensorsAddFlagToJSONMapping(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/sim/gateways/5/sensors" { - t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + t.Errorf(fmtUnexpectedRequest, r.Method, r.URL.Path) } body := readBody(t, r) - // --type → "type" checkKey(t, body, "type", "humidity") - // --min → "minRange" (NOT "min") checkKey(t, body, "minRange", float64(10)) checkAbsent(t, body, "min") - // --max → "maxRange" (NOT "max") checkKey(t, body, "maxRange", float64(90)) checkAbsent(t, body, "max") - // --algorithm → "algorithm" checkKey(t, body, "algorithm", "uniform_random") writeJSON(w, http.StatusCreated, map[string]any{ @@ -225,7 +220,7 @@ func TestSensorsAdd_FlagToJSONMapping(t *testing.T) { } } -func TestSensorsAdd_NegativeMinRange(t *testing.T) { +func TestSensorsAddNegativeMinRange(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { body := readBody(t, r) checkKey(t, body, "minRange", float64(-40)) @@ -244,29 +239,28 @@ func TestSensorsAdd_NegativeMinRange(t *testing.T) { } } -// ── anomalies disconnect ────────────────────────────────────────────────────── +// -- anomalies disconnect ----------------------------------------------------- -func TestAnomaliesDisconnect_DurationFieldName(t *testing.T) { +func TestAnomaliesDisconnectDurationFieldName(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/sim/gateways/uuid-dc/anomaly/disconnect" { t.Errorf("path = %s", r.URL.Path) } body := readBody(t, r) - // Backend expects "duration_seconds", not "duration" checkKey(t, body, "duration_seconds", float64(10)) checkAbsent(t, body, "duration") checkAbsent(t, body, "durationSeconds") w.WriteHeader(http.StatusNoContent) }) - if err := runCmd("anomalies", "disconnect", "uuid-dc", "--duration", "10"); err != nil { + if err := runCmd("anomalies", "disconnect", "uuid-dc", testFlagDuration, "10"); err != nil { t.Fatalf("anomalies disconnect failed: %v", err) } } -// ── anomalies network-degradation ───────────────────────────────────────────── +// -- anomalies network-degradation ------------------------------------------- -func TestAnomaliesNetworkDegradation_FieldNames(t *testing.T) { +func TestAnomaliesNetworkDegradationFieldNames(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { body := readBody(t, r) checkKey(t, body, "duration_seconds", float64(20)) @@ -276,7 +270,7 @@ func TestAnomaliesNetworkDegradation_FieldNames(t *testing.T) { }) err := runCmd("anomalies", "network-degradation", "uuid-nd", - "--duration", "20", + testFlagDuration, "20", "--packet-loss", "0.5", ) if err != nil { @@ -284,31 +278,29 @@ func TestAnomaliesNetworkDegradation_FieldNames(t *testing.T) { } } -func TestAnomaliesNetworkDegradation_PacketLossOmitted_WhenNotProvided(t *testing.T) { +func TestAnomaliesNetworkDegradationPacketLossOmittedWhenNotProvided(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { body := readBody(t, r) - // --packet-loss not passed → field absent → backend uses its default (0.3) checkAbsent(t, body, "packet_loss_pct") w.WriteHeader(http.StatusNoContent) }) - err := runCmd("anomalies", "network-degradation", "uuid-nd", "--duration", "5") + err := runCmd("anomalies", "network-degradation", "uuid-nd", testFlagDuration, "5") if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(fmtUnexpectedError, err) } } -// ── anomalies outlier ───────────────────────────────────────────────────────── +// -- anomalies outlier -------------------------------------------------------- -func TestAnomaliesOutlier_ValueOmitted_WhenNotProvided(t *testing.T) { +func TestAnomaliesOutlierValueOmittedWhenNotProvided(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { body := readBody(t, r) - // --value not passed → field must be absent (nil pointer + omitempty) checkAbsent(t, body, "value") w.WriteHeader(http.StatusNoContent) }) if err := runCmd("anomalies", "outlier", "10"); err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(fmtUnexpectedError, err) } } diff --git a/cmd/root.go b/cmd/root.go index b16866a..1974149 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,7 +23,6 @@ func Execute() error { func init() { // Disable PTerm styling when stdout is not a TTY (e.g., CI pipelines, - // docker compose run without -it) to avoid raw ANSI escape sequences. if !term.IsTerminal(int(os.Stdout.Fd())) { pterm.DisableStyling() pterm.DisableColor() diff --git a/cmd/shell_test.go b/cmd/shell_test.go index 1da6f60..646cad4 100644 --- a/cmd/shell_test.go +++ b/cmd/shell_test.go @@ -7,7 +7,7 @@ import ( "github.com/pterm/pterm" ) -func TestPrintPrompt_RawOutput(t *testing.T) { +func TestPrintPromptRawOutput(t *testing.T) { prevRaw := pterm.RawOutput pterm.RawOutput = true t.Cleanup(func() { @@ -20,7 +20,7 @@ func TestPrintPrompt_RawOutput(t *testing.T) { } } -func TestPrintWelcomeBanner_RawOutput(t *testing.T) { +func TestPrintWelcomeBannerRawOutput(t *testing.T) { prevRaw := pterm.RawOutput pterm.RawOutput = true t.Cleanup(func() { @@ -30,7 +30,7 @@ func TestPrintWelcomeBanner_RawOutput(t *testing.T) { _ = captureStdout(t, printWelcomeBanner) } -func TestPrintPrompt_NonRawOutput(t *testing.T) { +func TestPrintPromptNonRawOutput(t *testing.T) { prevRaw := pterm.RawOutput pterm.RawOutput = false t.Cleanup(func() { @@ -43,7 +43,7 @@ func TestPrintPrompt_NonRawOutput(t *testing.T) { } } -func TestPrintWelcomeBanner_NonRawOutput(t *testing.T) { +func TestPrintWelcomeBannerNonRawOutput(t *testing.T) { prevRaw := pterm.RawOutput pterm.RawOutput = false t.Cleanup(func() { diff --git a/cmd/spinner_test.go b/cmd/spinner_test.go index 45ae8cd..dd8d9b3 100644 --- a/cmd/spinner_test.go +++ b/cmd/spinner_test.go @@ -6,7 +6,7 @@ import ( "github.com/pterm/pterm" ) -func TestStartSpinner_RawOutputReturnsNoop(t *testing.T) { +func TestStartSpinnerRawOutputReturnsNoop(t *testing.T) { prevRaw := pterm.RawOutput pterm.RawOutput = true t.Cleanup(func() { @@ -24,7 +24,7 @@ func TestStartSpinner_RawOutputReturnsNoop(t *testing.T) { sp.Warning("warn") } -func TestPtermSpinner_NilInnerIsSafe(t *testing.T) { +func TestPtermSpinnerNilInnerIsSafe(t *testing.T) { sp := ptermSpinner{} sp.Success("ok") @@ -32,7 +32,7 @@ func TestPtermSpinner_NilInnerIsSafe(t *testing.T) { sp.Warning("warn") } -func TestNoopSpinner_DirectMethods(t *testing.T) { +func TestNoopSpinnerDirectMethods(t *testing.T) { var sp noopSpinner sp.Success("ok") diff --git a/internal/client/client_test.go b/internal/client/client_test.go index 20fa413..cdb363d 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -11,6 +11,19 @@ import ( "github.com/NoTIPswe/notip-simulator-cli/internal/client" ) +const ( + headerContentType = "Content-Type" + mediaTypeJSON = "application/json" + gwUUID1 = "uuid-1" + errFmtUnexpected = "unexpected error: %v" + errMsgNotFound = "not found" + errExpected404 = "expected error on 404, got nil" + errExpectedDecode = "expected decode error, got nil" + invalidJSONPayload = "not-json" + invalidJSONPayloadBroken = "{invalid-json" + pathGatewaysPrefix = "/sim/gateways/" +) + // ── helpers ─────────────────────────────────────────────────────────────────── func newTestServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *client.Client) { @@ -28,7 +41,7 @@ func decodeBody(t *testing.T, r *http.Request, dst any) { } func writeJSON(w http.ResponseWriter, status int, v any) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set(headerContentType, mediaTypeJSON) w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } @@ -49,8 +62,8 @@ func assertPath(t *testing.T, r *http.Request, want string) { // ── Gateway ─────────────────────────────────────────────────────────────────── -func TestCreateGateway_Success(t *testing.T) { - want := client.Gateway{ID: 1, ManagementGatewayID: "uuid-1", Status: "online"} +func TestCreateGatewaySuccess(t *testing.T) { + want := client.Gateway{ID: 1, ManagementGatewayID: gwUUID1, Status: "online"} _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodPost) assertPath(t, r, "/sim/gateways") @@ -69,14 +82,14 @@ func TestCreateGateway_Success(t *testing.T) { SerialNumber: "SN-001", }) if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errFmtUnexpected, err) } if got.ID != want.ID || got.ManagementGatewayID != want.ManagementGatewayID { t.Errorf("got %+v, want %+v", got, want) } } -func TestCreateGateway_ServerError(t *testing.T) { +func TestCreateGatewayServerError(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, "internal server error", http.StatusInternalServerError) }) @@ -86,7 +99,7 @@ func TestCreateGateway_ServerError(t *testing.T) { } } -func TestBulkCreateGateways_AllSuccess(t *testing.T) { +func TestBulkCreateGatewaysAllSuccess(t *testing.T) { want := client.BulkCreateResponse{ Gateways: []client.Gateway{{ID: 1}, {ID: 2}}, Errors: []string{"", ""}, @@ -105,14 +118,14 @@ func TestBulkCreateGateways_AllSuccess(t *testing.T) { got, err := c.BulkCreateGateways(client.BulkCreateGatewaysRequest{Count: 2, FactoryID: "f", FactoryKey: "k"}) if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errFmtUnexpected, err) } if len(got.Gateways) != 2 { t.Errorf("got %d gateways, want 2", len(got.Gateways)) } } -func TestBulkCreateGateways_PartialErrors_207(t *testing.T) { +func TestBulkCreateGatewaysPartialErrors207(t *testing.T) { want := client.BulkCreateResponse{ Gateways: []client.Gateway{{ID: 1}}, Errors: []string{"", "factory key mismatch"}, @@ -130,7 +143,7 @@ func TestBulkCreateGateways_PartialErrors_207(t *testing.T) { } } -func TestListGateways_Success(t *testing.T) { +func TestListGatewaysSuccess(t *testing.T) { want := []client.Gateway{{ID: 1, Status: "online"}, {ID: 2, Status: "offline"}} _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodGet) @@ -140,7 +153,7 @@ func TestListGateways_Success(t *testing.T) { got, err := c.ListGateways() if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errFmtUnexpected, err) } if len(got) != 2 { t.Fatalf("got %d gateways, want 2", len(got)) @@ -150,20 +163,20 @@ func TestListGateways_Success(t *testing.T) { } } -func TestListGateways_Empty(t *testing.T) { +func TestListGatewaysEmpty(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, []client.Gateway{}) }) got, err := c.ListGateways() if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errFmtUnexpected, err) } if len(got) != 0 { t.Errorf("expected empty slice, got %d items", len(got)) } } -func TestGetGateway_Success(t *testing.T) { +func TestGetGatewaySuccess(t *testing.T) { want := client.Gateway{ID: 42, ManagementGatewayID: "uuid-42", Status: "online"} _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodGet) @@ -173,77 +186,77 @@ func TestGetGateway_Success(t *testing.T) { got, err := c.GetGateway("uuid-42") if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errFmtUnexpected, err) } if got.ID != 42 { t.Errorf("got ID %d, want 42", got.ID) } } -func TestGetGateway_NotFound(t *testing.T) { +func TestGetGatewayNotFound(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "not found", http.StatusNotFound) + http.Error(w, errMsgNotFound, http.StatusNotFound) }) _, err := c.GetGateway("unknown-uuid") if err == nil { - t.Fatal("expected error on 404, got nil") + t.Fatal(errExpected404) } } -func TestStartGateway_Success(t *testing.T) { +func TestStartGatewaySuccess(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodPost) - assertPath(t, r, "/sim/gateways/uuid-1/start") + assertPath(t, r, pathGatewaysPrefix+gwUUID1+"/start") w.WriteHeader(http.StatusNoContent) }) - if err := c.StartGateway("uuid-1"); err != nil { - t.Fatalf("unexpected error: %v", err) + if err := c.StartGateway(gwUUID1); err != nil { + t.Fatalf(errFmtUnexpected, err) } } -func TestStartGateway_AlreadyRunning_409(t *testing.T) { +func TestStartGatewayAlreadyRunning409(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, "already running", http.StatusConflict) }) - if err := c.StartGateway("uuid-1"); err == nil { + if err := c.StartGateway(gwUUID1); err == nil { t.Fatal("expected error on 409, got nil") } } -func TestStopGateway_Success(t *testing.T) { +func TestStopGatewaySuccess(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodPost) - assertPath(t, r, "/sim/gateways/uuid-1/stop") + assertPath(t, r, pathGatewaysPrefix+gwUUID1+"/stop") w.WriteHeader(http.StatusNoContent) }) - if err := c.StopGateway("uuid-1"); err != nil { - t.Fatalf("unexpected error: %v", err) + if err := c.StopGateway(gwUUID1); err != nil { + t.Fatalf(errFmtUnexpected, err) } } -func TestDeleteGateway_Success(t *testing.T) { +func TestDeleteGatewaySuccess(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodDelete) - assertPath(t, r, "/sim/gateways/uuid-1") + assertPath(t, r, pathGatewaysPrefix+gwUUID1) w.WriteHeader(http.StatusNoContent) }) - if err := c.DeleteGateway("uuid-1"); err != nil { - t.Fatalf("unexpected error: %v", err) + if err := c.DeleteGateway(gwUUID1); err != nil { + t.Fatalf(errFmtUnexpected, err) } } -func TestDeleteGateway_NotFound(t *testing.T) { +func TestDeleteGatewayNotFound(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "not found", http.StatusNotFound) + http.Error(w, errMsgNotFound, http.StatusNotFound) }) if err := c.DeleteGateway("ghost"); err == nil { - t.Fatal("expected error on 404, got nil") + t.Fatal(errExpected404) } } // ── Sensor ──────────────────────────────────────────────────────────────────── -func TestAddSensor_Success(t *testing.T) { +func TestAddSensorSuccess(t *testing.T) { want := client.Sensor{ID: 10, GatewayID: 5, Type: "temperature", MinRange: 0, MaxRange: 100, Algorithm: "sine_wave"} _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodPost) @@ -264,24 +277,24 @@ func TestAddSensor_Success(t *testing.T) { Algorithm: "sine_wave", }) if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errFmtUnexpected, err) } if got.ID != 10 || got.Type != "temperature" { t.Errorf("got %+v, want %+v", got, want) } } -func TestAddSensor_GatewayNotFound(t *testing.T) { +func TestAddSensorGatewayNotFound(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "not found", http.StatusNotFound) + http.Error(w, errMsgNotFound, http.StatusNotFound) }) _, err := c.AddSensor(999, client.AddSensorRequest{Type: "temperature", Algorithm: "constant"}) if err == nil { - t.Fatal("expected error on 404, got nil") + t.Fatal(errExpected404) } } -func TestListSensors_Success(t *testing.T) { +func TestListSensorsSuccess(t *testing.T) { want := []client.Sensor{ {ID: 1, Type: "temperature"}, {ID: 2, Type: "humidity"}, @@ -294,39 +307,39 @@ func TestListSensors_Success(t *testing.T) { got, err := c.ListSensors(7) if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errFmtUnexpected, err) } if len(got) != 2 { t.Fatalf("got %d sensors, want 2", len(got)) } } -func TestDeleteSensor_Success(t *testing.T) { +func TestDeleteSensorSuccess(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodDelete) assertPath(t, r, "/sim/sensors/99") w.WriteHeader(http.StatusNoContent) }) if err := c.DeleteSensor(99); err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errFmtUnexpected, err) } } -func TestDeleteSensor_NotFound(t *testing.T) { +func TestDeleteSensorNotFound(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "not found", http.StatusNotFound) + http.Error(w, errMsgNotFound, http.StatusNotFound) }) if err := c.DeleteSensor(0); err == nil { - t.Fatal("expected error on 404, got nil") + t.Fatal(errExpected404) } } // ── Anomaly ─────────────────────────────────────────────────────────────────── -func TestDisconnect_Success(t *testing.T) { +func TestDisconnectSuccess(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodPost) - assertPath(t, r, "/sim/gateways/uuid-1/anomaly/disconnect") + assertPath(t, r, pathGatewaysPrefix+gwUUID1+"/anomaly/disconnect") var req client.DisconnectRequest decodeBody(t, r, &req) @@ -335,24 +348,24 @@ func TestDisconnect_Success(t *testing.T) { } w.WriteHeader(http.StatusNoContent) }) - if err := c.Disconnect("uuid-1", 5); err != nil { - t.Fatalf("unexpected error: %v", err) + if err := c.Disconnect(gwUUID1, 5); err != nil { + t.Fatalf(errFmtUnexpected, err) } } -func TestDisconnect_GatewayNotFound(t *testing.T) { +func TestDisconnectGatewayNotFound(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "not found", http.StatusNotFound) + http.Error(w, errMsgNotFound, http.StatusNotFound) }) if err := c.Disconnect("ghost", 3); err == nil { - t.Fatal("expected error on 404, got nil") + t.Fatal(errExpected404) } } -func TestInjectNetworkDegradation_WithPacketLoss(t *testing.T) { +func TestInjectNetworkDegradationWithPacketLoss(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodPost) - assertPath(t, r, "/sim/gateways/uuid-1/anomaly/network-degradation") + assertPath(t, r, pathGatewaysPrefix+gwUUID1+"/anomaly/network-degradation") var req client.NetworkDegradationRequest decodeBody(t, r, &req) @@ -364,12 +377,12 @@ func TestInjectNetworkDegradation_WithPacketLoss(t *testing.T) { } w.WriteHeader(http.StatusNoContent) }) - if err := c.InjectNetworkDegradation("uuid-1", 10, 0.5); err != nil { - t.Fatalf("unexpected error: %v", err) + if err := c.InjectNetworkDegradation(gwUUID1, 10, 0.5); err != nil { + t.Fatalf(errFmtUnexpected, err) } } -func TestInjectNetworkDegradation_DefaultPacketLoss(t *testing.T) { +func TestInjectNetworkDegradationDefaultPacketLoss(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) var req map[string]any @@ -380,12 +393,12 @@ func TestInjectNetworkDegradation_DefaultPacketLoss(t *testing.T) { } w.WriteHeader(http.StatusNoContent) }) - if err := c.InjectNetworkDegradation("uuid-1", 5, 0); err != nil { - t.Fatalf("unexpected error: %v", err) + if err := c.InjectNetworkDegradation(gwUUID1, 5, 0); err != nil { + t.Fatalf(errFmtUnexpected, err) } } -func TestInjectOutlier_WithValue(t *testing.T) { +func TestInjectOutlierWithValue(t *testing.T) { val := 999.9 _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodPost) @@ -400,11 +413,11 @@ func TestInjectOutlier_WithValue(t *testing.T) { w.WriteHeader(http.StatusNoContent) }) if err := c.InjectOutlier(42, &val); err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errFmtUnexpected, err) } } -func TestInjectOutlier_NoValue(t *testing.T) { +func TestInjectOutlierNoValue(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) var req map[string]any @@ -415,20 +428,20 @@ func TestInjectOutlier_NoValue(t *testing.T) { w.WriteHeader(http.StatusNoContent) }) if err := c.InjectOutlier(42, nil); err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errFmtUnexpected, err) } } -func TestInjectOutlier_SensorNotFound(t *testing.T) { +func TestInjectOutlierSensorNotFound(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "not found", http.StatusNotFound) + http.Error(w, errMsgNotFound, http.StatusNotFound) }) if err := c.InjectOutlier(0, nil); err == nil { - t.Fatal("expected error on 404, got nil") + t.Fatal(errExpected404) } } -func TestGetGateway_ErrorIncludesStatusAndBody(t *testing.T) { +func TestGetGatewayErrorIncludesStatusAndBody(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte("gateway id format is invalid")) @@ -446,11 +459,11 @@ func TestGetGateway_ErrorIncludesStatusAndBody(t *testing.T) { } } -func TestCreateGateway_InvalidJSONResponse(t *testing.T) { +func TestCreateGatewayInvalidJSONResponse(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set(headerContentType, mediaTypeJSON) w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte("{invalid-json")) + _, _ = w.Write([]byte(invalidJSONPayloadBroken)) }) _, err := c.CreateGateway(client.CreateGatewayRequest{ @@ -459,28 +472,28 @@ func TestCreateGateway_InvalidJSONResponse(t *testing.T) { SerialNumber: "SN-1", }) if err == nil { - t.Fatal("expected decode error, got nil") + t.Fatal(errExpectedDecode) } } -func TestListGateways_InvalidJSONResponse(t *testing.T) { +func TestListGatewaysInvalidJSONResponse(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set(headerContentType, mediaTypeJSON) w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("not-json")) + _, _ = w.Write([]byte(invalidJSONPayload)) }) _, err := c.ListGateways() if err == nil { - t.Fatal("expected decode error, got nil") + t.Fatal(errExpectedDecode) } } -func TestBulkCreateGateways_InvalidJSONResponse(t *testing.T) { +func TestBulkCreateGatewaysInvalidJSONResponse(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set(headerContentType, mediaTypeJSON) w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte("not-json")) + _, _ = w.Write([]byte(invalidJSONPayload)) }) _, err := c.BulkCreateGateways(client.BulkCreateGatewaysRequest{ @@ -489,45 +502,45 @@ func TestBulkCreateGateways_InvalidJSONResponse(t *testing.T) { FactoryKey: "k-1", }) if err == nil { - t.Fatal("expected decode error, got nil") + t.Fatal(errExpectedDecode) } } -func TestGetGateway_InvalidJSONResponse(t *testing.T) { +func TestGetGatewayInvalidJSONResponse(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set(headerContentType, mediaTypeJSON) w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("not-json")) + _, _ = w.Write([]byte(invalidJSONPayload)) }) _, err := c.GetGateway("uuid-1") if err == nil { - t.Fatal("expected decode error, got nil") + t.Fatal(errExpectedDecode) } } -func TestAddSensor_InvalidJSONResponse(t *testing.T) { +func TestAddSensorInvalidJSONResponse(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set(headerContentType, mediaTypeJSON) w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte("not-json")) + _, _ = w.Write([]byte(invalidJSONPayload)) }) _, err := c.AddSensor(1, client.AddSensorRequest{Type: "temperature", Algorithm: "constant"}) if err == nil { - t.Fatal("expected decode error, got nil") + t.Fatal(errExpectedDecode) } } -func TestListSensors_InvalidJSONResponse(t *testing.T) { +func TestListSensorsInvalidJSONResponse(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set(headerContentType, mediaTypeJSON) w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("not-json")) + _, _ = w.Write([]byte(invalidJSONPayload)) }) _, err := c.ListSensors(1) if err == nil { - t.Fatal("expected decode error, got nil") + t.Fatal(errExpectedDecode) } } From 157c8dd63d9eeb2fdd4639b18477e1fe30b3bd47 Mon Sep 17 00:00:00 2001 From: marcon-effe Date: Sat, 4 Apr 2026 10:26:05 +0000 Subject: [PATCH 2/2] feat: enhance client context management and improve command handling --- Dockerfile | 2 +- cmd/anomalies.go | 6 ++-- cmd/commands_test.go | 61 ++++++++++++++++++++++++++++++++++----- cmd/gateways.go | 25 ++++++---------- cmd/root.go | 15 +++++++++- cmd/sensors.go | 42 +++++++++++++++++---------- cmd/shell.go | 3 +- internal/client/client.go | 30 ++++++++++++++----- 8 files changed, 131 insertions(+), 53 deletions(-) diff --git a/Dockerfile b/Dockerfile index b62b23b..8a01d28 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o sim-cli . FROM ghcr.io/notipswe/notip-go-base:v0.0.1 AS prod LABEL org.opencontainers.image.source="https://github.com/NoTIPswe/notip-simulator-cli" \ - org.opencontainers.image.description="NoTIP Simulator CLI" \ + org.opencontainers.image.description="NoTIP Simulator CLI" \ org.opencontainers.image.licenses="MIT" RUN groupadd -r appuser && useradd -r -g appuser appuser \ diff --git a/cmd/anomalies.go b/cmd/anomalies.go index bfe619f..494903a 100644 --- a/cmd/anomalies.go +++ b/cmd/anomalies.go @@ -35,7 +35,7 @@ var anomaliesDisconnectCmd = &cobra.Command{ spinner := startSpinner( fmt.Sprintf("Triggering disconnect anomaly on gateway %s (%ds)...", args[0], duration), ) - if err := client.New(simulatorURL).Disconnect(args[0], duration); err != nil { + if err := client.New(simulatorURL).WithContext(cmd.Context()).Disconnect(args[0], duration); err != nil { spinner.Fail("Failed to trigger disconnect") return err } @@ -58,7 +58,7 @@ var anomaliesNetworkDegradationCmd = &cobra.Command{ fmt.Sprintf("Triggering network-degradation on gateway %s (%ds, %.0f%% loss)...", args[0], duration, loss*100), ) - if err := client.New(simulatorURL).InjectNetworkDegradation(args[0], duration, loss); err != nil { + if err := client.New(simulatorURL).WithContext(cmd.Context()).InjectNetworkDegradation(args[0], duration, loss); err != nil { spinner.Fail("Failed to trigger network degradation") return err } @@ -88,7 +88,7 @@ var anomaliesOutlierCmd = &cobra.Command{ spinner := startSpinner( fmt.Sprintf("Injecting outlier into sensor %d...", sensorID), ) - if err := client.New(simulatorURL).InjectOutlier(sensorID, valuePtr); err != nil { + if err := client.New(simulatorURL).WithContext(cmd.Context()).InjectOutlier(sensorID, valuePtr); err != nil { spinner.Fail("Failed to inject outlier") return err } diff --git a/cmd/commands_test.go b/cmd/commands_test.go index d443827..b41d583 100644 --- a/cmd/commands_test.go +++ b/cmd/commands_test.go @@ -32,16 +32,21 @@ func TestMain(m *testing.M) { const ( testGatewayUUID = "uuid-1" + testSensorUUID = "s-uuid-1" cmdNetDegradation = "network-degradation" fmtUnexpectedPath = "unexpected path: %s" fmtUnexpectedRequest = "unexpected request: %s %s" testFlagFactoryID = "--factory-id" testFlagFactoryKey = "--factory-key" testFlagDuration = "--duration" + testFlagType = "--type" + testFlagAlgorithm = "--algorithm" bodyNotFound = "not found" errExpected404 = "expected error on 404" fmtPipeErr = "pipe: %v" fmtWriteErr = "write: %v" + pathGatewayUUID = "/sim/gateways/" + testGatewayUUID + pathGatewaySensors = "/sim/gateways/5/sensors" ) // ── helpers ─────────────────────────────────────────────────────────────────── @@ -169,14 +174,16 @@ func TestGatewaysDeleteNoArgs(t *testing.T) { } } -func TestSensorsAddNonNumericID(t *testing.T) { +func TestSensorsAddInvalidGatewayIdentifier(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { - // Should never reach the server — arg parsing fails first. - t.Error("server should not have been called") + if r.URL.Path != "/sim/gateways/not-a-number" { + t.Errorf(fmtUnexpectedPath, r.URL.Path) + } + http.Error(w, bodyNotFound, http.StatusNotFound) }) if err := runCmd("sensors", "add", "not-a-number", - "--type", "temperature", "--min", "0", "--max", "100", "--algorithm", "constant"); err == nil { - t.Error("expected error for non-numeric gateway ID") + testFlagType, "temperature", "--min", "0", "--max", "100", testFlagAlgorithm, "constant"); err == nil { + t.Error("expected error for invalid gateway identifier") } } @@ -245,7 +252,7 @@ func TestGatewaysStopIntegration(t *testing.T) { func TestGatewaysDeleteIntegration(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodDelete || r.URL.Path != "/sim/gateways/uuid-1" { + if r.Method != http.MethodDelete || r.URL.Path != pathGatewayUUID { t.Errorf(fmtUnexpectedRequest, r.Method, r.URL.Path) } w.WriteHeader(http.StatusNoContent) @@ -257,10 +264,10 @@ func TestGatewaysDeleteIntegration(t *testing.T) { func TestSensorsListIntegration(t *testing.T) { sensors := []map[string]any{ - {"id": 1, "gatewayId": 5, "sensorId": "s-uuid-1", "type": "temperature", "minRange": 0, "maxRange": 100, "algorithm": "sine_wave"}, + {"id": 1, "gatewayId": 5, "sensorId": testSensorUUID, "type": "temperature", "minRange": 0, "maxRange": 100, "algorithm": "sine_wave"}, } newMockServer(t, func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/sim/gateways/5/sensors" { + if r.URL.Path != pathGatewaySensors { t.Errorf(fmtUnexpectedPath, r.URL.Path) } writeJSON(w, http.StatusOK, sensors) @@ -270,6 +277,44 @@ func TestSensorsListIntegration(t *testing.T) { } } +func TestSensorsListUUIDIntegration(t *testing.T) { + sensors := []map[string]any{ + {"id": 1, "gatewayId": 5, "sensorId": "s-uuid-1", "type": "temperature", "minRange": 0, "maxRange": 100, "algorithm": "sine_wave"}, + } + newMockServer(t, func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case pathGatewayUUID: + writeJSON(w, http.StatusOK, map[string]any{"id": 5, "managementGatewayId": testGatewayUUID}) + case pathGatewaySensors: + writeJSON(w, http.StatusOK, sensors) + default: + t.Errorf(fmtUnexpectedPath, r.URL.Path) + } + }) + if err := runCmd("sensors", "list", testGatewayUUID); err != nil { + t.Fatalf("sensors list by uuid failed: %v", err) + } +} + +func TestSensorsAddUUIDIntegration(t *testing.T) { + newMockServer(t, func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case pathGatewayUUID: + writeJSON(w, http.StatusOK, map[string]any{"id": 5, "managementGatewayId": testGatewayUUID}) + case pathGatewaySensors: + writeJSON(w, http.StatusCreated, map[string]any{ + "id": 1, "gatewayId": 5, "sensorId": testSensorUUID, "type": "temperature", "minRange": 0, "maxRange": 100, "algorithm": "constant", + }) + default: + t.Errorf(fmtUnexpectedPath, r.URL.Path) + } + }) + if err := runCmd("sensors", "add", testGatewayUUID, + testFlagType, "temperature", "--min", "0", "--max", "100", testFlagAlgorithm, "constant"); err != nil { + t.Fatalf("sensors add by uuid failed: %v", err) + } +} + func TestSensorsDeleteIntegration(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete || r.URL.Path != "/sim/sensors/99" { diff --git a/cmd/gateways.go b/cmd/gateways.go index 39c1a27..feb483e 100644 --- a/cmd/gateways.go +++ b/cmd/gateways.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "os" "strconv" "github.com/NoTIPswe/notip-simulator-cli/internal/client" @@ -28,7 +27,7 @@ var gatewaysListCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { spinner := startSpinner("Fetching gateways...") - c := client.New(simulatorURL) + c := client.New(simulatorURL).WithContext(cmd.Context()) gateways, err := c.ListGateways() if err != nil { spinner.Fail("Failed to fetch gateways") @@ -68,7 +67,7 @@ var gatewaysGetCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { spinner := startSpinner("Fetching gateway " + args[0] + "...") - c := client.New(simulatorURL) + c := client.New(simulatorURL).WithContext(cmd.Context()) gw, err := c.GetGateway(args[0]) if err != nil { spinner.Fail("Failed to fetch gateway") @@ -108,7 +107,7 @@ var gatewaysCreateCmd = &cobra.Command{ req.SendFrequencyMs, _ = cmd.Flags().GetInt("freq") spinner := startSpinner("Creating gateway...") - c := client.New(simulatorURL) + c := client.New(simulatorURL).WithContext(cmd.Context()) gw, err := c.CreateGateway(req) if err != nil { spinner.Fail("Failed to create gateway") @@ -137,7 +136,7 @@ var gatewaysBulkCmd = &cobra.Command{ spinner := startSpinner( fmt.Sprintf("Creating %d gateway(s)...", req.Count), ) - c := client.New(simulatorURL) + c := client.New(simulatorURL).WithContext(cmd.Context()) result, err := c.BulkCreateGateways(req) if err != nil { spinner.Fail("Failed to create gateways") @@ -175,7 +174,7 @@ var gatewaysStartCmd = &cobra.Command{ Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { spinner := startSpinner("Starting gateway " + args[0] + "...") - if err := client.New(simulatorURL).StartGateway(args[0]); err != nil { + if err := client.New(simulatorURL).WithContext(cmd.Context()).StartGateway(args[0]); err != nil { spinner.Fail("Failed to start gateway") return err } @@ -192,7 +191,7 @@ var gatewaysStopCmd = &cobra.Command{ Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { spinner := startSpinner("Stopping gateway " + args[0] + "...") - if err := client.New(simulatorURL).StopGateway(args[0]); err != nil { + if err := client.New(simulatorURL).WithContext(cmd.Context()).StopGateway(args[0]); err != nil { spinner.Fail("Failed to stop gateway") return err } @@ -209,7 +208,7 @@ var gatewaysDeleteCmd = &cobra.Command{ Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { spinner := startSpinner("Deleting gateway " + args[0] + "...") - if err := client.New(simulatorURL).DeleteGateway(args[0]); err != nil { + if err := client.New(simulatorURL).WithContext(cmd.Context()).DeleteGateway(args[0]); err != nil { spinner.Fail("Failed to delete gateway") return err } @@ -272,10 +271,7 @@ func init() { gatewaysCreateCmd.Flags().String("firmware", "", "Firmware version") gatewaysCreateCmd.Flags().Int("freq", 1000, "Send frequency in milliseconds") for _, f := range []string{flagFactoryID, flagFactoryKey, "serial"} { - if err := gatewaysCreateCmd.MarkFlagRequired(f); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } + mustMarkRequired(gatewaysCreateCmd, f) } // bulk flags @@ -286,9 +282,6 @@ func init() { gatewaysBulkCmd.Flags().String("firmware", "", "Firmware version") gatewaysBulkCmd.Flags().Int("freq", 1000, "Send frequency in milliseconds") for _, f := range []string{"count", flagFactoryID, flagFactoryKey} { - if err := gatewaysBulkCmd.MarkFlagRequired(f); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } + mustMarkRequired(gatewaysBulkCmd, f) } } diff --git a/cmd/root.go b/cmd/root.go index 1974149..3a6abd9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,7 @@ import ( "github.com/pterm/pterm" "github.com/spf13/cobra" + "github.com/spf13/pflag" "golang.org/x/term" ) @@ -21,8 +22,20 @@ func Execute() error { return rootCmd.Execute() } +// resetAllCommandFlags clears flag values/changed state across the command tree. +// Cobra flag state is sticky within the same process, which affects shell mode. +func resetAllCommandFlags(c *cobra.Command) { + c.Flags().VisitAll(func(f *pflag.Flag) { + _ = f.Value.Set(f.DefValue) + f.Changed = false + }) + for _, child := range c.Commands() { + resetAllCommandFlags(child) + } +} + func init() { - // Disable PTerm styling when stdout is not a TTY (e.g., CI pipelines, + // Disable PTerm styling when stdout is not a TTY (e.g., CI pipelines, redirected output). if !term.IsTerminal(int(os.Stdout.Fd())) { pterm.DisableStyling() pterm.DisableColor() diff --git a/cmd/sensors.go b/cmd/sensors.go index f5b6726..e373181 100644 --- a/cmd/sensors.go +++ b/cmd/sensors.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "os" "strconv" "github.com/NoTIPswe/notip-simulator-cli/internal/client" @@ -15,16 +14,29 @@ var sensorsCmd = &cobra.Command{ Short: "Manage sensors attached to gateways", } +func resolveGatewayID(c *client.Client, input string) (int64, error) { + if gatewayID, err := strconv.ParseInt(input, 10, 64); err == nil { + return gatewayID, nil + } + + gw, err := c.GetGateway(input) + if err != nil { + return 0, fmt.Errorf("gateway must be a numeric ID or a valid gateway UUID: %w", err) + } + return gw.ID, nil +} + // ── add ─────────────────────────────────────────────────────────────────────── var sensorsAddCmd = &cobra.Command{ - Use: "add ", - Short: "Add a sensor to a gateway (uses the numeric gateway ID, not the UUID)", + Use: "add ", + Short: "Add a sensor to a gateway (accepts numeric ID or UUID)", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - gatewayID, err := strconv.ParseInt(args[0], 10, 64) + c := client.New(simulatorURL).WithContext(cmd.Context()) + gatewayID, err := resolveGatewayID(c, args[0]) if err != nil { - return fmt.Errorf("gateway-id must be a numeric ID: %w", err) + return err } req := client.AddSensorRequest{} @@ -36,7 +48,7 @@ var sensorsAddCmd = &cobra.Command{ spinner := startSpinner( fmt.Sprintf("Adding %s sensor to gateway %d...", req.Type, gatewayID), ) - sensor, err := client.New(simulatorURL).AddSensor(gatewayID, req) + sensor, err := c.AddSensor(gatewayID, req) if err != nil { spinner.Fail("Failed to add sensor") return err @@ -50,19 +62,20 @@ var sensorsAddCmd = &cobra.Command{ // ── list ────────────────────────────────────────────────────────────────────── var sensorsListCmd = &cobra.Command{ - Use: "list ", - Short: "List all sensors for a gateway (uses the numeric gateway ID, not the UUID)", + Use: "list ", + Short: "List sensors for a gateway (accepts numeric ID or UUID)", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - gatewayID, err := strconv.ParseInt(args[0], 10, 64) + c := client.New(simulatorURL).WithContext(cmd.Context()) + gatewayID, err := resolveGatewayID(c, args[0]) if err != nil { - return fmt.Errorf("gateway-id must be a numeric ID: %w", err) + return err } spinner := startSpinner( fmt.Sprintf("Fetching sensors for gateway %d...", gatewayID), ) - sensors, err := client.New(simulatorURL).ListSensors(gatewayID) + sensors, err := c.ListSensors(gatewayID) if err != nil { spinner.Fail("Failed to fetch sensors") return err @@ -91,7 +104,7 @@ var sensorsDeleteCmd = &cobra.Command{ } spinner := startSpinner(fmt.Sprintf("Deleting sensor %d...", sensorID)) - if err := client.New(simulatorURL).DeleteSensor(sensorID); err != nil { + if err := client.New(simulatorURL).WithContext(cmd.Context()).DeleteSensor(sensorID); err != nil { spinner.Fail("Failed to delete sensor") return err } @@ -131,9 +144,6 @@ func init() { sensorsAddCmd.Flags().Float64("max", 100, "Maximum range value (required)") sensorsAddCmd.Flags().String("algorithm", "", "Generation algorithm: uniform_random|sine_wave|spike|constant (required)") for _, f := range []string{"type", "min", "max", "algorithm"} { - if err := sensorsAddCmd.MarkFlagRequired(f); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } + mustMarkRequired(sensorsAddCmd, f) } } diff --git a/cmd/shell.go b/cmd/shell.go index d3dbd97..091d430 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -54,6 +54,7 @@ restarting the container. Type 'help' for available commands, 'exit' to quit.`, continue } + resetAllCommandFlags(rootCmd) rootCmd.SetArgs(parts) if execErr := rootCmd.Execute(); execErr != nil { pterm.Error.Println(execErr) @@ -85,7 +86,7 @@ func printWelcomeBanner() { Println("Type a command to execute it.\n" + pterm.Gray(" gateways list\n") + pterm.Gray(" gateways get \n") + - pterm.Gray(" sensors list \n") + + pterm.Gray(" sensors list \n") + pterm.Gray(" anomalies disconnect --duration 5\n") + "\nType " + pterm.Bold.Sprint("exit") + " or press Ctrl+D to quit.") } diff --git a/internal/client/client.go b/internal/client/client.go index 85f8e5a..1c71949 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "strconv" + "time" ) // ── Internal constants ──────────────────────────────────────────────────────── @@ -113,14 +114,29 @@ type OutlierRequest struct { type Client struct { baseURL string httpClient *http.Client + ctx context.Context } +// defaultTimeout is the maximum time allowed for a single HTTP request. +const defaultTimeout = 30 * time.Second + // New creates a new Client targeting the given baseURL. func New(baseURL string) *Client { return &Client{ baseURL: baseURL, - httpClient: &http.Client{}, + httpClient: &http.Client{Timeout: defaultTimeout}, + ctx: context.Background(), + } +} + +// WithContext returns a shallow copy of the client bound to ctx. +func (c *Client) WithContext(ctx context.Context) *Client { + if ctx == nil { + ctx = context.Background() } + cc := *c + cc.ctx = ctx + return &cc } // ── Gateway endpoints ───────────────────────────────────────────────────────── @@ -155,7 +171,7 @@ func (c *Client) BulkCreateGateways(req BulkCreateGatewaysRequest) (*BulkCreateR // ListGateways calls GET /sim/gateways. func (c *Client) ListGateways() ([]Gateway, error) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, c.baseURL+pathGateways[:len(pathGateways)-1], nil) + req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, c.baseURL+pathGateways[:len(pathGateways)-1], nil) if err != nil { return nil, fmt.Errorf(errBuildRequest, err) } @@ -173,7 +189,7 @@ func (c *Client) ListGateways() ([]Gateway, error) { // GetGateway calls GET /sim/gateways/{id} (UUID). func (c *Client) GetGateway(id string) (*Gateway, error) { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, c.baseURL+pathGateways+id, nil) + req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, c.baseURL+pathGateways+id, nil) if err != nil { return nil, fmt.Errorf(errBuildRequest, err) } @@ -211,7 +227,7 @@ func (c *Client) StopGateway(id string) error { // DeleteGateway calls DELETE /sim/gateways/{id} (UUID). func (c *Client) DeleteGateway(id string) error { - req, err := http.NewRequestWithContext(context.Background(), http.MethodDelete, c.baseURL+pathGateways+id, nil) + req, err := http.NewRequestWithContext(c.ctx, http.MethodDelete, c.baseURL+pathGateways+id, nil) if err != nil { return fmt.Errorf(errBuildRequest, err) } @@ -245,7 +261,7 @@ func (c *Client) AddSensor(gatewayID int64, req AddSensorRequest) (*Sensor, erro // gatewayID is the numeric int64 ID. func (c *Client) ListSensors(gatewayID int64) ([]Sensor, error) { url := c.baseURL + pathGateways + strconv.FormatInt(gatewayID, 10) + "/sensors" - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf(errBuildRequest, err) } @@ -264,7 +280,7 @@ func (c *Client) ListSensors(gatewayID int64) ([]Sensor, error) { // DeleteSensor calls DELETE /sim/sensors/{sensorId}. // sensorID is the numeric int64 ID. func (c *Client) DeleteSensor(sensorID int64) error { - req, err := http.NewRequestWithContext(context.Background(), http.MethodDelete, c.baseURL+pathSensors+strconv.FormatInt(sensorID, 10), nil) + req, err := http.NewRequestWithContext(c.ctx, http.MethodDelete, c.baseURL+pathSensors+strconv.FormatInt(sensorID, 10), nil) if err != nil { return fmt.Errorf(errBuildRequest, err) } @@ -328,7 +344,7 @@ func (c *Client) post(path string, body any) (*http.Response, error) { } r = bytes.NewReader(b) } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, c.baseURL+path, r) + req, err := http.NewRequestWithContext(c.ctx, http.MethodPost, c.baseURL+path, r) if err != nil { return nil, fmt.Errorf(errBuildRequest, err) }