From cd9a2651a269c47cc4e86bfe97bcfda3275c2317 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 29 Nov 2025 17:06:18 +0000 Subject: [PATCH] ADA: Add normalizeCommandStatus helper with unit tests Extract command status normalization logic from HandleCommandAck into a dedicated helper function. This improves testability and makes the status alias handling explicit and documented. The function accepts client-provided status strings and maps them to internal status constants: - acknowledged: "", "ack", "acknowledged" - completed: "success", "completed", "complete" - failed: "fail", "failed", "error" Adds 25 table-driven test cases covering all aliases, case insensitivity, whitespace handling, and invalid inputs. --- internal/api/docker_agents.go | 36 ++++++++++++----- internal/api/docker_agents_test.go | 62 ++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 internal/api/docker_agents_test.go diff --git a/internal/api/docker_agents.go b/internal/api/docker_agents.go index f5cdfe36c..b059bef29 100644 --- a/internal/api/docker_agents.go +++ b/internal/api/docker_agents.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "errors" "net/http" "strings" "time" @@ -25,6 +26,30 @@ type dockerCommandAckRequest struct { Message string `json:"message,omitempty"` } +// errInvalidCommandStatus is returned when an unrecognized command status is provided. +var errInvalidCommandStatus = errors.New("invalid command status") + +// normalizeCommandStatus converts a client-provided status string into a canonical +// internal status constant. It accepts multiple aliases for each status: +// - acknowledged: "", "ack", "acknowledged" +// - completed: "success", "completed", "complete" +// - failed: "fail", "failed", "error" +// +// Returns errInvalidCommandStatus for unrecognized values. +func normalizeCommandStatus(status string) (string, error) { + status = strings.ToLower(strings.TrimSpace(status)) + switch status { + case "", "ack", "acknowledged": + return monitoring.DockerCommandStatusAcknowledged, nil + case "success", "completed", "complete": + return monitoring.DockerCommandStatusCompleted, nil + case "fail", "failed", "error": + return monitoring.DockerCommandStatusFailed, nil + default: + return "", errInvalidCommandStatus + } +} + // NewDockerAgentHandlers constructs a new Docker agent handler group. func NewDockerAgentHandlers(m *monitoring.Monitor, hub *websocket.Hub) *DockerAgentHandlers { return &DockerAgentHandlers{monitor: m, wsHub: hub} @@ -154,15 +179,8 @@ func (h *DockerAgentHandlers) HandleCommandAck(w http.ResponseWriter, r *http.Re return } - status := strings.ToLower(strings.TrimSpace(req.Status)) - switch status { - case "", "ack", "acknowledged": - status = monitoring.DockerCommandStatusAcknowledged - case "success", "completed", "complete": - status = monitoring.DockerCommandStatusCompleted - case "fail", "failed", "error": - status = monitoring.DockerCommandStatusFailed - default: + status, err := normalizeCommandStatus(req.Status) + if err != nil { writeErrorResponse(w, http.StatusBadRequest, "invalid_status", "Invalid command status", nil) return } diff --git a/internal/api/docker_agents_test.go b/internal/api/docker_agents_test.go new file mode 100644 index 000000000..915e47a75 --- /dev/null +++ b/internal/api/docker_agents_test.go @@ -0,0 +1,62 @@ +package api + +import ( + "testing" + + "github.com/rcourtman/pulse-go-rewrite/internal/monitoring" +) + +func TestNormalizeCommandStatus(t *testing.T) { + tests := []struct { + name string + input string + want string + wantError bool + }{ + // Acknowledged status variants + {name: "empty string maps to acknowledged", input: "", want: monitoring.DockerCommandStatusAcknowledged}, + {name: "ack maps to acknowledged", input: "ack", want: monitoring.DockerCommandStatusAcknowledged}, + {name: "acknowledged maps to acknowledged", input: "acknowledged", want: monitoring.DockerCommandStatusAcknowledged}, + {name: "ACK uppercase maps to acknowledged", input: "ACK", want: monitoring.DockerCommandStatusAcknowledged}, + {name: "Acknowledged mixed case maps to acknowledged", input: "Acknowledged", want: monitoring.DockerCommandStatusAcknowledged}, + + // Completed status variants + {name: "success maps to completed", input: "success", want: monitoring.DockerCommandStatusCompleted}, + {name: "completed maps to completed", input: "completed", want: monitoring.DockerCommandStatusCompleted}, + {name: "complete maps to completed", input: "complete", want: monitoring.DockerCommandStatusCompleted}, + {name: "SUCCESS uppercase maps to completed", input: "SUCCESS", want: monitoring.DockerCommandStatusCompleted}, + {name: "Completed mixed case maps to completed", input: "Completed", want: monitoring.DockerCommandStatusCompleted}, + + // Failed status variants + {name: "fail maps to failed", input: "fail", want: monitoring.DockerCommandStatusFailed}, + {name: "failed maps to failed", input: "failed", want: monitoring.DockerCommandStatusFailed}, + {name: "error maps to failed", input: "error", want: monitoring.DockerCommandStatusFailed}, + {name: "FAILED uppercase maps to failed", input: "FAILED", want: monitoring.DockerCommandStatusFailed}, + {name: "Error mixed case maps to failed", input: "Error", want: monitoring.DockerCommandStatusFailed}, + + // Whitespace handling + {name: "whitespace around ack is trimmed", input: " ack ", want: monitoring.DockerCommandStatusAcknowledged}, + {name: "tab and newline around success is trimmed", input: "\tsuccess\n", want: monitoring.DockerCommandStatusCompleted}, + {name: "spaces only maps to acknowledged", input: " ", want: monitoring.DockerCommandStatusAcknowledged}, + + // Invalid inputs + {name: "unknown status returns error", input: "unknown", wantError: true}, + {name: "pending returns error", input: "pending", wantError: true}, + {name: "queued returns error", input: "queued", wantError: true}, + {name: "random string returns error", input: "foobar", wantError: true}, + {name: "partial match returns error", input: "acked", wantError: true}, + {name: "partial match fail returns error", input: "failure", wantError: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := normalizeCommandStatus(tt.input) + if (err != nil) != tt.wantError { + t.Fatalf("normalizeCommandStatus(%q) error = %v, wantError %v", tt.input, err, tt.wantError) + } + if !tt.wantError && got != tt.want { + t.Fatalf("normalizeCommandStatus(%q) = %q, want %q", tt.input, got, tt.want) + } + }) + } +}