package main import ( "bytes" "encoding/json" "errors" "strings" "testing" ) func TestRunVersionJSON(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer if err := runWithIO(t.Context(), []string{"version", "-json"}, &stdout, &stderr); err != nil { t.Fatalf("runWithIO() error = %v; stderr = %s", err, stderr.String()) } var output versionOutput if err := json.Unmarshal(stdout.Bytes(), &output); err != nil { t.Fatalf("parse JSON: %v", err) } if output.GatewayProtocolVersion == 0 || output.WorkerProtocolVersion == 0 { t.Fatalf("protocol versions were not populated: %+v", output) } } func TestCommonOptionsRedactsAPIKey(t *testing.T) { options, err := (&commonOptions{ Endpoint: "localhost:5000", APIKey: "mxgw_super_secret", Plaintext: true, CallTimeout: "2s", }).resolved() if err != nil { t.Fatalf("resolved() error = %v", err) } data, err := json.Marshal(options) if err != nil { t.Fatalf("marshal options: %v", err) } if strings.Contains(string(data), "super_secret") { t.Fatalf("redacted JSON leaked API key: %s", data) } if !strings.Contains(string(data), "mxgw") { t.Fatalf("redacted JSON did not preserve key shape: %s", data) } } func TestParseValueBuildsTypedValue(t *testing.T) { value, err := parseValue("int32", "123") if err != nil { t.Fatalf("parseValue() error = %v", err) } if got := value.GetInt32Value(); got != 123 { t.Fatalf("int32 value = %d, want 123", got) } } func TestParseInt32ListParsesValidTokens(t *testing.T) { items, err := parseInt32List("1, 2 ,3") if err != nil { t.Fatalf("parseInt32List() error = %v", err) } want := []int32{1, 2, 3} if len(items) != len(want) { t.Fatalf("parseInt32List() = %v, want %v", items, want) } for i := range want { if items[i] != want[i] { t.Fatalf("parseInt32List()[%d] = %d, want %d", i, items[i], want[i]) } } } func TestParseInt32ListReturnsErrorOnMalformedToken(t *testing.T) { items, err := parseInt32List("1,foo") if err == nil { t.Fatalf("parseInt32List() error = nil, want a parse error; items = %v", items) } if items != nil { t.Fatalf("parseInt32List() items = %v, want nil on error", items) } if !strings.Contains(err.Error(), "foo") { t.Fatalf("parseInt32List() error = %q, want it to name the bad token", err.Error()) } } // TestParseValueWrapsStrconvErrorWithFlagContext pins Client.Go-017: each // typed branch of parseValue wraps the bare strconv error with `%w` and names // the offending flag and value, so the CLI surface is consistent with // parseInt32List ("invalid item handle %q: %w") and parseRfc3339Timestamp // ("invalid RFC 3339 timestamp %q: %w"). func TestParseValueWrapsStrconvErrorWithFlagContext(t *testing.T) { cases := []struct { valueType string valueText string }{ {"bool", "notabool"}, {"int32", "foo"}, {"int64", "foo"}, {"float", "notafloat"}, {"double", "notadouble"}, } for _, tc := range cases { t.Run(tc.valueType, func(t *testing.T) { _, err := parseValue(tc.valueType, tc.valueText) if err == nil { t.Fatalf("parseValue(%q, %q) error = nil, want a parse error", tc.valueType, tc.valueText) } msg := err.Error() if !strings.Contains(msg, "-value") { t.Fatalf("parseValue() error = %q, want it to name the -value flag", msg) } if !strings.Contains(msg, tc.valueType) { t.Fatalf("parseValue() error = %q, want it to name the type %q", msg, tc.valueType) } if !strings.Contains(msg, tc.valueText) { t.Fatalf("parseValue() error = %q, want it to name the bad token %q", msg, tc.valueText) } // errors.Unwrap must reach the underlying strconv error so callers // can still errors.Is/As against strconv.ErrSyntax if they care. if errors.Unwrap(err) == nil { t.Fatalf("parseValue() returned unwrapped error %q, want a %%w wrap", msg) } }) } } // TestRunWriteBulkVariantGatesSecuredFlags pins the Client.Go-015 fix at the // CLI surface: secured-only flags (-current-user-id, -verifier-user-id) must // not be registered on the non-secured variants, and -user-id must not be // registered on the secured variants. The flag package rejects an unknown // flag with "flag provided but not defined", which a future refactor that // re-broadens flag registration would silently undo without this test. func TestRunWriteBulkVariantGatesSecuredFlags(t *testing.T) { cases := []struct { name string command string flag string }{ {"write-bulk rejects -current-user-id", "write-bulk", "-current-user-id"}, {"write-bulk rejects -verifier-user-id", "write-bulk", "-verifier-user-id"}, {"write2-bulk rejects -current-user-id", "write2-bulk", "-current-user-id"}, {"write-secured-bulk rejects -user-id", "write-secured-bulk", "-user-id"}, {"write-secured2-bulk rejects -user-id", "write-secured2-bulk", "-user-id"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var stdout, stderr bytes.Buffer err := runWithIO(t.Context(), []string{ tc.command, "-plaintext", "-session-id", "sess", "-server-handle", "1", "-item-handles", "1", "-values", "1", tc.flag, "1", }, &stdout, &stderr) if err == nil { t.Fatalf("runWithIO(%s %s) error = nil, want flag-not-defined", tc.command, tc.flag) } combined := err.Error() + stderr.String() if !strings.Contains(combined, "flag provided but not defined") { t.Fatalf("runWithIO(%s %s) error/stderr = %q, want 'flag provided but not defined'", tc.command, tc.flag, combined) } }) } } // TestRunReadBulkRejectsMissingArgs pins the "session-id and items are // required" validation in runReadBulk before any network dial happens. func TestRunReadBulkRejectsMissingArgs(t *testing.T) { cases := []struct { name string args []string }{ {"no flags", []string{"read-bulk"}}, {"missing items", []string{"read-bulk", "-plaintext", "-session-id", "sess"}}, {"missing session-id", []string{"read-bulk", "-plaintext", "-items", "Tag.Attr"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var stdout, stderr bytes.Buffer err := runWithIO(t.Context(), tc.args, &stdout, &stderr) if err == nil { t.Fatalf("runWithIO(%v) error = nil, want validation error", tc.args) } if !strings.Contains(err.Error(), "session-id and items are required") { t.Fatalf("runWithIO(%v) error = %q, want 'session-id and items are required'", tc.args, err.Error()) } }) } } // TestRunBenchReadBulkRejectsNonPositiveBulkSize pins the bulk-size>=1 check // at runBenchReadBulk's flag-parsing stage so a future refactor cannot drop // the positivity guard without breaking this test. func TestRunBenchReadBulkRejectsNonPositiveBulkSize(t *testing.T) { var stdout, stderr bytes.Buffer err := runWithIO(t.Context(), []string{ "bench-read-bulk", "-plaintext", "-bulk-size", "0", }, &stdout, &stderr) if err == nil { t.Fatalf("runWithIO(bench-read-bulk -bulk-size 0) error = nil, want positivity error") } if !strings.Contains(err.Error(), "bulk-size must be positive") { t.Fatalf("runWithIO error = %q, want 'bulk-size must be positive'", err.Error()) } } // TestRunBenchReadBulkRejectsNonPositiveDuration pins the duration-seconds>=1 // check at runBenchReadBulk's flag-parsing stage. func TestRunBenchReadBulkRejectsNonPositiveDuration(t *testing.T) { var stdout, stderr bytes.Buffer err := runWithIO(t.Context(), []string{ "bench-read-bulk", "-plaintext", "-duration-seconds", "0", }, &stdout, &stderr) if err == nil { t.Fatalf("runWithIO(bench-read-bulk -duration-seconds 0) error = nil, want positivity error") } if !strings.Contains(err.Error(), "duration-seconds must be positive") { t.Fatalf("runWithIO error = %q, want 'duration-seconds must be positive'", err.Error()) } } // TestRunWriteBulkVariantRejectsMismatchedHandlesAndValues pins the explicit // "item-handles count ... does not match values count ..." check at the CLI // surface so the validation error surfaces before any dial happens. func TestRunWriteBulkVariantRejectsMismatchedHandlesAndValues(t *testing.T) { var stdout, stderr bytes.Buffer err := runWithIO(t.Context(), []string{ "write-bulk", "-plaintext", "-session-id", "sess", "-server-handle", "1", "-item-handles", "1,2,3", "-values", "10,20", }, &stdout, &stderr) if err == nil { t.Fatalf("runWithIO(write-bulk mismatched counts) error = nil, want mismatch error") } if !strings.Contains(err.Error(), "item-handles count") || !strings.Contains(err.Error(), "values count") { t.Fatalf("runWithIO error = %q, want 'item-handles count ... values count ...'", err.Error()) } }