From f6261588847e52704fbd5529d12ce87dc230274c Mon Sep 17 00:00:00 2001 From: pr3o <158464877+pr3o@users.noreply.github.com> Date: Fri, 10 Apr 2026 00:50:42 +0000 Subject: [PATCH] feat: bulk creation --- README.md | 5 ++-- cmd/commands_test.go | 19 +++++++------ cmd/gateways.go | 28 +++++++++++++++----- cmd/request_mapping_test.go | 20 +++++++++++--- cmd/root.go | 6 ++++- internal/client/client.go | 11 ++++---- internal/client/client_test.go | 11 ++++---- internal/client/request_construction_test.go | 21 +++++++++++---- 8 files changed, 83 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 62efc6a..f6bb493 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,11 @@ docker compose run --rm sim-cli gateways create \ --firmware 1.0.0 \ --freq 1000 -# Bulk create 5 gateways +# Bulk create gateways with one shared config and multiple factory IDs docker compose run --rm sim-cli gateways bulk \ - --count 5 \ --factory-id FAC-001 \ + --factory-id FAC-002 \ + --factory-id FAC-003 \ --factory-key KEY-001 \ --model GW-X \ --firmware 1.0.0 \ diff --git a/cmd/commands_test.go b/cmd/commands_test.go index 8890b83..5f4f06e 100644 --- a/cmd/commands_test.go +++ b/cmd/commands_test.go @@ -62,7 +62,11 @@ const ( // process, which would otherwise cause test pollution when the full suite runs. func resetAllFlags(c *cobra.Command) { c.Flags().VisitAll(func(f *pflag.Flag) { - _ = f.Value.Set(f.DefValue) + if slice, ok := f.Value.(interface{ Replace([]string) error }); ok { + _ = slice.Replace(nil) + } else { + _ = f.Value.Set(f.DefValue) + } f.Changed = false }) for _, child := range c.Commands() { @@ -141,15 +145,14 @@ func TestGatewaysCreateMissingRequiredFlags(t *testing.T) { } } -func TestGatewaysBulkMissingCount(t *testing.T) { +func TestGatewaysBulkMissingFactoryID(t *testing.T) { 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") + t.Error("expected error when --factory-id is missing") } } @@ -438,8 +441,8 @@ func TestGatewaysBulkServerError(t *testing.T) { http.Error(w, "server error", http.StatusInternalServerError) }) err := runCmd("gateways", "bulk", - "--count", "2", - testFlagFactoryID, "f", + testFlagFactoryID, "f-1", + testFlagFactoryID, "f-2", testFlagFactoryKey, "k", testFlagModel, "GW-X", testFlagFirmware, "1.0.0", @@ -458,8 +461,8 @@ func TestGatewaysBulkPartialErrors(t *testing.T) { }) }) err := runCmd("gateways", "bulk", - "--count", "2", - testFlagFactoryID, "f", + testFlagFactoryID, "f-1", + testFlagFactoryID, "f-2", testFlagFactoryKey, "k", testFlagModel, "GW-X", testFlagFirmware, "1.0.0", diff --git a/cmd/gateways.go b/cmd/gateways.go index 4d0b263..468d2ef 100644 --- a/cmd/gateways.go +++ b/cmd/gateways.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "strconv" + "strings" "github.com/NoTIPswe/notip-simulator-cli/internal/client" "github.com/pterm/pterm" @@ -123,15 +124,20 @@ var gatewaysBulkCmd = &cobra.Command{ Short: "Create multiple gateways at once (POST /sim/gateways/bulk)", RunE: func(cmd *cobra.Command, args []string) error { req := client.BulkCreateGatewaysRequest{} - req.Count, _ = cmd.Flags().GetInt("count") - req.FactoryID, _ = cmd.Flags().GetString(flagFactoryID) + factoryIDs, _ := cmd.Flags().GetStringSlice(flagFactoryID) + factoryIDs = normalizeFactoryIDs(factoryIDs) + if len(factoryIDs) == 0 { + return fmt.Errorf("at least one --factory-id is required") + } + req.FactoryIDs = factoryIDs + req.FactoryKey, _ = cmd.Flags().GetString(flagFactoryKey) req.Model, _ = cmd.Flags().GetString("model") req.FirmwareVersion, _ = cmd.Flags().GetString("firmware") req.SendFrequencyMs, _ = cmd.Flags().GetInt("freq") spinner := startSpinner( - fmt.Sprintf("Creating %d gateway(s)...", req.Count), + fmt.Sprintf("Creating %d gateway(s)...", len(req.FactoryIDs)), ) c := client.New(simulatorURL).WithContext(cmd.Context()) result, err := c.BulkCreateGateways(req) @@ -252,6 +258,17 @@ func gatewayUUID(gw client.Gateway) string { return gw.ID } +func normalizeFactoryIDs(factoryIDs []string) []string { + out := make([]string, 0, len(factoryIDs)) + for _, factoryID := range factoryIDs { + trimmed := strings.TrimSpace(factoryID) + if trimmed != "" { + out = append(out, trimmed) + } + } + return out +} + // ── init ────────────────────────────────────────────────────────────────────── func init() { @@ -277,13 +294,12 @@ func init() { } // bulk flags - gatewaysBulkCmd.Flags().Int("count", 1, "Number of gateways to create (required)") - gatewaysBulkCmd.Flags().String(flagFactoryID, "", "Factory ID (required)") + gatewaysBulkCmd.Flags().StringSlice(flagFactoryID, nil, "Factory ID (required, repeat flag to create one gateway per ID)") gatewaysBulkCmd.Flags().String(flagFactoryKey, "", "Factory key (required)") 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"} { + for _, f := range []string{flagFactoryID, flagFactoryKey, "model", "firmware", "freq"} { mustMarkRequired(gatewaysBulkCmd, f) } } diff --git a/cmd/request_mapping_test.go b/cmd/request_mapping_test.go index f198bd2..e5a460c 100644 --- a/cmd/request_mapping_test.go +++ b/cmd/request_mapping_test.go @@ -119,8 +119,20 @@ func TestGatewaysBulkFlagToJSONMapping(t *testing.T) { t.Errorf(fmtUnexpectedRequest, r.Method, r.URL.Path) } body := readBody(t, r) - checkKey(t, body, "count", float64(5)) - checkKey(t, body, "factoryId", "fac-bulk") + checkAbsent(t, body, "count") + checkAbsent(t, body, "factoryId") + + rawFactoryIDs, ok := body["factoryIds"].([]any) + if !ok { + t.Fatalf("factoryIds must be an array, got %#v", body["factoryIds"]) + } + if len(rawFactoryIDs) != 2 { + t.Fatalf("want 2 factoryIds, got %d", len(rawFactoryIDs)) + } + if rawFactoryIDs[0] != "fac-bulk-1" || rawFactoryIDs[1] != "fac-bulk-2" { + t.Fatalf("unexpected factoryIds payload: %#v", rawFactoryIDs) + } + checkKey(t, body, "factoryKey", "key-bulk") checkKey(t, body, "model", "GW-MINI") checkKey(t, body, "firmwareVersion", "1.2.3") @@ -133,8 +145,8 @@ func TestGatewaysBulkFlagToJSONMapping(t *testing.T) { }) err := runCmd("gateways", "bulk", - "--count", "5", - testFlagFactoryID, "fac-bulk", + testFlagFactoryID, "fac-bulk-1", + testFlagFactoryID, "fac-bulk-2", testFlagFactoryKey, "key-bulk", testFlagModel, "GW-MINI", testFlagFirmware, "1.2.3", diff --git a/cmd/root.go b/cmd/root.go index 3a6abd9..e17b03a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -26,7 +26,11 @@ func Execute() error { // 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) + if slice, ok := f.Value.(interface{ Replace([]string) error }); ok { + _ = slice.Replace(nil) + } else { + _ = f.Value.Set(f.DefValue) + } f.Changed = false }) for _, child := range c.Commands() { diff --git a/internal/client/client.go b/internal/client/client.go index 94d5c87..0897cf8 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -60,12 +60,11 @@ type CreateGatewayRequest struct { // BulkCreateGatewaysRequest is the payload for POST /sim/gateways/bulk. type BulkCreateGatewaysRequest struct { - Count int `json:"count"` - FactoryID string `json:"factoryId"` - FactoryKey string `json:"factoryKey"` - Model string `json:"model,omitempty"` - FirmwareVersion string `json:"firmwareVersion,omitempty"` - SendFrequencyMs int `json:"sendFrequencyMs,omitempty"` + FactoryIDs []string `json:"factoryIds"` + FactoryKey string `json:"factoryKey"` + Model string `json:"model,omitempty"` + FirmwareVersion string `json:"firmwareVersion,omitempty"` + SendFrequencyMs int `json:"sendFrequencyMs,omitempty"` } // BulkCreateResponse is the response for POST /sim/gateways/bulk. diff --git a/internal/client/client_test.go b/internal/client/client_test.go index a708bd2..7e499c1 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -111,13 +111,13 @@ func TestBulkCreateGatewaysAllSuccess(t *testing.T) { var req client.BulkCreateGatewaysRequest decodeBody(t, r, &req) - if req.Count != 2 { - t.Errorf("count = %d, want 2", req.Count) + if len(req.FactoryIDs) != 2 { + t.Errorf("factoryIds length = %d, want 2", len(req.FactoryIDs)) } writeJSON(w, http.StatusCreated, want) }) - got, err := c.BulkCreateGateways(client.BulkCreateGatewaysRequest{Count: 2, FactoryID: "f", FactoryKey: "k"}) + got, err := c.BulkCreateGateways(client.BulkCreateGatewaysRequest{FactoryIDs: []string{"f-1", "f-2"}, FactoryKey: "k"}) if err != nil { t.Fatalf(errFmtUnexpected, err) } @@ -135,7 +135,7 @@ func TestBulkCreateGatewaysPartialErrors207(t *testing.T) { writeJSON(w, http.StatusMultiStatus, want) }) - got, err := c.BulkCreateGateways(client.BulkCreateGatewaysRequest{Count: 2, FactoryID: "f", FactoryKey: "k"}) + got, err := c.BulkCreateGateways(client.BulkCreateGatewaysRequest{FactoryIDs: []string{"f-1", "f-2"}, FactoryKey: "k"}) if err != nil { t.Fatalf("unexpected error on 207: %v", err) } @@ -497,8 +497,7 @@ func TestBulkCreateGatewaysInvalidJSONResponse(t *testing.T) { }) _, err := c.BulkCreateGateways(client.BulkCreateGatewaysRequest{ - Count: 1, - FactoryID: "f-1", + FactoryIDs: []string{"f-1"}, FactoryKey: "k-1", }) if err == nil { diff --git a/internal/client/request_construction_test.go b/internal/client/request_construction_test.go index 4603246..b392aba 100644 --- a/internal/client/request_construction_test.go +++ b/internal/client/request_construction_test.go @@ -142,8 +142,20 @@ func TestBulkCreateGatewaysRequestConstruction(t *testing.T) { assertContentType(t, r) body := readBodyAsMap(t, r) - assertKey(t, body, "count", float64(3)) - assertKey(t, body, "factoryId", "fac-bulk") + assertKeyAbsent(t, body, "count") + assertKeyAbsent(t, body, "factoryId") + + rawFactoryIDs, ok := body["factoryIds"].([]any) + if !ok { + t.Fatalf("factoryIds should be an array, got %#v", body["factoryIds"]) + } + if len(rawFactoryIDs) != 3 { + t.Fatalf("factoryIds length = %d, want 3", len(rawFactoryIDs)) + } + if rawFactoryIDs[0] != "fac-bulk-1" || rawFactoryIDs[1] != "fac-bulk-2" || rawFactoryIDs[2] != "fac-bulk-3" { + t.Fatalf("unexpected factoryIds payload: %#v", rawFactoryIDs) + } + assertKey(t, body, "factoryKey", "key-bulk") assertKey(t, body, "model", "GW-BULK") assertKey(t, body, "firmwareVersion", "1.0.0") @@ -156,8 +168,7 @@ func TestBulkCreateGatewaysRequestConstruction(t *testing.T) { }) _, err := c.BulkCreateGateways(client.BulkCreateGatewaysRequest{ - Count: 3, - FactoryID: "fac-bulk", + FactoryIDs: []string{"fac-bulk-1", "fac-bulk-2", "fac-bulk-3"}, FactoryKey: "key-bulk", Model: "GW-BULK", FirmwareVersion: "1.0.0", @@ -176,7 +187,7 @@ func TestBulkCreateGatewaysOptionalFieldsOmittedWhenZero(t *testing.T) { assertKeyAbsent(t, body, "sendFrequencyMs") writeJSON(w, http.StatusCreated, client.BulkCreateResponse{}) }) - _, _ = c.BulkCreateGateways(client.BulkCreateGatewaysRequest{Count: 1, FactoryID: "f", FactoryKey: "k"}) + _, _ = c.BulkCreateGateways(client.BulkCreateGatewaysRequest{FactoryIDs: []string{"f"}, FactoryKey: "k"}) } // ── GET /sim/gateways — no body ───────────────────────────────────────────────