From af4a2840227c830d76527c55767990e942d9d50d Mon Sep 17 00:00:00 2001 From: marcon-effe Date: Sat, 4 Apr 2026 12:21:51 +0000 Subject: [PATCH 1/9] feat: make model, firmware, and frequency required flags for gateways commands --- cmd/commands_test.go | 40 ++++++++++++++++++++++++++++++++----- cmd/gateways.go | 16 +++++++-------- cmd/request_mapping_test.go | 32 ++++++++++++++--------------- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/cmd/commands_test.go b/cmd/commands_test.go index b41d583..fdaf677 100644 --- a/cmd/commands_test.go +++ b/cmd/commands_test.go @@ -41,6 +41,9 @@ const ( testFlagDuration = "--duration" testFlagType = "--type" testFlagAlgorithm = "--algorithm" + testFlagModel = "--model" + testFlagFirmware = "--firmware" + testFlagFreq = "--freq" bodyNotFound = "not found" errExpected404 = "expected error on 404" fmtPipeErr = "pipe: %v" @@ -130,14 +133,20 @@ func TestCommandTree(t *testing.T) { // ── Required-flag validation ────────────────────────────────────────────────── func TestGatewaysCreateMissingRequiredFlags(t *testing.T) { - // factory-id, factory-key, serial are all required + // all create flags are required if err := runCmd("gateways", "create"); err == nil { t.Error("expected error when required flags are missing") } } func TestGatewaysBulkMissingCount(t *testing.T) { - if err := runCmd("gateways", "bulk", testFlagFactoryID, "f", testFlagFactoryKey, "k"); err == nil { + if err := runCmd("gateways", "bulk", + testFlagFactoryID, "f", + testFlagFactoryKey, "k", + testFlagModel, "GW-X", + testFlagFirmware, "1.0.0", + testFlagFreq, "1000", + ); err == nil { t.Error("expected error when --count is missing") } } @@ -405,7 +414,14 @@ func TestGatewaysCreateServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, "bad request", http.StatusBadRequest) }) - err := runCmd("gateways", "create", testFlagFactoryID, "f", testFlagFactoryKey, "k", "--serial", "SN") + err := runCmd("gateways", "create", + testFlagFactoryID, "f", + testFlagFactoryKey, "k", + "--serial", "SN", + testFlagModel, "GW-X", + testFlagFirmware, "1.0.0", + testFlagFreq, "1000", + ) if err == nil { t.Error("expected error when server returns 400") } @@ -415,7 +431,14 @@ func TestGatewaysBulkServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, "server error", http.StatusInternalServerError) }) - err := runCmd("gateways", "bulk", "--count", "2", testFlagFactoryID, "f", testFlagFactoryKey, "k") + err := runCmd("gateways", "bulk", + "--count", "2", + testFlagFactoryID, "f", + testFlagFactoryKey, "k", + testFlagModel, "GW-X", + testFlagFirmware, "1.0.0", + testFlagFreq, "1000", + ) if err == nil { t.Error("expected error when bulk server returns 500") } @@ -428,7 +451,14 @@ func TestGatewaysBulkPartialErrors(t *testing.T) { "errors": []any{"", "factory key mismatch"}, }) }) - err := runCmd("gateways", "bulk", "--count", "2", testFlagFactoryID, "f", testFlagFactoryKey, "k") + err := runCmd("gateways", "bulk", + "--count", "2", + testFlagFactoryID, "f", + testFlagFactoryKey, "k", + testFlagModel, "GW-X", + testFlagFirmware, "1.0.0", + testFlagFreq, "1000", + ) if err != nil { t.Fatalf("bulk partial error should succeed at cmd level: %v", err) } diff --git a/cmd/gateways.go b/cmd/gateways.go index feb483e..f0b6d13 100644 --- a/cmd/gateways.go +++ b/cmd/gateways.go @@ -267,10 +267,10 @@ func init() { gatewaysCreateCmd.Flags().String(flagFactoryID, "", "Factory ID (required)") gatewaysCreateCmd.Flags().String(flagFactoryKey, "", "Factory key (required)") gatewaysCreateCmd.Flags().String("serial", "", "Serial number (required)") - gatewaysCreateCmd.Flags().String("model", "", "Gateway model") - gatewaysCreateCmd.Flags().String("firmware", "", "Firmware version") - gatewaysCreateCmd.Flags().Int("freq", 1000, "Send frequency in milliseconds") - for _, f := range []string{flagFactoryID, flagFactoryKey, "serial"} { + gatewaysCreateCmd.Flags().String("model", "", "Gateway model (required)") + gatewaysCreateCmd.Flags().String("firmware", "", "Firmware version (required)") + gatewaysCreateCmd.Flags().Int("freq", 1000, "Send frequency in milliseconds (required)") + for _, f := range []string{flagFactoryID, flagFactoryKey, "serial", "model", "firmware", "freq"} { mustMarkRequired(gatewaysCreateCmd, f) } @@ -278,10 +278,10 @@ func init() { gatewaysBulkCmd.Flags().Int("count", 1, "Number of gateways to create (required)") gatewaysBulkCmd.Flags().String(flagFactoryID, "", "Factory ID (required)") gatewaysBulkCmd.Flags().String(flagFactoryKey, "", "Factory key (required)") - gatewaysBulkCmd.Flags().String("model", "", "Gateway model") - gatewaysBulkCmd.Flags().String("firmware", "", "Firmware version") - gatewaysBulkCmd.Flags().Int("freq", 1000, "Send frequency in milliseconds") - for _, f := range []string{"count", flagFactoryID, flagFactoryKey} { + gatewaysBulkCmd.Flags().String("model", "", "Gateway model (required)") + gatewaysBulkCmd.Flags().String("firmware", "", "Firmware version (required)") + gatewaysBulkCmd.Flags().Int("freq", 1000, "Send frequency in milliseconds (required)") + for _, f := range []string{"count", flagFactoryID, flagFactoryKey, "model", "firmware", "freq"} { mustMarkRequired(gatewaysBulkCmd, f) } } diff --git a/cmd/request_mapping_test.go b/cmd/request_mapping_test.go index e36039e..1a12e40 100644 --- a/cmd/request_mapping_test.go +++ b/cmd/request_mapping_test.go @@ -74,16 +74,16 @@ func TestGatewaysCreateFlagToJSONMapping(t *testing.T) { testFlagFactoryID, "fac-42", testFlagFactoryKey, "secret-key", flagSerialArg, "SN-XYZ", - "--model", "GW-PRO", - "--firmware", "3.0.1", - "--freq", "250", + testFlagModel, "GW-PRO", + testFlagFirmware, "3.0.1", + testFlagFreq, "250", ) if err != nil { t.Fatalf("gateways create failed: %v", err) } } -func TestGatewaysCreateDefaultFreqIs1000(t *testing.T) { +func TestGatewaysCreateFreqMapping(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { body := readBody(t, r) checkKey(t, body, "sendFrequencyMs", float64(1000)) @@ -94,27 +94,25 @@ func TestGatewaysCreateDefaultFreqIs1000(t *testing.T) { testFlagFactoryID, "f", testFlagFactoryKey, "k", flagSerialArg, "SN", + testFlagModel, "GW-X", + testFlagFirmware, "1.0.0", + testFlagFreq, "1000", ) if err != nil { t.Fatalf(fmtUnexpectedError, err) } } -func TestGatewaysCreateOptionalFieldsOmittedWhenNotProvided(t *testing.T) { - newMockServer(t, func(w http.ResponseWriter, r *http.Request) { - body := readBody(t, r) - checkAbsent(t, body, "model") - checkAbsent(t, body, "firmwareVersion") - writeJSON(w, http.StatusCreated, map[string]any{"id": 1}) - }) - +func TestGatewaysCreateMissingModelFails(t *testing.T) { err := runCmd("gateways", "create", testFlagFactoryID, "f", testFlagFactoryKey, "k", flagSerialArg, "SN", + testFlagFirmware, "1.0.0", + testFlagFreq, "1000", ) - if err != nil { - t.Fatalf(fmtUnexpectedError, err) + if err == nil { + t.Fatal("expected error when --model is missing") } } @@ -143,9 +141,9 @@ func TestGatewaysBulkFlagToJSONMapping(t *testing.T) { "--count", "5", testFlagFactoryID, "fac-bulk", testFlagFactoryKey, "key-bulk", - "--model", "GW-MINI", - "--firmware", "1.2.3", - "--freq", "500", + testFlagModel, "GW-MINI", + testFlagFirmware, "1.2.3", + testFlagFreq, "500", ) if err != nil { t.Fatalf("gateways bulk failed: %v", err) From 79ada90ee78c9fd38d363247cbe1093775db5651 Mon Sep 17 00:00:00 2001 From: marcon-effe Date: Sat, 4 Apr 2026 13:26:50 +0000 Subject: [PATCH 2/9] fix: remove serial number from gateway list and detail outputs --- cmd/gateways.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cmd/gateways.go b/cmd/gateways.go index f0b6d13..d85cf00 100644 --- a/cmd/gateways.go +++ b/cmd/gateways.go @@ -41,7 +41,7 @@ var gatewaysListCmd = &cobra.Command{ } tableData := pterm.TableData{ - {"ID", "UUID", "Status", "Model", "Serial", "Freq (ms)", "Tenant"}, + {"ID", "UUID", "Status", "Model", "Freq (ms)", "Tenant"}, } for _, gw := range gateways { tableData = append(tableData, []string{ @@ -49,7 +49,6 @@ var gatewaysListCmd = &cobra.Command{ gw.ManagementGatewayID, statusStyle(gw.Status), gw.Model, - gw.SerialNumber, strconv.Itoa(gw.SendFrequencyMs), gw.TenantID, }) @@ -80,7 +79,6 @@ var gatewaysGetCmd = &cobra.Command{ {"ID", strconv.FormatInt(gw.ID, 10)}, {"UUID", gw.ManagementGatewayID}, {"Factory ID", gw.FactoryID}, - {"Serial", gw.SerialNumber}, {"Model", gw.Model}, {"Firmware", gw.FirmwareVersion}, {"Status", statusStyle(gw.Status)}, @@ -235,14 +233,13 @@ func printGatewayTable(gateways []client.Gateway) { if len(gateways) == 0 { return } - tableData := pterm.TableData{{"ID", "UUID", "Status", "Model", "Serial", "Freq (ms)"}} + tableData := pterm.TableData{{"ID", "UUID", "Status", "Model", "Freq (ms)"}} for _, gw := range gateways { tableData = append(tableData, []string{ strconv.FormatInt(gw.ID, 10), gw.ManagementGatewayID, statusStyle(gw.Status), gw.Model, - gw.SerialNumber, strconv.Itoa(gw.SendFrequencyMs), }) } From b0c52a87247244625ef63413ef3207e9e0ca10a0 Mon Sep 17 00:00:00 2001 From: marcon-effe Date: Sat, 4 Apr 2026 13:34:55 +0000 Subject: [PATCH 3/9] fix: remove serial number from gateway creation and related tests --- cmd/commands_test.go | 3 +-- cmd/gateways.go | 4 +--- cmd/request_mapping_test.go | 6 ------ internal/client/client.go | 2 -- internal/client/client_test.go | 12 +++++------- internal/client/request_construction_test.go | 7 ++----- 6 files changed, 9 insertions(+), 25 deletions(-) diff --git a/cmd/commands_test.go b/cmd/commands_test.go index fdaf677..9c33ab7 100644 --- a/cmd/commands_test.go +++ b/cmd/commands_test.go @@ -212,7 +212,7 @@ func TestAnomaliesOutlierNonNumericID(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"}, + {"id": 1, "managementGatewayId": testGatewayUUID, "status": "online", "model": "X", "sendFrequencyMs": 1000, "tenantId": "t1"}, } newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet || r.URL.Path != "/sim/gateways" { @@ -417,7 +417,6 @@ func TestGatewaysCreateServerError(t *testing.T) { err := runCmd("gateways", "create", testFlagFactoryID, "f", testFlagFactoryKey, "k", - "--serial", "SN", testFlagModel, "GW-X", testFlagFirmware, "1.0.0", testFlagFreq, "1000", diff --git a/cmd/gateways.go b/cmd/gateways.go index d85cf00..4601118 100644 --- a/cmd/gateways.go +++ b/cmd/gateways.go @@ -99,7 +99,6 @@ var gatewaysCreateCmd = &cobra.Command{ req := client.CreateGatewayRequest{} req.FactoryID, _ = cmd.Flags().GetString(flagFactoryID) req.FactoryKey, _ = cmd.Flags().GetString(flagFactoryKey) - req.SerialNumber, _ = cmd.Flags().GetString("serial") req.Model, _ = cmd.Flags().GetString("model") req.FirmwareVersion, _ = cmd.Flags().GetString("firmware") req.SendFrequencyMs, _ = cmd.Flags().GetInt("freq") @@ -263,11 +262,10 @@ func init() { // create flags gatewaysCreateCmd.Flags().String(flagFactoryID, "", "Factory ID (required)") gatewaysCreateCmd.Flags().String(flagFactoryKey, "", "Factory key (required)") - gatewaysCreateCmd.Flags().String("serial", "", "Serial number (required)") gatewaysCreateCmd.Flags().String("model", "", "Gateway model (required)") gatewaysCreateCmd.Flags().String("firmware", "", "Firmware version (required)") gatewaysCreateCmd.Flags().Int("freq", 1000, "Send frequency in milliseconds (required)") - for _, f := range []string{flagFactoryID, flagFactoryKey, "serial", "model", "firmware", "freq"} { + for _, f := range []string{flagFactoryID, flagFactoryKey, "model", "firmware", "freq"} { mustMarkRequired(gatewaysCreateCmd, f) } diff --git a/cmd/request_mapping_test.go b/cmd/request_mapping_test.go index 1a12e40..8cf1e71 100644 --- a/cmd/request_mapping_test.go +++ b/cmd/request_mapping_test.go @@ -13,7 +13,6 @@ import ( const ( fmtUnexpectedError = "unexpected error: %v" - flagSerialArg = "--serial" ) // readBody parses the request body into a generic map for field inspection. @@ -62,7 +61,6 @@ func TestGatewaysCreateFlagToJSONMapping(t *testing.T) { body := readBody(t, r) 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)) @@ -73,7 +71,6 @@ func TestGatewaysCreateFlagToJSONMapping(t *testing.T) { err := runCmd("gateways", "create", testFlagFactoryID, "fac-42", testFlagFactoryKey, "secret-key", - flagSerialArg, "SN-XYZ", testFlagModel, "GW-PRO", testFlagFirmware, "3.0.1", testFlagFreq, "250", @@ -93,7 +90,6 @@ func TestGatewaysCreateFreqMapping(t *testing.T) { err := runCmd("gateways", "create", testFlagFactoryID, "f", testFlagFactoryKey, "k", - flagSerialArg, "SN", testFlagModel, "GW-X", testFlagFirmware, "1.0.0", testFlagFreq, "1000", @@ -107,7 +103,6 @@ func TestGatewaysCreateMissingModelFails(t *testing.T) { err := runCmd("gateways", "create", testFlagFactoryID, "f", testFlagFactoryKey, "k", - flagSerialArg, "SN", testFlagFirmware, "1.0.0", testFlagFreq, "1000", ) @@ -162,7 +157,6 @@ func TestGatewaysGetIntegration(t *testing.T) { "managementGatewayId": "uuid-get-1", "status": "online", "model": "GW-X", - "serialNumber": "SN001", "firmwareVersion": "1.0", "sendFrequencyMs": 1000, "tenantId": "t-1", diff --git a/internal/client/client.go b/internal/client/client.go index 1c71949..5d27f38 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -29,7 +29,6 @@ type Gateway struct { ID int64 `json:"id"` ManagementGatewayID string `json:"managementGatewayId"` FactoryID string `json:"factoryId"` - SerialNumber string `json:"serialNumber"` Model string `json:"model"` FirmwareVersion string `json:"firmwareVersion"` Provisioned bool `json:"provisioned"` @@ -57,7 +56,6 @@ type Sensor struct { type CreateGatewayRequest struct { FactoryID string `json:"factoryId"` FactoryKey string `json:"factoryKey"` - SerialNumber string `json:"serialNumber"` Model string `json:"model,omitempty"` FirmwareVersion string `json:"firmwareVersion,omitempty"` SendFrequencyMs int `json:"sendFrequencyMs,omitempty"` diff --git a/internal/client/client_test.go b/internal/client/client_test.go index cdb363d..5516d05 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -70,16 +70,15 @@ func TestCreateGatewaySuccess(t *testing.T) { var req client.CreateGatewayRequest decodeBody(t, r, &req) - if req.FactoryID != "fac-1" || req.SerialNumber != "SN-001" { + if req.FactoryID != "fac-1" { t.Errorf("unexpected request body: %+v", req) } writeJSON(w, http.StatusCreated, want) }) got, err := c.CreateGateway(client.CreateGatewayRequest{ - FactoryID: "fac-1", - FactoryKey: "key-1", - SerialNumber: "SN-001", + FactoryID: "fac-1", + FactoryKey: "key-1", }) if err != nil { t.Fatalf(errFmtUnexpected, err) @@ -467,9 +466,8 @@ func TestCreateGatewayInvalidJSONResponse(t *testing.T) { }) _, err := c.CreateGateway(client.CreateGatewayRequest{ - FactoryID: "f-1", - FactoryKey: "k-1", - SerialNumber: "SN-1", + FactoryID: "f-1", + FactoryKey: "k-1", }) if err == nil { t.Fatal(errExpectedDecode) diff --git a/internal/client/request_construction_test.go b/internal/client/request_construction_test.go index bb1c69f..5e9c887 100644 --- a/internal/client/request_construction_test.go +++ b/internal/client/request_construction_test.go @@ -85,7 +85,6 @@ func TestCreateGateway_RequestConstruction(t *testing.T) { body := readBodyAsMap(t, r) assertKey(t, body, "factoryId", "fac-1") assertKey(t, body, "factoryKey", "key-secret") - assertKey(t, body, "serialNumber", "SN-001") assertKey(t, body, "model", "GW-X200") assertKey(t, body, "firmwareVersion", "2.1.0") assertKey(t, body, "sendFrequencyMs", float64(500)) @@ -96,7 +95,6 @@ func TestCreateGateway_RequestConstruction(t *testing.T) { _, err := c.CreateGateway(client.CreateGatewayRequest{ FactoryID: "fac-1", FactoryKey: "key-secret", - SerialNumber: "SN-001", Model: "GW-X200", FirmwareVersion: "2.1.0", SendFrequencyMs: 500, @@ -117,9 +115,8 @@ func TestCreateGateway_OptionalFields_OmittedWhenZero(t *testing.T) { }) _, _ = c.CreateGateway(client.CreateGatewayRequest{ - FactoryID: "f", - FactoryKey: "k", - SerialNumber: "SN", + FactoryID: "f", + FactoryKey: "k", }) } From 8965e505ace3e7599f4ced80b9f8790077543f7b Mon Sep 17 00:00:00 2001 From: Scafu Date: Sat, 4 Apr 2026 16:43:10 +0200 Subject: [PATCH 4/9] fix: bugs --- cmd/anomalies.go | 14 +++++--------- cmd/gateways.go | 19 +++++++++++++------ cmd/sensors.go | 38 +++++++++++++++----------------------- internal/client/client.go | 38 ++++++++++++++++++-------------------- 4 files changed, 51 insertions(+), 58 deletions(-) diff --git a/cmd/anomalies.go b/cmd/anomalies.go index 494903a..b01065b 100644 --- a/cmd/anomalies.go +++ b/cmd/anomalies.go @@ -3,7 +3,6 @@ package cmd import ( "fmt" "os" - "strconv" "github.com/NoTIPswe/notip-simulator-cli/internal/client" "github.com/spf13/cobra" @@ -70,14 +69,11 @@ var anomaliesNetworkDegradationCmd = &cobra.Command{ // ── outlier ─────────────────────────────────────────────────────────────────── var anomaliesOutlierCmd = &cobra.Command{ - Use: "outlier ", - Short: "Inject an outlier reading into a sensor (uses the numeric sensor ID)", + Use: "outlier ", + Short: "Inject an outlier reading into a sensor", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - sensorID, err := strconv.ParseInt(args[0], 10, 64) - if err != nil { - return fmt.Errorf("sensor-id must be a numeric ID: %w", err) - } + sensorID := args[0] var valuePtr *float64 if cmd.Flags().Changed("value") { @@ -86,13 +82,13 @@ var anomaliesOutlierCmd = &cobra.Command{ } spinner := startSpinner( - fmt.Sprintf("Injecting outlier into sensor %d...", sensorID), + fmt.Sprintf("Injecting outlier into sensor %s...", sensorID), ) if err := client.New(simulatorURL).WithContext(cmd.Context()).InjectOutlier(sensorID, valuePtr); err != nil { spinner.Fail("Failed to inject outlier") return err } - spinner.Success(fmt.Sprintf("Outlier injected into sensor %d", sensorID)) + spinner.Success(fmt.Sprintf("Outlier injected into sensor %s", sensorID)) return nil }, } diff --git a/cmd/gateways.go b/cmd/gateways.go index 4601118..4d0b263 100644 --- a/cmd/gateways.go +++ b/cmd/gateways.go @@ -45,8 +45,8 @@ var gatewaysListCmd = &cobra.Command{ } for _, gw := range gateways { tableData = append(tableData, []string{ - strconv.FormatInt(gw.ID, 10), - gw.ManagementGatewayID, + gw.ID, + gatewayUUID(gw), statusStyle(gw.Status), gw.Model, strconv.Itoa(gw.SendFrequencyMs), @@ -76,8 +76,8 @@ var gatewaysGetCmd = &cobra.Command{ return pterm.DefaultTable.WithData(pterm.TableData{ {"Field", "Value"}, - {"ID", strconv.FormatInt(gw.ID, 10)}, - {"UUID", gw.ManagementGatewayID}, + {"ID", gw.ID}, + {"UUID", gatewayUUID(*gw)}, {"Factory ID", gw.FactoryID}, {"Model", gw.Model}, {"Firmware", gw.FirmwareVersion}, @@ -235,8 +235,8 @@ func printGatewayTable(gateways []client.Gateway) { tableData := pterm.TableData{{"ID", "UUID", "Status", "Model", "Freq (ms)"}} for _, gw := range gateways { tableData = append(tableData, []string{ - strconv.FormatInt(gw.ID, 10), - gw.ManagementGatewayID, + gw.ID, + gatewayUUID(gw), statusStyle(gw.Status), gw.Model, strconv.Itoa(gw.SendFrequencyMs), @@ -245,6 +245,13 @@ func printGatewayTable(gateways []client.Gateway) { pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() //nolint:errcheck } +func gatewayUUID(gw client.Gateway) string { + if gw.ManagementGatewayID != "" { + return gw.ManagementGatewayID + } + return gw.ID +} + // ── init ────────────────────────────────────────────────────────────────────── func init() { diff --git a/cmd/sensors.go b/cmd/sensors.go index e373181..9311519 100644 --- a/cmd/sensors.go +++ b/cmd/sensors.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "strconv" "github.com/NoTIPswe/notip-simulator-cli/internal/client" "github.com/pterm/pterm" @@ -14,14 +13,10 @@ 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 - } - +func resolveGatewayID(c *client.Client, input string) (string, error) { 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 "", fmt.Errorf("gateway must be a valid gateway UUID: %w", err) } return gw.ID, nil } @@ -29,8 +24,8 @@ func resolveGatewayID(c *client.Client, input string) (int64, error) { // ── add ─────────────────────────────────────────────────────────────────────── var sensorsAddCmd = &cobra.Command{ - Use: "add ", - Short: "Add a sensor to a gateway (accepts numeric ID or UUID)", + Use: "add ", + Short: "Add a sensor to a gateway", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { c := client.New(simulatorURL).WithContext(cmd.Context()) @@ -46,7 +41,7 @@ var sensorsAddCmd = &cobra.Command{ req.Algorithm, _ = cmd.Flags().GetString("algorithm") spinner := startSpinner( - fmt.Sprintf("Adding %s sensor to gateway %d...", req.Type, gatewayID), + fmt.Sprintf("Adding %s sensor to gateway %s...", req.Type, gatewayID), ) sensor, err := c.AddSensor(gatewayID, req) if err != nil { @@ -62,8 +57,8 @@ var sensorsAddCmd = &cobra.Command{ // ── list ────────────────────────────────────────────────────────────────────── var sensorsListCmd = &cobra.Command{ - Use: "list ", - Short: "List sensors for a gateway (accepts numeric ID or UUID)", + Use: "list ", + Short: "List sensors for a gateway", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { c := client.New(simulatorURL).WithContext(cmd.Context()) @@ -73,7 +68,7 @@ var sensorsListCmd = &cobra.Command{ } spinner := startSpinner( - fmt.Sprintf("Fetching sensors for gateway %d...", gatewayID), + fmt.Sprintf("Fetching sensors for gateway %s...", gatewayID), ) sensors, err := c.ListSensors(gatewayID) if err != nil { @@ -94,21 +89,18 @@ var sensorsListCmd = &cobra.Command{ // ── delete ──────────────────────────────────────────────────────────────────── var sensorsDeleteCmd = &cobra.Command{ - Use: "delete ", - Short: "Delete a sensor by its numeric ID", + Use: "delete ", + Short: "Delete a sensor by UUID", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - sensorID, err := strconv.ParseInt(args[0], 10, 64) - if err != nil { - return fmt.Errorf("sensor-id must be a numeric ID: %w", err) - } + sensorID := args[0] - spinner := startSpinner(fmt.Sprintf("Deleting sensor %d...", sensorID)) + spinner := startSpinner(fmt.Sprintf("Deleting sensor %s...", sensorID)) if err := client.New(simulatorURL).WithContext(cmd.Context()).DeleteSensor(sensorID); err != nil { spinner.Fail("Failed to delete sensor") return err } - spinner.Success(fmt.Sprintf("Sensor %d deleted", sensorID)) + spinner.Success(fmt.Sprintf("Sensor %s deleted", sensorID)) return nil }, } @@ -122,8 +114,8 @@ func printSensorTable(sensors []client.Sensor) { tableData := pterm.TableData{{"ID", "UUID", "Type", "Min", "Max", "Algorithm"}} for _, s := range sensors { tableData = append(tableData, []string{ - strconv.FormatInt(s.ID, 10), - s.SensorID, + s.ID, + s.ID, s.Type, fmt.Sprintf("%.2f", s.MinRange), fmt.Sprintf("%.2f", s.MaxRange), diff --git a/internal/client/client.go b/internal/client/client.go index 5d27f38..7a14377 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "net/http" - "strconv" "time" ) @@ -23,11 +22,10 @@ const ( // ── Domain types ───────────────────────────────────────────────────────────── // Gateway mirrors the GatewayResponse DTO returned by the backend. -// The numeric ID is used for sensor operations; the UUID (ManagementGatewayID) -// is used for gateway lifecycle and anomaly operations. +// ID is the public gateway identifier (UUID in current API). type Gateway struct { - ID int64 `json:"id"` - ManagementGatewayID string `json:"managementGatewayId"` + ID string `json:"id"` + ManagementGatewayID string `json:"managementGatewayId,omitempty"` FactoryID string `json:"factoryId"` Model string `json:"model"` FirmwareVersion string `json:"firmwareVersion"` @@ -40,9 +38,8 @@ type Gateway struct { // Sensor mirrors the SensorResponse DTO returned by the backend. type Sensor struct { - ID int64 `json:"id"` - GatewayID int64 `json:"gatewayId"` - SensorID string `json:"sensorId"` + ID string `json:"id"` + GatewayID string `json:"gatewayId"` Type string `json:"type"` MinRange float64 `json:"minRange"` MaxRange float64 `json:"maxRange"` @@ -240,9 +237,9 @@ func (c *Client) DeleteGateway(id string) error { // ── Sensor endpoints ────────────────────────────────────────────────────────── // AddSensor calls POST /sim/gateways/{id}/sensors. -// gatewayID is the numeric int64 ID (not the UUID). -func (c *Client) AddSensor(gatewayID int64, req AddSensorRequest) (*Sensor, error) { - path := pathGateways + strconv.FormatInt(gatewayID, 10) + "/sensors" +// gatewayID is the public gateway identifier (UUID in current API). +func (c *Client) AddSensor(gatewayID string, req AddSensorRequest) (*Sensor, error) { + path := pathGateways + gatewayID + "/sensors" resp, err := c.post(path, req) if err != nil { return nil, err @@ -256,9 +253,9 @@ func (c *Client) AddSensor(gatewayID int64, req AddSensorRequest) (*Sensor, erro } // ListSensors calls GET /sim/gateways/{id}/sensors. -// gatewayID is the numeric int64 ID. -func (c *Client) ListSensors(gatewayID int64) ([]Sensor, error) { - url := c.baseURL + pathGateways + strconv.FormatInt(gatewayID, 10) + "/sensors" +// gatewayID is the public gateway identifier (UUID in current API). +func (c *Client) ListSensors(gatewayID string) ([]Sensor, error) { + url := c.baseURL + pathGateways + gatewayID + "/sensors" req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf(errBuildRequest, err) @@ -276,9 +273,9 @@ 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(c.ctx, http.MethodDelete, c.baseURL+pathSensors+strconv.FormatInt(sensorID, 10), nil) +// sensorID is the public sensor identifier (UUID in current API). +func (c *Client) DeleteSensor(sensorID string) error { + req, err := http.NewRequestWithContext(c.ctx, http.MethodDelete, c.baseURL+pathSensors+sensorID, nil) if err != nil { return fmt.Errorf(errBuildRequest, err) } @@ -320,10 +317,11 @@ func (c *Client) Disconnect(gatewayID string, durationSeconds int) error { } // InjectOutlier calls POST /sim/sensors/{sensorId}/anomaly/outlier. -// sensorID is the numeric int64 ID. value is optional (pass nil to omit). -func (c *Client) InjectOutlier(sensorID int64, value *float64) error { +// sensorID is the public sensor identifier (UUID in current API). +// value is optional (pass nil to omit). +func (c *Client) InjectOutlier(sensorID string, value *float64) error { req := OutlierRequest{Value: value} - resp, err := c.post(pathSensors+strconv.FormatInt(sensorID, 10)+"/anomaly/outlier", req) + resp, err := c.post(pathSensors+sensorID+"/anomaly/outlier", req) if err != nil { return err } From c76758e407241c00c25d648ed429177dce168f30 Mon Sep 17 00:00:00 2001 From: marcon-effe Date: Mon, 6 Apr 2026 07:07:27 +0000 Subject: [PATCH 5/9] fix: update README and tests to use UUIDs for gateways and sensors --- README.md | 34 +++-- cmd/commands_test.go | 81 +++++++----- cmd/request_mapping_test.go | 68 ++++++---- internal/client/client_test.go | 50 ++++---- internal/client/request_construction_test.go | 128 ++++++++++--------- 5 files changed, 214 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 99c3a7c..62efc6a 100644 --- a/README.md +++ b/README.md @@ -22,25 +22,43 @@ docker compose run --rm sim-cli --help # List all gateways (requires -it for styled output) docker compose run --rm -it sim-cli gateways list -# Create 5 gateways for a tenant -docker compose run --rm sim-cli gateways create --count 5 --tenant - -# Delete a gateway by ID -docker compose run --rm sim-cli gateways delete +# Create a single gateway +docker compose run --rm sim-cli gateways create \ + --factory-id FAC-001 \ + --factory-key KEY-001 \ + --model GW-X \ + --firmware 1.0.0 \ + --freq 1000 + +# Bulk create 5 gateways +docker compose run --rm sim-cli gateways bulk \ + --count 5 \ + --factory-id FAC-001 \ + --factory-key KEY-001 \ + --model GW-X \ + --firmware 1.0.0 \ + --freq 1000 + +# Delete a gateway by UUID +docker compose run --rm sim-cli gateways delete ``` ### Sensors ```bash -# Add a temperature sensor to a gateway -docker compose run --rm sim-cli sensors add --type temperature --min 20.0 --max 80.0 +# Add a temperature sensor to a gateway (numeric ID or UUID) +docker compose run --rm sim-cli sensors add \ + --type temperature \ + --min 20.0 \ + --max 80.0 \ + --algorithm uniform_random ``` ### Anomalies ```bash # Trigger a disconnect anomaly on a gateway -docker compose run --rm sim-cli anomalies disconnect +docker compose run --rm sim-cli anomalies disconnect --duration 10 ``` ## Docker Compose configuration diff --git a/cmd/commands_test.go b/cmd/commands_test.go index 9c33ab7..8d23b23 100644 --- a/cmd/commands_test.go +++ b/cmd/commands_test.go @@ -32,8 +32,10 @@ func TestMain(m *testing.M) { const ( testGatewayUUID = "uuid-1" + testGatewayPublicID = "gw-public-1" testSensorUUID = "s-uuid-1" cmdNetDegradation = "network-degradation" + pathGatewaysPrefix = "/sim/gateways/" fmtUnexpectedPath = "unexpected path: %s" fmtUnexpectedRequest = "unexpected request: %s %s" testFlagFactoryID = "--factory-id" @@ -48,8 +50,8 @@ const ( errExpected404 = "expected error on 404" fmtPipeErr = "pipe: %v" fmtWriteErr = "write: %v" - pathGatewayUUID = "/sim/gateways/" + testGatewayUUID - pathGatewaySensors = "/sim/gateways/5/sensors" + pathGatewayUUID = pathGatewaysPrefix + testGatewayUUID + pathGatewaySensors = pathGatewaysPrefix + testGatewayPublicID + "/sensors" ) // ── helpers ─────────────────────────────────────────────────────────────────── @@ -196,15 +198,15 @@ func TestSensorsAddInvalidGatewayIdentifier(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 TestSensorsDeleteNoArgs(t *testing.T) { + if err := runCmd("sensors", "delete"); err == nil { + t.Error("expected error when sensor uuid arg is missing") } } -func TestAnomaliesOutlierNonNumericID(t *testing.T) { - if err := runCmd("anomalies", "outlier", "not-a-number"); err == nil { - t.Error("expected error for non-numeric sensor ID") +func TestAnomaliesOutlierNoArgs(t *testing.T) { + if err := runCmd("anomalies", "outlier"); err == nil { + t.Error("expected error when sensor uuid arg is missing") } } @@ -212,7 +214,7 @@ func TestAnomaliesOutlierNonNumericID(t *testing.T) { func TestGatewaysListIntegration(t *testing.T) { gateways := []map[string]any{ - {"id": 1, "managementGatewayId": testGatewayUUID, "status": "online", "model": "X", "sendFrequencyMs": 1000, "tenantId": "t1"}, + {"id": testGatewayPublicID, "managementGatewayId": testGatewayUUID, "status": "online", "model": "X", "sendFrequencyMs": 1000, "tenantId": "t1"}, } newMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet || r.URL.Path != "/sim/gateways" { @@ -273,28 +275,33 @@ func TestGatewaysDeleteIntegration(t *testing.T) { func TestSensorsListIntegration(t *testing.T) { sensors := []map[string]any{ - {"id": 1, "gatewayId": 5, "sensorId": testSensorUUID, "type": "temperature", "minRange": 0, "maxRange": 100, "algorithm": "sine_wave"}, + {"id": testSensorUUID, "gatewayId": testGatewayPublicID, "type": "temperature", "minRange": 0, "maxRange": 100, "algorithm": "sine_wave"}, } newMockServer(t, func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != pathGatewaySensors { + switch r.URL.Path { + case pathGatewayUUID: + writeJSON(w, http.StatusOK, map[string]any{"id": testGatewayPublicID, "managementGatewayId": testGatewayUUID}) + case pathGatewaySensors: + writeJSON(w, http.StatusOK, sensors) + default: t.Errorf(fmtUnexpectedPath, r.URL.Path) } - writeJSON(w, http.StatusOK, sensors) }) - if err := runCmd("sensors", "list", "5"); err != nil { + if err := runCmd("sensors", "list", testGatewayUUID); err != nil { t.Fatalf("sensors list failed: %v", err) } } func TestSensorsListUUIDIntegration(t *testing.T) { + const publicID = "gw-public-uuid-test" sensors := []map[string]any{ - {"id": 1, "gatewayId": 5, "sensorId": "s-uuid-1", "type": "temperature", "minRange": 0, "maxRange": 100, "algorithm": "sine_wave"}, + {"id": "s-uuid-1", "gatewayId": publicID, "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, map[string]any{"id": publicID, "managementGatewayId": testGatewayUUID}) + case "/sim/gateways/" + publicID + "/sensors": writeJSON(w, http.StatusOK, sensors) default: t.Errorf(fmtUnexpectedPath, r.URL.Path) @@ -309,10 +316,10 @@ 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}) + writeJSON(w, http.StatusOK, map[string]any{"id": testGatewayPublicID, "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", + "id": testSensorUUID, "gatewayId": testGatewayPublicID, "type": "temperature", "minRange": 0, "maxRange": 100, "algorithm": "constant", }) default: t.Errorf(fmtUnexpectedPath, r.URL.Path) @@ -326,12 +333,12 @@ func TestSensorsAddUUIDIntegration(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" { + if r.Method != http.MethodDelete || r.URL.Path != "/sim/sensors/"+testSensorUUID { t.Errorf(fmtUnexpectedRequest, r.Method, r.URL.Path) } w.WriteHeader(http.StatusNoContent) }) - if err := runCmd("sensors", "delete", "99"); err != nil { + if err := runCmd("sensors", "delete", testSensorUUID); err != nil { t.Fatalf("sensors delete failed: %v", err) } } @@ -375,7 +382,7 @@ func TestAnomaliesNetworkDegradationIntegration(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" { + if r.URL.Path != "/sim/sensors/sensor-42/anomaly/outlier" { t.Errorf(fmtUnexpectedPath, r.URL.Path) } var body map[string]any @@ -385,7 +392,7 @@ func TestAnomaliesOutlierIntegration(t *testing.T) { } w.WriteHeader(http.StatusNoContent) }) - if err := runCmd("anomalies", "outlier", "42", "--value", "999.9"); err != nil { + if err := runCmd("anomalies", "outlier", "sensor-42", "--value", "999.9"); err != nil { t.Fatalf("anomalies outlier failed: %v", err) } } @@ -446,7 +453,7 @@ func TestGatewaysBulkServerError(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}}, + "gateways": []any{map[string]any{"id": "gw-1"}}, "errors": []any{"", "factory key mismatch"}, }) }) @@ -494,7 +501,7 @@ func TestSensorsAddServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) }) - err := runCmd("sensors", "add", "5", "--type", "temperature", "--min", "0", "--max", "100", "--algorithm", "constant") + err := runCmd("sensors", "add", testGatewayUUID, "--type", "temperature", "--min", "0", "--max", "100", "--algorithm", "constant") if err == nil { t.Error(errExpected404) } @@ -502,18 +509,32 @@ func TestSensorsAddServerError(t *testing.T) { func TestSensorsListEmptyResult(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { - writeJSON(w, http.StatusOK, []any{}) + switch r.URL.Path { + case pathGatewayUUID: + writeJSON(w, http.StatusOK, map[string]any{"id": testGatewayPublicID, "managementGatewayId": testGatewayUUID}) + case pathGatewaySensors: + writeJSON(w, http.StatusOK, []any{}) + default: + t.Errorf(fmtUnexpectedPath, r.URL.Path) + } }) - if err := runCmd("sensors", "list", "5"); err != nil { + if err := runCmd("sensors", "list", testGatewayUUID); err != nil { t.Fatalf("sensors list empty failed: %v", err) } } func TestSensorsListServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { - http.Error(w, bodyNotFound, http.StatusNotFound) + switch r.URL.Path { + case pathGatewayUUID: + writeJSON(w, http.StatusOK, map[string]any{"id": testGatewayPublicID, "managementGatewayId": testGatewayUUID}) + case pathGatewaySensors: + http.Error(w, bodyNotFound, http.StatusNotFound) + default: + t.Errorf(fmtUnexpectedPath, r.URL.Path) + } }) - if err := runCmd("sensors", "list", "5"); err == nil { + if err := runCmd("sensors", "list", testGatewayUUID); err == nil { t.Error(errExpected404) } } @@ -522,7 +543,7 @@ func TestSensorsDeleteServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) }) - if err := runCmd("sensors", "delete", "99"); err == nil { + if err := runCmd("sensors", "delete", testSensorUUID); err == nil { t.Error(errExpected404) } } @@ -549,7 +570,7 @@ func TestAnomaliesOutlierServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) }) - if err := runCmd("anomalies", "outlier", "42"); err == nil { + if err := runCmd("anomalies", "outlier", testSensorUUID); err == nil { t.Error(errExpected404) } } diff --git a/cmd/request_mapping_test.go b/cmd/request_mapping_test.go index 8cf1e71..f198bd2 100644 --- a/cmd/request_mapping_test.go +++ b/cmd/request_mapping_test.go @@ -65,7 +65,7 @@ func TestGatewaysCreateFlagToJSONMapping(t *testing.T) { checkKey(t, body, "firmwareVersion", "3.0.1") checkKey(t, body, "sendFrequencyMs", float64(250)) - writeJSON(w, http.StatusCreated, map[string]any{"id": 1}) + writeJSON(w, http.StatusCreated, map[string]any{"id": "gw-1"}) }) err := runCmd("gateways", "create", @@ -84,7 +84,7 @@ func TestGatewaysCreateFreqMapping(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { body := readBody(t, r) checkKey(t, body, "sendFrequencyMs", float64(1000)) - writeJSON(w, http.StatusCreated, map[string]any{"id": 1}) + writeJSON(w, http.StatusCreated, map[string]any{"id": "gw-1"}) }) err := runCmd("gateways", "create", @@ -153,7 +153,7 @@ func TestGatewaysGetIntegration(t *testing.T) { t.Errorf(fmtUnexpectedRequest, r.Method, r.URL.Path) } writeJSON(w, http.StatusOK, map[string]any{ - "id": 42, + "id": "gw-42", "managementGatewayId": "uuid-get-1", "status": "online", "model": "GW-X", @@ -183,25 +183,35 @@ func TestGatewaysGetNotFound(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(fmtUnexpectedRequest, r.Method, r.URL.Path) + switch r.URL.Path { + case "/sim/gateways/uuid-gw-1": + writeJSON(w, http.StatusOK, map[string]any{ + "id": "gw-public-5", + "managementGatewayId": "uuid-gw-1", + }) + case "/sim/gateways/gw-public-5/sensors": + if r.Method != http.MethodPost { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + body := readBody(t, r) + + checkKey(t, body, "type", "humidity") + checkKey(t, body, "minRange", float64(10)) + checkAbsent(t, body, "min") + checkKey(t, body, "maxRange", float64(90)) + checkAbsent(t, body, "max") + checkKey(t, body, "algorithm", "uniform_random") + + writeJSON(w, http.StatusCreated, map[string]any{ + "id": "s-uuid", "gatewayId": "gw-public-5", + "type": "humidity", "minRange": 10, "maxRange": 90, "algorithm": "uniform_random", + }) + default: + t.Errorf(fmtUnexpectedPath, r.URL.Path) } - body := readBody(t, r) - - checkKey(t, body, "type", "humidity") - checkKey(t, body, "minRange", float64(10)) - checkAbsent(t, body, "min") - checkKey(t, body, "maxRange", float64(90)) - checkAbsent(t, body, "max") - checkKey(t, body, "algorithm", "uniform_random") - - writeJSON(w, http.StatusCreated, map[string]any{ - "id": 1, "gatewayId": 5, "sensorId": "s-uuid", - "type": "humidity", "minRange": 10, "maxRange": 90, "algorithm": "uniform_random", - }) }) - err := runCmd("sensors", "add", "5", + err := runCmd("sensors", "add", "uuid-gw-1", "--type", "humidity", "--min", "10", "--max", "90", @@ -214,13 +224,23 @@ func TestSensorsAddFlagToJSONMapping(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)) - checkKey(t, body, "maxRange", float64(85)) - writeJSON(w, http.StatusCreated, map[string]any{"id": 2, "gatewayId": 3, "sensorId": "s-2"}) + switch r.URL.Path { + case "/sim/gateways/uuid-gw-2": + writeJSON(w, http.StatusOK, map[string]any{ + "id": "gw-public-3", + "managementGatewayId": "uuid-gw-2", + }) + case "/sim/gateways/gw-public-3/sensors": + body := readBody(t, r) + checkKey(t, body, "minRange", float64(-40)) + checkKey(t, body, "maxRange", float64(85)) + writeJSON(w, http.StatusCreated, map[string]any{"id": "s-2", "gatewayId": "gw-public-3"}) + default: + t.Errorf(fmtUnexpectedPath, r.URL.Path) + } }) - err := runCmd("sensors", "add", "3", + err := runCmd("sensors", "add", "uuid-gw-2", "--type", "temperature", "--min", "-40", "--max", "85", diff --git a/internal/client/client_test.go b/internal/client/client_test.go index 5516d05..15bc2a0 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -63,7 +63,7 @@ func assertPath(t *testing.T, r *http.Request, want string) { // ── Gateway ─────────────────────────────────────────────────────────────────── func TestCreateGatewaySuccess(t *testing.T) { - want := client.Gateway{ID: 1, ManagementGatewayID: gwUUID1, Status: "online"} + want := client.Gateway{ID: "gw-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") @@ -100,7 +100,7 @@ func TestCreateGatewayServerError(t *testing.T) { func TestBulkCreateGatewaysAllSuccess(t *testing.T) { want := client.BulkCreateResponse{ - Gateways: []client.Gateway{{ID: 1}, {ID: 2}}, + Gateways: []client.Gateway{{ID: "gw-1"}, {ID: "gw-2"}}, Errors: []string{"", ""}, } _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { @@ -126,7 +126,7 @@ func TestBulkCreateGatewaysAllSuccess(t *testing.T) { func TestBulkCreateGatewaysPartialErrors207(t *testing.T) { want := client.BulkCreateResponse{ - Gateways: []client.Gateway{{ID: 1}}, + Gateways: []client.Gateway{{ID: "gw-1"}}, Errors: []string{"", "factory key mismatch"}, } _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { @@ -143,7 +143,7 @@ func TestBulkCreateGatewaysPartialErrors207(t *testing.T) { } func TestListGatewaysSuccess(t *testing.T) { - want := []client.Gateway{{ID: 1, Status: "online"}, {ID: 2, Status: "offline"}} + want := []client.Gateway{{ID: "gw-1", Status: "online"}, {ID: "gw-2", Status: "offline"}} _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodGet) assertPath(t, r, "/sim/gateways") @@ -176,7 +176,7 @@ func TestListGatewaysEmpty(t *testing.T) { } func TestGetGatewaySuccess(t *testing.T) { - want := client.Gateway{ID: 42, ManagementGatewayID: "uuid-42", Status: "online"} + want := client.Gateway{ID: "gw-42", ManagementGatewayID: "uuid-42", Status: "online"} _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodGet) assertPath(t, r, "/sim/gateways/uuid-42") @@ -187,8 +187,8 @@ func TestGetGatewaySuccess(t *testing.T) { if err != nil { t.Fatalf(errFmtUnexpected, err) } - if got.ID != 42 { - t.Errorf("got ID %d, want 42", got.ID) + if got.ID != "gw-42" { + t.Errorf("got ID %s, want gw-42", got.ID) } } @@ -256,10 +256,10 @@ func TestDeleteGatewayNotFound(t *testing.T) { // ── Sensor ──────────────────────────────────────────────────────────────────── func TestAddSensorSuccess(t *testing.T) { - want := client.Sensor{ID: 10, GatewayID: 5, Type: "temperature", MinRange: 0, MaxRange: 100, Algorithm: "sine_wave"} + want := client.Sensor{ID: "sensor-10", GatewayID: "gw-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) - assertPath(t, r, "/sim/gateways/5/sensors") + assertPath(t, r, "/sim/gateways/gw-5/sensors") var req client.AddSensorRequest decodeBody(t, r, &req) @@ -269,7 +269,7 @@ func TestAddSensorSuccess(t *testing.T) { writeJSON(w, http.StatusCreated, want) }) - got, err := c.AddSensor(5, client.AddSensorRequest{ + got, err := c.AddSensor("gw-5", client.AddSensorRequest{ Type: "temperature", MinRange: 0, MaxRange: 100, @@ -278,7 +278,7 @@ func TestAddSensorSuccess(t *testing.T) { if err != nil { t.Fatalf(errFmtUnexpected, err) } - if got.ID != 10 || got.Type != "temperature" { + if got.ID != "sensor-10" || got.Type != "temperature" { t.Errorf("got %+v, want %+v", got, want) } } @@ -287,7 +287,7 @@ func TestAddSensorGatewayNotFound(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, errMsgNotFound, http.StatusNotFound) }) - _, err := c.AddSensor(999, client.AddSensorRequest{Type: "temperature", Algorithm: "constant"}) + _, err := c.AddSensor("ghost", client.AddSensorRequest{Type: "temperature", Algorithm: "constant"}) if err == nil { t.Fatal(errExpected404) } @@ -295,16 +295,16 @@ func TestAddSensorGatewayNotFound(t *testing.T) { func TestListSensorsSuccess(t *testing.T) { want := []client.Sensor{ - {ID: 1, Type: "temperature"}, - {ID: 2, Type: "humidity"}, + {ID: "sensor-1", Type: "temperature"}, + {ID: "sensor-2", Type: "humidity"}, } _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodGet) - assertPath(t, r, "/sim/gateways/7/sensors") + assertPath(t, r, "/sim/gateways/gw-7/sensors") writeJSON(w, http.StatusOK, want) }) - got, err := c.ListSensors(7) + got, err := c.ListSensors("gw-7") if err != nil { t.Fatalf(errFmtUnexpected, err) } @@ -316,10 +316,10 @@ func TestListSensorsSuccess(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") + assertPath(t, r, "/sim/sensors/sensor-99") w.WriteHeader(http.StatusNoContent) }) - if err := c.DeleteSensor(99); err != nil { + if err := c.DeleteSensor("sensor-99"); err != nil { t.Fatalf(errFmtUnexpected, err) } } @@ -328,7 +328,7 @@ func TestDeleteSensorNotFound(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, errMsgNotFound, http.StatusNotFound) }) - if err := c.DeleteSensor(0); err == nil { + if err := c.DeleteSensor("ghost"); err == nil { t.Fatal(errExpected404) } } @@ -401,7 +401,7 @@ func TestInjectOutlierWithValue(t *testing.T) { val := 999.9 _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { assertMethod(t, r, http.MethodPost) - assertPath(t, r, "/sim/sensors/42/anomaly/outlier") + assertPath(t, r, "/sim/sensors/sensor-42/anomaly/outlier") body, _ := io.ReadAll(r.Body) var req map[string]any @@ -411,7 +411,7 @@ func TestInjectOutlierWithValue(t *testing.T) { } w.WriteHeader(http.StatusNoContent) }) - if err := c.InjectOutlier(42, &val); err != nil { + if err := c.InjectOutlier("sensor-42", &val); err != nil { t.Fatalf(errFmtUnexpected, err) } } @@ -426,7 +426,7 @@ func TestInjectOutlierNoValue(t *testing.T) { } w.WriteHeader(http.StatusNoContent) }) - if err := c.InjectOutlier(42, nil); err != nil { + if err := c.InjectOutlier("sensor-42", nil); err != nil { t.Fatalf(errFmtUnexpected, err) } } @@ -435,7 +435,7 @@ func TestInjectOutlierSensorNotFound(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, errMsgNotFound, http.StatusNotFound) }) - if err := c.InjectOutlier(0, nil); err == nil { + if err := c.InjectOutlier("ghost", nil); err == nil { t.Fatal(errExpected404) } } @@ -524,7 +524,7 @@ func TestAddSensorInvalidJSONResponse(t *testing.T) { _, _ = w.Write([]byte(invalidJSONPayload)) }) - _, err := c.AddSensor(1, client.AddSensorRequest{Type: "temperature", Algorithm: "constant"}) + _, err := c.AddSensor("gw-1", client.AddSensorRequest{Type: "temperature", Algorithm: "constant"}) if err == nil { t.Fatal(errExpectedDecode) } @@ -537,7 +537,7 @@ func TestListSensorsInvalidJSONResponse(t *testing.T) { _, _ = w.Write([]byte(invalidJSONPayload)) }) - _, err := c.ListSensors(1) + _, err := c.ListSensors("gw-1") if err == nil { t.Fatal(errExpectedDecode) } diff --git a/internal/client/request_construction_test.go b/internal/client/request_construction_test.go index 5e9c887..4603246 100644 --- a/internal/client/request_construction_test.go +++ b/internal/client/request_construction_test.go @@ -1,7 +1,7 @@ // request_construction_test.go verifies that every client method builds the HTTP // request exactly as the simulator backend expects it: // - correct HTTP method -// - correct URL path (including /sim/ prefix and numeric vs UUID IDs) +// - correct URL path (including /sim/ prefix and UUID/public string IDs) // - correct Content-Type header on POST-with-body requests // - exact JSON field names in the request body // - omitempty: optional fields are absent when zero-valued @@ -17,6 +17,15 @@ import ( "github.com/NoTIPswe/notip-simulator-cli/internal/client" ) +const ( + reqHeaderContentType = "Content-Type" + reqMediaTypeJSON = "application/json" + fmtMethodWantPost = "method = %s, want POST" + fmtPathOnly = "path = %s" + errUnexpected = "unexpected error: %v" + testGatewayUUIDX = "uuid-x" +) + // ── helpers ─────────────────────────────────────────────────────────────────── func readBodyAsMap(t *testing.T, r *http.Request) map[string]any { @@ -37,9 +46,9 @@ func readBodyAsMap(t *testing.T, r *http.Request) map[string]any { func assertContentType(t *testing.T, r *http.Request) { t.Helper() - ct := r.Header.Get("Content-Type") - if ct != "application/json" { - t.Errorf("Content-Type = %q, want %q", ct, "application/json") + ct := r.Header.Get(reqHeaderContentType) + if ct != reqMediaTypeJSON { + t.Errorf("Content-Type = %q, want %q", ct, reqMediaTypeJSON) } } @@ -72,10 +81,10 @@ func assertKeyAbsent(t *testing.T, m map[string]any, key string) { // ── POST /sim/gateways — single create ─────────────────────────────────────── -func TestCreateGateway_RequestConstruction(t *testing.T) { +func TestCreateGatewayRequestConstruction(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - t.Errorf("method = %s, want POST", r.Method) + t.Errorf(fmtMethodWantPost, r.Method) } if r.URL.Path != "/sim/gateways" { t.Errorf("path = %s, want /sim/gateways", r.URL.Path) @@ -89,7 +98,7 @@ func TestCreateGateway_RequestConstruction(t *testing.T) { assertKey(t, body, "firmwareVersion", "2.1.0") assertKey(t, body, "sendFrequencyMs", float64(500)) - writeJSON(w, http.StatusCreated, client.Gateway{ID: 1}) + writeJSON(w, http.StatusCreated, client.Gateway{ID: "gw-1"}) }) _, err := c.CreateGateway(client.CreateGatewayRequest{ @@ -100,18 +109,18 @@ func TestCreateGateway_RequestConstruction(t *testing.T) { SendFrequencyMs: 500, }) if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errUnexpected, err) } } -func TestCreateGateway_OptionalFields_OmittedWhenZero(t *testing.T) { +func TestCreateGatewayOptionalFieldsOmittedWhenZero(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { body := readBodyAsMap(t, r) // model, firmwareVersion, sendFrequencyMs are omitempty — absent when zero assertKeyAbsent(t, body, "model") assertKeyAbsent(t, body, "firmwareVersion") assertKeyAbsent(t, body, "sendFrequencyMs") - writeJSON(w, http.StatusCreated, client.Gateway{ID: 1}) + writeJSON(w, http.StatusCreated, client.Gateway{ID: "gw-1"}) }) _, _ = c.CreateGateway(client.CreateGatewayRequest{ @@ -122,10 +131,10 @@ func TestCreateGateway_OptionalFields_OmittedWhenZero(t *testing.T) { // ── POST /sim/gateways/bulk ─────────────────────────────────────────────────── -func TestBulkCreateGateways_RequestConstruction(t *testing.T) { +func TestBulkCreateGatewaysRequestConstruction(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - t.Errorf("method = %s, want POST", r.Method) + t.Errorf(fmtMethodWantPost, r.Method) } if r.URL.Path != "/sim/gateways/bulk" { t.Errorf("path = %s, want /sim/gateways/bulk", r.URL.Path) @@ -141,7 +150,7 @@ func TestBulkCreateGateways_RequestConstruction(t *testing.T) { assertKey(t, body, "sendFrequencyMs", float64(2000)) writeJSON(w, http.StatusCreated, client.BulkCreateResponse{ - Gateways: []client.Gateway{{ID: 1}, {ID: 2}, {ID: 3}}, + Gateways: []client.Gateway{{ID: "gw-1"}, {ID: "gw-2"}, {ID: "gw-3"}}, Errors: []string{"", "", ""}, }) }) @@ -155,11 +164,11 @@ func TestBulkCreateGateways_RequestConstruction(t *testing.T) { SendFrequencyMs: 2000, }) if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errUnexpected, err) } } -func TestBulkCreateGateways_OptionalFields_OmittedWhenZero(t *testing.T) { +func TestBulkCreateGatewaysOptionalFieldsOmittedWhenZero(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { body := readBodyAsMap(t, r) assertKeyAbsent(t, body, "model") @@ -172,10 +181,10 @@ func TestBulkCreateGateways_OptionalFields_OmittedWhenZero(t *testing.T) { // ── GET /sim/gateways — no body ─────────────────────────────────────────────── -func TestListGateways_NoBodyNoContentType(t *testing.T) { +func TestListGatewaysNoBodyNoContentType(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Content-Type") != "" { - t.Errorf("GET should not set Content-Type, got %q", r.Header.Get("Content-Type")) + if r.Header.Get(reqHeaderContentType) != "" { + t.Errorf("GET should not set Content-Type, got %q", r.Header.Get(reqHeaderContentType)) } assertNoBody(t, r) writeJSON(w, http.StatusOK, []client.Gateway{}) @@ -185,13 +194,13 @@ func TestListGateways_NoBodyNoContentType(t *testing.T) { // ── POST /sim/gateways/{id}/start & stop — no body ─────────────────────────── -func TestStartGateway_NoBody(t *testing.T) { +func TestStartGatewayNoBody(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/sim/gateways/uuid-abc/start" { - t.Errorf("path = %s", r.URL.Path) + t.Errorf(fmtPathOnly, r.URL.Path) } // POST with no body: Content-Type should not be set - if r.Header.Get("Content-Type") != "" { + if r.Header.Get(reqHeaderContentType) != "" { t.Errorf("POST-no-body should not set Content-Type") } assertNoBody(t, r) @@ -200,12 +209,12 @@ func TestStartGateway_NoBody(t *testing.T) { _ = c.StartGateway("uuid-abc") } -func TestStopGateway_NoBody(t *testing.T) { +func TestStopGatewayNoBody(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/sim/gateways/uuid-abc/stop" { - t.Errorf("path = %s", r.URL.Path) + t.Errorf(fmtPathOnly, r.URL.Path) } - if r.Header.Get("Content-Type") != "" { + if r.Header.Get(reqHeaderContentType) != "" { t.Errorf("POST-no-body should not set Content-Type") } assertNoBody(t, r) @@ -216,13 +225,13 @@ func TestStopGateway_NoBody(t *testing.T) { // ── DELETE /sim/gateways/{id} — no body ────────────────────────────────────── -func TestDeleteGateway_MethodAndNoBody(t *testing.T) { +func TestDeleteGatewayMethodAndNoBody(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { t.Errorf("method = %s, want DELETE", r.Method) } if r.URL.Path != "/sim/gateways/uuid-del" { - t.Errorf("path = %s", r.URL.Path) + t.Errorf(fmtPathOnly, r.URL.Path) } assertNoBody(t, r) w.WriteHeader(http.StatusNoContent) @@ -235,13 +244,13 @@ func TestDeleteGateway_MethodAndNoBody(t *testing.T) { // CRITICAL: CLI flags --min/--max must map to "minRange"/"maxRange" in JSON, // not "min"/"max". This is the field name the backend expects. -func TestAddSensor_RequestFieldNames(t *testing.T) { +func TestAddSensorRequestFieldNames(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - t.Errorf("method = %s, want POST", r.Method) + t.Errorf(fmtMethodWantPost, r.Method) } - if r.URL.Path != "/sim/gateways/7/sensors" { - t.Errorf("path = %s, want /sim/gateways/7/sensors", r.URL.Path) + if r.URL.Path != "/sim/gateways/gw-public-7/sensors" { + t.Errorf("path = %s, want /sim/gateways/gw-public-7/sensors", r.URL.Path) } assertContentType(t, r) @@ -257,55 +266,54 @@ func TestAddSensor_RequestFieldNames(t *testing.T) { // Field name must be "type" assertKey(t, body, "type", "temperature") - writeJSON(w, http.StatusCreated, client.Sensor{ID: 1}) + writeJSON(w, http.StatusCreated, client.Sensor{ID: "sensor-1"}) }) - _, err := c.AddSensor(7, client.AddSensorRequest{ + _, err := c.AddSensor("gw-public-7", client.AddSensorRequest{ Type: "temperature", MinRange: -10, MaxRange: 120, Algorithm: "sine_wave", }) if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf(errUnexpected, err) } } -// ── GET /sim/gateways/{id}/sensors — numeric ID in path ────────────────────── +// ── GET /sim/gateways/{id}/sensors — gateway ID in path ────────────────────── -func TestListSensors_NumericIDInPath(t *testing.T) { +func TestListSensorsGatewayIDInPath(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - // ID must be the numeric int64, not a UUID - if r.URL.Path != "/sim/gateways/1234567890/sensors" { - t.Errorf("path = %s, want /sim/gateways/1234567890/sensors", r.URL.Path) + if r.URL.Path != "/sim/gateways/gw-public-123/sensors" { + t.Errorf("path = %s, want /sim/gateways/gw-public-123/sensors", r.URL.Path) } writeJSON(w, http.StatusOK, []client.Sensor{}) }) - _, _ = c.ListSensors(1234567890) + _, _ = c.ListSensors("gw-public-123") } -// ── DELETE /sim/sensors/{sensorId} — numeric ID in path ────────────────────── +// ── DELETE /sim/sensors/{sensorId} — sensor ID in path ─────────────────────── -func TestDeleteSensor_NumericIDInPath(t *testing.T) { +func TestDeleteSensorSensorIDInPath(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { t.Errorf("method = %s, want DELETE", r.Method) } - if r.URL.Path != "/sim/sensors/42" { - t.Errorf("path = %s, want /sim/sensors/42", r.URL.Path) + if r.URL.Path != "/sim/sensors/sensor-42" { + t.Errorf("path = %s, want /sim/sensors/sensor-42", r.URL.Path) } assertNoBody(t, r) w.WriteHeader(http.StatusNoContent) }) - _ = c.DeleteSensor(42) + _ = c.DeleteSensor("sensor-42") } // ── POST /sim/gateways/{id}/anomaly/disconnect — duration_seconds field ─────── -func TestDisconnect_RequestFieldNames(t *testing.T) { +func TestDisconnectRequestFieldNames(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/sim/gateways/uuid-x/anomaly/disconnect" { - t.Errorf("path = %s", r.URL.Path) + if r.URL.Path != "/sim/gateways/"+testGatewayUUIDX+"/anomaly/disconnect" { + t.Errorf(fmtPathOnly, r.URL.Path) } assertContentType(t, r) @@ -317,15 +325,15 @@ func TestDisconnect_RequestFieldNames(t *testing.T) { w.WriteHeader(http.StatusNoContent) }) - _ = c.Disconnect("uuid-x", 7) + _ = c.Disconnect(testGatewayUUIDX, 7) } // ── POST /sim/gateways/{id}/anomaly/network-degradation ────────────────────── -func TestNetworkDegradation_RequestFieldNames(t *testing.T) { +func TestNetworkDegradationRequestFieldNames(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/sim/gateways/uuid-x/anomaly/network-degradation" { - t.Errorf("path = %s", r.URL.Path) + if r.URL.Path != "/sim/gateways/"+testGatewayUUIDX+"/anomaly/network-degradation" { + t.Errorf(fmtPathOnly, r.URL.Path) } assertContentType(t, r) @@ -338,26 +346,26 @@ func TestNetworkDegradation_RequestFieldNames(t *testing.T) { w.WriteHeader(http.StatusNoContent) }) - _ = c.InjectNetworkDegradation("uuid-x", 15, 0.25) + _ = c.InjectNetworkDegradation(testGatewayUUIDX, 15, 0.25) } -func TestNetworkDegradation_PacketLossOmitted_WhenZero(t *testing.T) { +func TestNetworkDegradationPacketLossOmittedWhenZero(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { body := readBodyAsMap(t, r) // 0.0 with omitempty → field must be absent so backend applies its default (0.3) assertKeyAbsent(t, body, "packet_loss_pct") w.WriteHeader(http.StatusNoContent) }) - _ = c.InjectNetworkDegradation("uuid-x", 5, 0) + _ = c.InjectNetworkDegradation(testGatewayUUIDX, 5, 0) } // ── POST /sim/sensors/{sensorId}/anomaly/outlier ────────────────────────────── -func TestOutlier_RequestFieldNames_WithValue(t *testing.T) { +func TestOutlierRequestFieldNamesWithValue(t *testing.T) { val := 42.5 _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/sim/sensors/99/anomaly/outlier" { - t.Errorf("path = %s, want /sim/sensors/99/anomaly/outlier", r.URL.Path) + if r.URL.Path != "/sim/sensors/sensor-99/anomaly/outlier" { + t.Errorf("path = %s, want /sim/sensors/sensor-99/anomaly/outlier", r.URL.Path) } assertContentType(t, r) @@ -366,17 +374,17 @@ func TestOutlier_RequestFieldNames_WithValue(t *testing.T) { w.WriteHeader(http.StatusNoContent) }) - _ = c.InjectOutlier(99, &val) + _ = c.InjectOutlier("sensor-99", &val) } -func TestOutlier_ValueOmitted_WhenNil(t *testing.T) { +func TestOutlierValueOmittedWhenNil(t *testing.T) { _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { body := readBodyAsMap(t, r) // nil pointer + omitempty → "value" key must be absent assertKeyAbsent(t, body, "value") w.WriteHeader(http.StatusNoContent) }) - _ = c.InjectOutlier(99, nil) + _ = c.InjectOutlier("sensor-99", nil) } // ── Default SIMULATOR_URL matches docker-compose service name ───────────────── From b9b733d6ea1a0b78999925059ed8bce694493559 Mon Sep 17 00:00:00 2001 From: marcon-effe Date: Mon, 6 Apr 2026 07:18:18 +0000 Subject: [PATCH 6/9] fix: update AddSensorRequest documentation to clarify gateway ID as UUID --- internal/client/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/client/client.go b/internal/client/client.go index 7a14377..94d5c87 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -76,7 +76,7 @@ type BulkCreateResponse struct { } // AddSensorRequest is the payload for POST /sim/gateways/{id}/sensors. -// The gateway ID in the path is the numeric int64 ID, not the UUID. +// The gateway ID in the path is the public gateway identifier (UUID string). type AddSensorRequest struct { Type string `json:"type"` MinRange float64 `json:"minRange"` From a37684d6e756e063e3770e0effd4429b418c1918 Mon Sep 17 00:00:00 2001 From: marcon-effe Date: Tue, 7 Apr 2026 10:27:26 +0000 Subject: [PATCH 7/9] fix: enhance shell command to support line editor and improve command processing --- cmd/shell.go | 99 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/cmd/shell.go b/cmd/shell.go index 091d430..f326f41 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -2,6 +2,7 @@ package cmd import ( "bufio" + "errors" "fmt" "io" "os" @@ -10,6 +11,7 @@ import ( "github.com/pterm/pterm" "github.com/pterm/pterm/putils" "github.com/spf13/cobra" + "golang.org/x/term" ) var shellCmd = &cobra.Command{ @@ -25,6 +27,14 @@ restarting the container. Type 'help' for available commands, 'exit' to quit.`, rootCmd.SilenceUsage = true defer func() { rootCmd.SilenceUsage = false }() + if canUseLineEditor() { + err := runShellWithLineEditor() + if err == nil { + return nil + } + pterm.Warning.Printf("line editor disabled: %v\n", err) + } + reader := bufio.NewReader(os.Stdin) for { printPrompt() @@ -38,29 +48,84 @@ restarting the container. Type 'help' for available commands, 'exit' to quit.`, return nil } - line = strings.TrimSpace(line) - if line == "" { - continue - } - if line == "exit" || line == "quit" { - pterm.Info.Println("Goodbye!") + if shouldExit := processShellLine(line); shouldExit { return nil } + } + }, +} - // Prevent the user from nesting shells. - parts := strings.Fields(line) - if parts[0] == "shell" { - pterm.Warning.Println("Already inside a shell session.") - continue - } +func canUseLineEditor() bool { + return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) +} + +func runShellWithLineEditor() error { + fd := int(os.Stdin.Fd()) + oldState, err := term.MakeRaw(fd) + if err != nil { + return err + } + defer func() { + _ = term.Restore(fd, oldState) + }() + + lineEditor := term.NewTerminal(struct { + io.Reader + io.Writer + }{ + Reader: os.Stdin, + Writer: os.Stdout, + }, "sim-cli> ") - resetAllCommandFlags(rootCmd) - rootCmd.SetArgs(parts) - if execErr := rootCmd.Execute(); execErr != nil { - pterm.Error.Println(execErr) + for { + line, readErr := lineEditor.ReadLine() + if readErr != nil { + if errors.Is(readErr, io.EOF) { + fmt.Println() + pterm.Info.Println("Goodbye!") + return nil } + return readErr } - }, + + // Leave raw mode while the command runs so output renders correctly. + if err := term.Restore(fd, oldState); err != nil { + return err + } + shouldExit := processShellLine(line) + if _, err := term.MakeRaw(fd); err != nil { + return err + } + if shouldExit { + return nil + } + } +} + +func processShellLine(line string) bool { + line = strings.TrimSpace(line) + if line == "" { + return false + } + if line == "exit" || line == "quit" { + pterm.Info.Println("Goodbye!") + return true + } + + // Prevent the user from nesting shells. + args := strings.Fields(line) + if args[0] == "shell" { + pterm.Warning.Println("Already inside a shell session.") + return false + } + + resetAllCommandFlags(rootCmd) + rootCmd.SetArgs(args) + if err := rootCmd.Execute(); err != nil { + pterm.Error.Println(err) + } + + return false } func printPrompt() { From 3fcb4bb0f1975d895c2b4074066923547f122d58 Mon Sep 17 00:00:00 2001 From: marcon-effe Date: Thu, 9 Apr 2026 15:21:56 +0000 Subject: [PATCH 8/9] fix: add tests for shell command line editor and context handling in client --- cmd/commands_test.go | 72 +++++++++++ cmd/shell.go | 63 +++++++--- cmd/shell_test.go | 216 +++++++++++++++++++++++++++++++++ internal/client/client_test.go | 58 +++++++++ 4 files changed, 389 insertions(+), 20 deletions(-) diff --git a/cmd/commands_test.go b/cmd/commands_test.go index 8d23b23..8890b83 100644 --- a/cmd/commands_test.go +++ b/cmd/commands_test.go @@ -507,6 +507,29 @@ func TestSensorsAddServerError(t *testing.T) { } } +func TestSensorsAddFailsAfterGatewayLookup(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": testGatewayPublicID, "managementGatewayId": testGatewayUUID}) + case pathGatewaySensors: + http.Error(w, bodyNotFound, http.StatusNotFound) + default: + t.Errorf(fmtUnexpectedPath, r.URL.Path) + } + }) + + err := runCmd("sensors", "add", testGatewayUUID, + testFlagType, "temperature", + "--min", "0", + "--max", "100", + testFlagAlgorithm, "constant", + ) + if err == nil { + t.Error(errExpected404) + } +} + func TestSensorsListEmptyResult(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { @@ -539,6 +562,16 @@ func TestSensorsListServerError(t *testing.T) { } } +func TestSensorsListGatewayLookupError(t *testing.T) { + newMockServer(t, func(w http.ResponseWriter, r *http.Request) { + http.Error(w, bodyNotFound, http.StatusNotFound) + }) + + if err := runCmd("sensors", "list", testGatewayUUID); err == nil { + t.Error(errExpected404) + } +} + func TestSensorsDeleteServerError(t *testing.T) { newMockServer(t, func(w http.ResponseWriter, r *http.Request) { http.Error(w, bodyNotFound, http.StatusNotFound) @@ -753,3 +786,42 @@ func TestPrintSensorTableEmptySliceNoOutput(t *testing.T) { t.Fatalf("expected no output for empty sensor table, got %q", out) } } + +func TestPrintGatewayTableEmptySliceNoOutput(t *testing.T) { + out := captureStdout(t, func() { + printGatewayTable([]client.Gateway{}) + }) + + if out != "" { + t.Fatalf("expected no output for empty gateway table, got %q", out) + } +} + +func TestGatewayUUIDResolution(t *testing.T) { + if got := gatewayUUID(client.Gateway{ID: "gw-1", ManagementGatewayID: testGatewayUUID}); got != testGatewayUUID { + t.Fatalf("gatewayUUID with management id = %q, want %q", got, testGatewayUUID) + } + if got := gatewayUUID(client.Gateway{ID: "gw-2"}); got != "gw-2" { + t.Fatalf("gatewayUUID fallback = %q, want %q", got, "gw-2") + } +} + +func TestAnomaliesOutlierNoValueIntegration(t *testing.T) { + newMockServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/sim/sensors/sensor-100/anomaly/outlier" { + t.Errorf(fmtUnexpectedPath, r.URL.Path) + } + + var body map[string]any + _ = json.NewDecoder(r.Body).Decode(&body) + if _, ok := body["value"]; ok { + t.Errorf("value should be omitted when --value is not provided") + } + + w.WriteHeader(http.StatusNoContent) + }) + + if err := runCmd("anomalies", "outlier", "sensor-100"); err != nil { + t.Fatalf("anomalies outlier without value failed: %v", err) + } +} diff --git a/cmd/shell.go b/cmd/shell.go index f326f41..8813c7c 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -14,6 +14,29 @@ import ( "golang.org/x/term" ) +type shellLineEditor interface { + ReadLine() (string, error) +} + +const goodbyeMessage = "Goodbye!" + +var ( + shellStdin = func() *os.File { return os.Stdin } + shellStdout = func() *os.File { return os.Stdout } + shellIsTerminal = term.IsTerminal + shellMakeRaw = term.MakeRaw + shellRestore = term.Restore + shellNewEditor = func(rw io.ReadWriter, prompt string) shellLineEditor { + return term.NewTerminal(rw, prompt) + } + renderWelcomeBigText = func() error { + return pterm.DefaultBigText.WithLetters( + putils.LettersFromStringWithStyle("sim", pterm.NewStyle(pterm.FgGreen)), + putils.LettersFromStringWithStyle("-cli", pterm.NewStyle(pterm.FgGray)), + ).Render() + } +) + var shellCmd = &cobra.Command{ Use: "shell", Short: "Start an interactive shell session", @@ -28,14 +51,14 @@ restarting the container. Type 'help' for available commands, 'exit' to quit.`, defer func() { rootCmd.SilenceUsage = false }() if canUseLineEditor() { - err := runShellWithLineEditor() - if err == nil { + if err := runShellWithLineEditor(); err != nil { + pterm.Warning.Printf("line editor disabled: %v\n", err) + } else { return nil } - pterm.Warning.Printf("line editor disabled: %v\n", err) } - reader := bufio.NewReader(os.Stdin) + reader := bufio.NewReader(shellStdin()) for { printPrompt() @@ -43,7 +66,7 @@ restarting the container. Type 'help' for available commands, 'exit' to quit.`, if err != nil { if err == io.EOF { fmt.Println() - pterm.Info.Println("Goodbye!") + pterm.Info.Println(goodbyeMessage) } return nil } @@ -56,25 +79,28 @@ restarting the container. Type 'help' for available commands, 'exit' to quit.`, } func canUseLineEditor() bool { - return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) + return shellIsTerminal(int(shellStdin().Fd())) && shellIsTerminal(int(shellStdout().Fd())) } func runShellWithLineEditor() error { - fd := int(os.Stdin.Fd()) - oldState, err := term.MakeRaw(fd) + stdin := shellStdin() + stdout := shellStdout() + + fd := int(stdin.Fd()) + oldState, err := shellMakeRaw(fd) if err != nil { return err } defer func() { - _ = term.Restore(fd, oldState) + _ = shellRestore(fd, oldState) }() - lineEditor := term.NewTerminal(struct { + lineEditor := shellNewEditor(struct { io.Reader io.Writer }{ - Reader: os.Stdin, - Writer: os.Stdout, + Reader: stdin, + Writer: stdout, }, "sim-cli> ") for { @@ -82,18 +108,18 @@ func runShellWithLineEditor() error { if readErr != nil { if errors.Is(readErr, io.EOF) { fmt.Println() - pterm.Info.Println("Goodbye!") + pterm.Info.Println(goodbyeMessage) return nil } return readErr } // Leave raw mode while the command runs so output renders correctly. - if err := term.Restore(fd, oldState); err != nil { + if err := shellRestore(fd, oldState); err != nil { return err } shouldExit := processShellLine(line) - if _, err := term.MakeRaw(fd); err != nil { + if _, err := shellMakeRaw(fd); err != nil { return err } if shouldExit { @@ -108,7 +134,7 @@ func processShellLine(line string) bool { return false } if line == "exit" || line == "quit" { - pterm.Info.Println("Goodbye!") + pterm.Info.Println(goodbyeMessage) return true } @@ -138,10 +164,7 @@ func printPrompt() { func printWelcomeBanner() { if !pterm.RawOutput { - if err := pterm.DefaultBigText.WithLetters( - putils.LettersFromStringWithStyle("sim", pterm.NewStyle(pterm.FgGreen)), - putils.LettersFromStringWithStyle("-cli", pterm.NewStyle(pterm.FgGray)), - ).Render(); err != nil { + if err := renderWelcomeBigText(); err != nil { pterm.Warning.Printf("failed to render banner: %v\n", err) } } diff --git a/cmd/shell_test.go b/cmd/shell_test.go index 646cad4..b49c0b1 100644 --- a/cmd/shell_test.go +++ b/cmd/shell_test.go @@ -1,12 +1,72 @@ package cmd import ( + "bytes" + "errors" + "io" + "os" "strings" "testing" "github.com/pterm/pterm" + "golang.org/x/term" ) +type shellReadEvent struct { + line string + err error +} + +type scriptedShellEditor struct { + events []shellReadEvent + index int +} + +func (s *scriptedShellEditor) ReadLine() (string, error) { + if s.index >= len(s.events) { + return "", io.EOF + } + event := s.events[s.index] + s.index++ + return event.line, event.err +} + +func setShellHooksForTest(t *testing.T) { + t.Helper() + + prevStdin := shellStdin + prevStdout := shellStdout + prevIsTerminal := shellIsTerminal + prevMakeRaw := shellMakeRaw + prevRestore := shellRestore + prevNewEditor := shellNewEditor + prevRenderBanner := renderWelcomeBigText + + t.Cleanup(func() { + shellStdin = prevStdin + shellStdout = prevStdout + shellIsTerminal = prevIsTerminal + shellMakeRaw = prevMakeRaw + shellRestore = prevRestore + shellNewEditor = prevNewEditor + renderWelcomeBigText = prevRenderBanner + }) +} + +func makePipePair(t *testing.T) (*os.File, *os.File) { + t.Helper() + + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("pipe: %v", err) + } + t.Cleanup(func() { + _ = r.Close() + _ = w.Close() + }) + return r, w +} + func TestPrintPromptRawOutput(t *testing.T) { prevRaw := pterm.RawOutput pterm.RawOutput = true @@ -52,3 +112,159 @@ func TestPrintWelcomeBannerNonRawOutput(t *testing.T) { _ = captureStdout(t, printWelcomeBanner) } + +func TestPrintWelcomeBannerRenderError(t *testing.T) { + setShellHooksForTest(t) + + prevRaw := pterm.RawOutput + pterm.RawOutput = false + t.Cleanup(func() { + pterm.RawOutput = prevRaw + }) + + renderWelcomeBigText = func() error { + return errors.New("render failed") + } + + _ = captureStdout(t, printWelcomeBanner) +} + +func TestRunShellWithLineEditorEOF(t *testing.T) { + setShellHooksForTest(t) + + inR, _ := makePipePair(t) + _, outW := makePipePair(t) + shellStdin = func() *os.File { return inR } + shellStdout = func() *os.File { return outW } + shellMakeRaw = func(int) (*term.State, error) { return &term.State{}, nil } + shellRestore = func(int, *term.State) error { return nil } + shellNewEditor = func(io.ReadWriter, string) shellLineEditor { + return &scriptedShellEditor{events: []shellReadEvent{{err: io.EOF}}} + } + + if err := runShellWithLineEditor(); err != nil { + t.Fatalf("runShellWithLineEditor() error = %v, want nil", err) + } +} + +func TestRunShellWithLineEditorReadError(t *testing.T) { + setShellHooksForTest(t) + + inR, _ := makePipePair(t) + _, outW := makePipePair(t) + shellStdin = func() *os.File { return inR } + shellStdout = func() *os.File { return outW } + shellMakeRaw = func(int) (*term.State, error) { return &term.State{}, nil } + shellRestore = func(int, *term.State) error { return nil } + shellNewEditor = func(io.ReadWriter, string) shellLineEditor { + return &scriptedShellEditor{events: []shellReadEvent{{err: errors.New("read failed")}}} + } + + if err := runShellWithLineEditor(); err == nil { + t.Fatal("expected read error, got nil") + } +} + +func TestRunShellWithLineEditorRestoreError(t *testing.T) { + setShellHooksForTest(t) + + inR, _ := makePipePair(t) + _, outW := makePipePair(t) + shellStdin = func() *os.File { return inR } + shellStdout = func() *os.File { return outW } + shellMakeRaw = func(int) (*term.State, error) { return &term.State{}, nil } + restoreCalls := 0 + shellRestore = func(int, *term.State) error { + restoreCalls++ + if restoreCalls == 1 { + return errors.New("restore failed") + } + return nil + } + shellNewEditor = func(io.ReadWriter, string) shellLineEditor { + return &scriptedShellEditor{events: []shellReadEvent{{line: ""}}} + } + + if err := runShellWithLineEditor(); err == nil { + t.Fatal("expected restore error, got nil") + } +} + +func TestRunShellWithLineEditorMakeRawAfterCommandError(t *testing.T) { + setShellHooksForTest(t) + + inR, _ := makePipePair(t) + _, outW := makePipePair(t) + shellStdin = func() *os.File { return inR } + shellStdout = func() *os.File { return outW } + makeRawCalls := 0 + shellMakeRaw = func(int) (*term.State, error) { + makeRawCalls++ + if makeRawCalls == 2 { + return nil, errors.New("make raw failed") + } + return &term.State{}, nil + } + shellRestore = func(int, *term.State) error { return nil } + shellNewEditor = func(io.ReadWriter, string) shellLineEditor { + return &scriptedShellEditor{events: []shellReadEvent{{line: ""}}} + } + + if err := runShellWithLineEditor(); err == nil { + t.Fatal("expected make-raw error, got nil") + } +} + +func TestShellCommandUsesLineEditor(t *testing.T) { + setShellHooksForTest(t) + + inR, _ := makePipePair(t) + _, outW := makePipePair(t) + shellStdin = func() *os.File { return inR } + shellStdout = func() *os.File { return outW } + shellIsTerminal = func(int) bool { return true } + shellMakeRaw = func(int) (*term.State, error) { return &term.State{}, nil } + shellRestore = func(int, *term.State) error { return nil } + shellNewEditor = func(io.ReadWriter, string) shellLineEditor { + return &scriptedShellEditor{events: []shellReadEvent{{line: "exit"}}} + } + + if err := runCmd("shell"); err != nil { + t.Fatalf("shell command failed with line editor: %v", err) + } +} + +func TestShellCommandFallsBackWhenLineEditorFails(t *testing.T) { + setShellHooksForTest(t) + + inR, inW := makePipePair(t) + _, outW := makePipePair(t) + shellStdin = func() *os.File { return inR } + shellStdout = func() *os.File { return outW } + shellIsTerminal = func(int) bool { return true } + shellMakeRaw = func(int) (*term.State, error) { return nil, errors.New("raw mode unavailable") } + + if _, err := inW.WriteString("exit\n"); err != nil { + t.Fatalf("write fallback input: %v", err) + } + if err := inW.Close(); err != nil { + t.Fatalf("close fallback input writer: %v", err) + } + + if err := runCmd("shell"); err != nil { + t.Fatalf("shell command failed during fallback path: %v", err) + } +} + +func TestShellDefaultHooksCoverage(t *testing.T) { + setShellHooksForTest(t) + + if shellStdout() == nil { + t.Fatal("shellStdout should return a file") + } + + var rw bytes.Buffer + if editor := shellNewEditor(&rw, "sim-cli> "); editor == nil { + t.Fatal("shellNewEditor should return a line editor") + } +} diff --git a/internal/client/client_test.go b/internal/client/client_test.go index 15bc2a0..a708bd2 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -1,8 +1,10 @@ package client_test import ( + "context" "encoding/json" "io" + "math" "net/http" "net/http/httptest" "strings" @@ -542,3 +544,59 @@ func TestListSensorsInvalidJSONResponse(t *testing.T) { t.Fatal(errExpectedDecode) } } + +func TestWithContextNilFallback(t *testing.T) { + _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, []client.Gateway{}) + }) + + cc := c.WithContext(nil) //nolint:staticcheck // Intentional nil input to validate WithContext fallback behavior. + if cc == c { + t.Fatal("WithContext should return a shallow copy") + } + + if _, err := cc.ListGateways(); err != nil { + t.Fatalf(errFmtUnexpected, err) + } +} + +func TestWithContextUsesProvidedContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, c := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatal("request should not be sent when context is already canceled") + }) + + err := c.WithContext(ctx).StartGateway("gw-1") + if err == nil { + t.Fatal("expected context cancellation error, got nil") + } +} + +func TestAddSensorPayloadEncodeError(t *testing.T) { + c := client.New("http://example.invalid") + _, err := c.AddSensor("gw-1", client.AddSensorRequest{ + Type: "temperature", + MinRange: math.NaN(), + MaxRange: 100, + Algorithm: "constant", + }) + if err == nil { + t.Fatal("expected payload encode error, got nil") + } + if !strings.Contains(err.Error(), "failed to encode payload") { + t.Fatalf(errFmtUnexpected, err) + } +} + +func TestListGatewaysInvalidBaseURL(t *testing.T) { + c := client.New("://bad-url") + _, err := c.ListGateways() + if err == nil { + t.Fatal("expected build request error, got nil") + } + if !strings.Contains(err.Error(), "failed to build request") { + t.Fatalf(errFmtUnexpected, err) + } +} From d7a213e6a8ff4eccd13e56e521465de0f88e4bee Mon Sep 17 00:00:00 2001 From: marcon-effe Date: Thu, 9 Apr 2026 15:28:54 +0000 Subject: [PATCH 9/9] fix: simplify shell command line processing and improve test coverage --- cmd/shell.go | 2 +- cmd/shell_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/shell.go b/cmd/shell.go index 8813c7c..ffe776a 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -71,7 +71,7 @@ restarting the container. Type 'help' for available commands, 'exit' to quit.`, return nil } - if shouldExit := processShellLine(line); shouldExit { + if processShellLine(line) { return nil } } diff --git a/cmd/shell_test.go b/cmd/shell_test.go index b49c0b1..1210710 100644 --- a/cmd/shell_test.go +++ b/cmd/shell_test.go @@ -264,7 +264,7 @@ func TestShellDefaultHooksCoverage(t *testing.T) { } var rw bytes.Buffer - if editor := shellNewEditor(&rw, "sim-cli> "); editor == nil { + if shellNewEditor(&rw, "sim-cli> ") == nil { t.Fatal("shellNewEditor should return a line editor") } }