From 71604739da270bb93794af15db38888a9ca522c5 Mon Sep 17 00:00:00 2001 From: "xukun.cx" Date: Tue, 2 Jun 2026 18:05:59 +0800 Subject: [PATCH 1/3] feat: add client-side page_size validation for service commands Validate query parameter values against min/max constraints declared in the API schema metadata before sending requests to the server. This provides clear error messages instead of the generic "field validation failed" response from the backend. The validation applies to all numeric query parameters that have min or max constraints in their schema definition, catching out-of-range values early with actionable hints (including the schema command path for reference). sprint: S2 --- cmd/service/service.go | 68 ++++++++++++++ cmd/service/service_test.go | 175 ++++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+) diff --git a/cmd/service/service.go b/cmd/service/service.go index 125cc584f..db4baf958 100644 --- a/cmd/service/service.go +++ b/cmd/service/service.go @@ -5,8 +5,10 @@ package service import ( "context" + "encoding/json" "fmt" "io" + "strconv" "strings" "github.com/larksuite/cli/errs" @@ -426,11 +428,17 @@ func buildServiceRequest(opts *ServiceMethodOptions) (client.RawApiRequest, *cmd WithParam(name) } if exists && !util.IsEmptyValue(value) { + if err := validateQueryParamRange(name, value, p, schemaPath); err != nil { + return client.RawApiRequest{}, nil, err + } queryParams[name] = value } } for name, value := range params { if _, ok := queryParams[name]; !ok { + if err := validateQueryParamRange(name, value, nil, schemaPath); err != nil { + return client.RawApiRequest{}, nil, err + } queryParams[name] = value } } @@ -533,3 +541,63 @@ func servicePaginate(ctx context.Context, ac *client.APIClient, request client.R return nil } } + +// validateQueryParamRange checks that a query parameter value falls within the +// range declared in the method's meta_data schema (min / max fields). +// If paramSpec is nil (parameter not declared in schema), validation is skipped. +func validateQueryParamRange(name string, value interface{}, paramSpec map[string]interface{}, schemaPath string) error { + if paramSpec == nil { + return nil + } + // Only validate numeric parameters with min/max constraints. + minStr := registry.GetStrFromMap(paramSpec, "min") + maxStr := registry.GetStrFromMap(paramSpec, "max") + if minStr == "" && maxStr == "" { + return nil + } + + // Convert value to float64 for comparison. + var numValue float64 + switch v := value.(type) { + case float64: + numValue = v + case float32: + numValue = float64(v) + case int: + numValue = float64(v) + case int64: + numValue = float64(v) + case json.Number: + f, err := v.Float64() + if err != nil { + return nil // not a number, skip validation + } + numValue = f + case string: + f, err := strconv.ParseFloat(v, 64) + if err != nil { + return nil // not a number, skip validation + } + numValue = f + default: + return nil // non-numeric type, skip validation + } + + if minStr != "" { + if minVal, err := strconv.ParseFloat(minStr, 64); err == nil && numValue < minVal { + return errs.NewValidationError(errs.SubtypeInvalidArgument, + "invalid --params %s: %v is less than the minimum allowed value %s", name, numValue, minStr). + WithHint("The API schema requires %s to be at least %s. Run: lark-cli schema %s", name, minStr, schemaPath). + WithParam(name) + } + } + if maxStr != "" { + if maxVal, err := strconv.ParseFloat(maxStr, 64); err == nil && numValue > maxVal { + return errs.NewValidationError(errs.SubtypeInvalidArgument, + "invalid --params %s: %v exceeds the maximum allowed value %s", name, numValue, maxStr). + WithHint("The API schema requires %s to be at most %s. Run: lark-cli schema %s", name, maxStr, schemaPath). + WithParam(name) + } + } + return nil +} diff --git a/cmd/service/service_test.go b/cmd/service/service_test.go index 42377850e..3eeeae8b0 100644 --- a/cmd/service/service_test.go +++ b/cmd/service/service_test.go @@ -765,3 +765,178 @@ func TestDetectFileFields(t *testing.T) { }) } } + +// ── query parameter range validation ── + +func TestValidateQueryParamRange_NilSpec(t *testing.T) { + if err := validateQueryParamRange("page_size", float64(50), nil, "svc.res.list"); err != nil { + t.Errorf("expected nil for nil spec, got: %v", err) + } +} + +func TestValidateQueryParamRange_NoMinMax(t *testing.T) { + spec := map[string]interface{}{"location": "query"} + if err := validateQueryParamRange("page_size", float64(50), spec, "svc.res.list"); err != nil { + t.Errorf("expected nil when no min/max, got: %v", err) + } +} + +func TestValidateQueryParamRange_WithinRange(t *testing.T) { + spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} + if err := validateQueryParamRange("page_size", float64(10), spec, "svc.res.list"); err != nil { + t.Errorf("expected nil for value within range, got: %v", err) + } +} + +func TestValidateQueryParamRange_ExactlyAtMax(t *testing.T) { + spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} + if err := validateQueryParamRange("page_size", float64(20), spec, "svc.res.list"); err != nil { + t.Errorf("expected nil for value at max boundary, got: %v", err) + } +} + +func TestValidateQueryParamRange_ExactlyAtMin(t *testing.T) { + spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} + if err := validateQueryParamRange("page_size", float64(1), spec, "svc.res.list"); err != nil { + t.Errorf("expected nil for value at min boundary, got: %v", err) + } +} + +func TestValidateQueryParamRange_ExceedsMax(t *testing.T) { + spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} + err := validateQueryParamRange("page_size", float64(50), spec, "mail.user_mailbox.messages.list") + if err == nil { + t.Fatal("expected error for page_size exceeding max") + } + if !strings.Contains(err.Error(), "exceeds the maximum") { + t.Errorf("expected 'exceeds the maximum' error, got: %v", err) + } + if !strings.Contains(err.Error(), "20") { + t.Errorf("expected error to mention max value 20, got: %v", err) + } +} + +func TestValidateQueryParamRange_BelowMin(t *testing.T) { + spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} + err := validateQueryParamRange("page_size", float64(0), spec, "mail.user_mailbox.messages.list") + if err == nil { + t.Fatal("expected error for page_size below min") + } + if !strings.Contains(err.Error(), "less than the minimum") { + t.Errorf("expected 'less than the minimum' error, got: %v", err) + } +} + +func TestValidateQueryParamRange_StringValue(t *testing.T) { + spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} + err := validateQueryParamRange("page_size", "50", spec, "svc.res.list") + if err == nil { + t.Fatal("expected error for string page_size exceeding max") + } + if !strings.Contains(err.Error(), "exceeds the maximum") { + t.Errorf("expected 'exceeds the maximum' error, got: %v", err) + } +} + +func TestValidateQueryParamRange_NonNumericStringValue(t *testing.T) { + spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} + if err := validateQueryParamRange("page_size", "abc", spec, "svc.res.list"); err != nil { + t.Errorf("expected nil for non-numeric string value, got: %v", err) + } +} + +func TestValidateQueryParamRange_IntValue(t *testing.T) { + spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} + err := validateQueryParamRange("page_size", 50, spec, "svc.res.list") + if err == nil { + t.Fatal("expected error for int page_size exceeding max") + } + if !strings.Contains(err.Error(), "exceeds the maximum") { + t.Errorf("expected 'exceeds the maximum' error, got: %v", err) + } +} + +func TestValidateQueryParamRange_OnlyMax(t *testing.T) { + spec := map[string]interface{}{"location": "query", "max": "100"} + if err := validateQueryParamRange("limit", float64(50), spec, "svc.res.list"); err != nil { + t.Errorf("expected nil for value within max-only range, got: %v", err) + } + err := validateQueryParamRange("limit", float64(200), spec, "svc.res.list") + if err == nil { + t.Fatal("expected error for value exceeding max-only constraint") + } +} + +func TestValidateQueryParamRange_OnlyMin(t *testing.T) { + spec := map[string]interface{}{"location": "query", "min": "1"} + if err := validateQueryParamRange("limit", float64(5), spec, "svc.res.list"); err != nil { + t.Errorf("expected nil for value above min-only constraint, got: %v", err) + } + err := validateQueryParamRange("limit", float64(0), spec, "svc.res.list") + if err == nil { + t.Fatal("expected error for value below min-only constraint") + } +} + +func TestServiceMethod_PageSizeExceedsMax(t *testing.T) { + spec := map[string]interface{}{ + "name": "mail", "servicePath": "/open-apis/mail/v1", + } + method := map[string]interface{}{ + "path": "user_mailboxes/{user_mailbox_id}/messages", + "httpMethod": "GET", + "parameters": map[string]interface{}{ + "user_mailbox_id": map[string]interface{}{ + "type": "string", "location": "path", "required": true, + }, + "page_size": map[string]interface{}{ + "type": "integer", "location": "query", "required": true, + "min": "1", "max": "20", + }, + }, + } + f, _, _, _ := cmdutil.TestFactory(t, testConfig) + cmd := NewCmdServiceMethod(f, spec, method, "list", "user_mailbox.messages", nil) + cmd.SetArgs([]string{"--params", `{"user_mailbox_id":"me","page_size":50}`, "--dry-run"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected error for page_size exceeding max") + } + if !strings.Contains(err.Error(), "exceeds the maximum") { + t.Errorf("expected 'exceeds the maximum' error, got: %v", err) + } + if !strings.Contains(err.Error(), "20") { + t.Errorf("expected error to mention max value 20, got: %v", err) + } +} + +func TestServiceMethod_PageSizeWithinMax(t *testing.T) { + spec := map[string]interface{}{ + "name": "mail", "servicePath": "/open-apis/mail/v1", + } + method := map[string]interface{}{ + "path": "user_mailboxes/{user_mailbox_id}/messages", + "httpMethod": "GET", + "parameters": map[string]interface{}{ + "user_mailbox_id": map[string]interface{}{ + "type": "string", "location": "path", "required": true, + }, + "page_size": map[string]interface{}{ + "type": "integer", "location": "query", "required": false, + "min": "1", "max": "20", + }, + }, + } + f, stdout, _, _ := cmdutil.TestFactory(t, testConfig) + cmd := NewCmdServiceMethod(f, spec, method, "list", "user_mailbox.messages", nil) + cmd.SetArgs([]string{"--params", `{"user_mailbox_id":"me","page_size":20}`, "--dry-run"}) + + err := cmd.Execute() + if err != nil { + t.Fatalf("expected no error for valid page_size, got: %v", err) + } + if !strings.Contains(stdout.String(), "Dry Run") { + t.Error("expected dry-run output") + } +} From 0ebb9de4a1d5a345b04247dfdfd27a99ea8cc29b Mon Sep 17 00:00:00 2001 From: "xukun.cx" Date: Tue, 2 Jun 2026 19:16:13 +0800 Subject: [PATCH 2/3] fix: avoid nilerr warning when numeric parsing fails Change-Type: ci-fix --- cmd/service/service.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cmd/service/service.go b/cmd/service/service.go index db4baf958..e4bb2deb8 100644 --- a/cmd/service/service.go +++ b/cmd/service/service.go @@ -568,14 +568,14 @@ func validateQueryParamRange(name string, value interface{}, paramSpec map[strin case int64: numValue = float64(v) case json.Number: - f, err := v.Float64() - if err != nil { + f, ok := parseFloat64(v.String()) + if !ok { return nil // not a number, skip validation } numValue = f case string: - f, err := strconv.ParseFloat(v, 64) - if err != nil { + f, ok := parseFloat64(v) + if !ok { return nil // not a number, skip validation } numValue = f @@ -601,3 +601,8 @@ func validateQueryParamRange(name string, value interface{}, paramSpec map[strin } return nil } + +func parseFloat64(value string) (float64, bool) { + result, err := strconv.ParseFloat(value, 64) + return result, err == nil +} From a7fa3e27ab314adb3cc05a24e54b59830ff536fa Mon Sep 17 00:00:00 2001 From: "xukun.cx" Date: Wed, 3 Jun 2026 14:34:59 +0800 Subject: [PATCH 3/3] refactor: scope page_size validation to allowlist for mail.user_mailbox.messages.list Replace the generic min/max validation that applied to all service commands with an explicit allowlist. Only mail.user_mailbox.messages.list page_size is validated (range 1..20). Non-integer values like 1.5 and "abc" are now rejected with a clear error message instead of silently skipped. sprint: S2 --- cmd/service/service.go | 96 ++++++++++++------------ cmd/service/service_test.go | 146 ++++++++++++++++++++++++------------ 2 files changed, 147 insertions(+), 95 deletions(-) diff --git a/cmd/service/service.go b/cmd/service/service.go index e4bb2deb8..ef0c53c7c 100644 --- a/cmd/service/service.go +++ b/cmd/service/service.go @@ -542,67 +542,69 @@ func servicePaginate(ctx context.Context, ac *client.APIClient, request client.R } } -// validateQueryParamRange checks that a query parameter value falls within the -// range declared in the method's meta_data schema (min / max fields). -// If paramSpec is nil (parameter not declared in schema), validation is skipped. -func validateQueryParamRange(name string, value interface{}, paramSpec map[string]interface{}, schemaPath string) error { - if paramSpec == nil { +// paramValidationRules defines per-(schemaPath, paramName) validation rules. +// Only explicitly listed entries are validated; all other parameters pass through. +var paramValidationRules = map[string]map[string]struct { + min int + max int +}{ + "mail.user_mailbox.messages.list": { + "page_size": {min: 1, max: 20}, + }, +} + +// validateQueryParamRange validates a query parameter value against explicit +// per-(schemaPath, paramName) rules. Only allowlisted entries are validated; +// parameters not in the allowlist are silently skipped. +func validateQueryParamRange(name string, value interface{}, _ map[string]interface{}, schemaPath string) error { + paramRules, ok := paramValidationRules[schemaPath] + if !ok { return nil } - // Only validate numeric parameters with min/max constraints. - minStr := registry.GetStrFromMap(paramSpec, "min") - maxStr := registry.GetStrFromMap(paramSpec, "max") - if minStr == "" && maxStr == "" { + rule, ok := paramRules[name] + if !ok { return nil } - // Convert value to float64 for comparison. - var numValue float64 + // Resolve the raw string representation of the value. + var rawStr string switch v := value.(type) { + case json.Number: + rawStr = v.String() + case string: + rawStr = v case float64: - numValue = v + rawStr = strconv.FormatFloat(v, 'f', -1, 64) case float32: - numValue = float64(v) + rawStr = strconv.FormatFloat(float64(v), 'f', -1, 32) case int: - numValue = float64(v) + rawStr = strconv.Itoa(v) case int64: - numValue = float64(v) - case json.Number: - f, ok := parseFloat64(v.String()) - if !ok { - return nil // not a number, skip validation - } - numValue = f - case string: - f, ok := parseFloat64(v) - if !ok { - return nil // not a number, skip validation - } - numValue = f + rawStr = strconv.FormatInt(v, 10) default: - return nil // non-numeric type, skip validation + return nil } - if minStr != "" { - if minVal, err := strconv.ParseFloat(minStr, 64); err == nil && numValue < minVal { - return errs.NewValidationError(errs.SubtypeInvalidArgument, - "invalid --params %s: %v is less than the minimum allowed value %s", name, numValue, minStr). - WithHint("The API schema requires %s to be at least %s. Run: lark-cli schema %s", name, minStr, schemaPath). - WithParam(name) - } + // Must parse as an integer (reject 1.5, "abc", etc.). + intVal, err := strconv.Atoi(rawStr) + if err != nil { + return errs.NewValidationError(errs.SubtypeInvalidArgument, + "invalid --params %s: %s is not a valid integer", name, rawStr). + WithHint("The parameter %s must be an integer between %d and %d. Run: lark-cli schema %s", name, rule.min, rule.max, schemaPath). + WithParam(name) } - if maxStr != "" { - if maxVal, err := strconv.ParseFloat(maxStr, 64); err == nil && numValue > maxVal { - return errs.NewValidationError(errs.SubtypeInvalidArgument, - "invalid --params %s: %v exceeds the maximum allowed value %s", name, numValue, maxStr). - WithHint("The API schema requires %s to be at most %s. Run: lark-cli schema %s", name, maxStr, schemaPath). - WithParam(name) - } + + if intVal < rule.min { + return errs.NewValidationError(errs.SubtypeInvalidArgument, + "invalid --params %s: %d is less than the minimum allowed value %d", name, intVal, rule.min). + WithHint("The parameter %s must be at least %d. Run: lark-cli schema %s", name, rule.min, schemaPath). + WithParam(name) + } + if intVal > rule.max { + return errs.NewValidationError(errs.SubtypeInvalidArgument, + "invalid --params %s: %d exceeds the maximum allowed value %d", name, intVal, rule.max). + WithHint("The parameter %s must be at most %d. Run: lark-cli schema %s", name, rule.max, schemaPath). + WithParam(name) } return nil } - -func parseFloat64(value string) (float64, bool) { - result, err := strconv.ParseFloat(value, 64) - return result, err == nil -} diff --git a/cmd/service/service_test.go b/cmd/service/service_test.go index 3eeeae8b0..fe8ef5016 100644 --- a/cmd/service/service_test.go +++ b/cmd/service/service_test.go @@ -766,45 +766,51 @@ func TestDetectFileFields(t *testing.T) { } } -// ── query parameter range validation ── +// ── query parameter range validation (allowlist-based) ── -func TestValidateQueryParamRange_NilSpec(t *testing.T) { - if err := validateQueryParamRange("page_size", float64(50), nil, "svc.res.list"); err != nil { - t.Errorf("expected nil for nil spec, got: %v", err) +const testSchemaPath = "mail.user_mailbox.messages.list" + +func TestValidateQueryParamRange_AllowlistNonTargetSkipped(t *testing.T) { + // Non-target schemaPath: validation is silently skipped. + if err := validateQueryParamRange("page_size", float64(50), nil, "calendar.events.list"); err != nil { + t.Errorf("expected nil for non-target schemaPath, got: %v", err) + } +} + +func TestValidateQueryParamRange_NonTargetParamSkipped(t *testing.T) { + // Target schemaPath but non-allowlisted param name: skipped. + if err := validateQueryParamRange("other_param", float64(9999), nil, testSchemaPath); err != nil { + t.Errorf("expected nil for non-allowlisted param, got: %v", err) } } -func TestValidateQueryParamRange_NoMinMax(t *testing.T) { - spec := map[string]interface{}{"location": "query"} - if err := validateQueryParamRange("page_size", float64(50), spec, "svc.res.list"); err != nil { - t.Errorf("expected nil when no min/max, got: %v", err) +func TestValidateQueryParamRange_NilSpec(t *testing.T) { + // paramSpec is now ignored (underscore); nil is fine. + if err := validateQueryParamRange("page_size", float64(50), nil, "other.service.list"); err != nil { + t.Errorf("expected nil for non-allowlisted schemaPath, got: %v", err) } } func TestValidateQueryParamRange_WithinRange(t *testing.T) { - spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} - if err := validateQueryParamRange("page_size", float64(10), spec, "svc.res.list"); err != nil { + if err := validateQueryParamRange("page_size", float64(10), nil, testSchemaPath); err != nil { t.Errorf("expected nil for value within range, got: %v", err) } } func TestValidateQueryParamRange_ExactlyAtMax(t *testing.T) { - spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} - if err := validateQueryParamRange("page_size", float64(20), spec, "svc.res.list"); err != nil { + if err := validateQueryParamRange("page_size", float64(20), nil, testSchemaPath); err != nil { t.Errorf("expected nil for value at max boundary, got: %v", err) } } func TestValidateQueryParamRange_ExactlyAtMin(t *testing.T) { - spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} - if err := validateQueryParamRange("page_size", float64(1), spec, "svc.res.list"); err != nil { + if err := validateQueryParamRange("page_size", float64(1), nil, testSchemaPath); err != nil { t.Errorf("expected nil for value at min boundary, got: %v", err) } } func TestValidateQueryParamRange_ExceedsMax(t *testing.T) { - spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} - err := validateQueryParamRange("page_size", float64(50), spec, "mail.user_mailbox.messages.list") + err := validateQueryParamRange("page_size", float64(21), nil, testSchemaPath) if err == nil { t.Fatal("expected error for page_size exceeding max") } @@ -817,8 +823,7 @@ func TestValidateQueryParamRange_ExceedsMax(t *testing.T) { } func TestValidateQueryParamRange_BelowMin(t *testing.T) { - spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} - err := validateQueryParamRange("page_size", float64(0), spec, "mail.user_mailbox.messages.list") + err := validateQueryParamRange("page_size", float64(0), nil, testSchemaPath) if err == nil { t.Fatal("expected error for page_size below min") } @@ -827,9 +832,15 @@ func TestValidateQueryParamRange_BelowMin(t *testing.T) { } } -func TestValidateQueryParamRange_StringValue(t *testing.T) { - spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} - err := validateQueryParamRange("page_size", "50", spec, "svc.res.list") +func TestValidateQueryParamRange_StringIntegerPasses(t *testing.T) { + // String integer "20" should pass. + if err := validateQueryParamRange("page_size", "20", nil, testSchemaPath); err != nil { + t.Errorf("expected nil for string integer \"20\", got: %v", err) + } +} + +func TestValidateQueryParamRange_StringIntegerExceedsMax(t *testing.T) { + err := validateQueryParamRange("page_size", "21", nil, testSchemaPath) if err == nil { t.Fatal("expected error for string page_size exceeding max") } @@ -838,43 +849,50 @@ func TestValidateQueryParamRange_StringValue(t *testing.T) { } } -func TestValidateQueryParamRange_NonNumericStringValue(t *testing.T) { - spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} - if err := validateQueryParamRange("page_size", "abc", spec, "svc.res.list"); err != nil { - t.Errorf("expected nil for non-numeric string value, got: %v", err) +func TestValidateQueryParamRange_FloatRejected(t *testing.T) { + // 1.5 is not an integer and must be rejected. + err := validateQueryParamRange("page_size", 1.5, nil, testSchemaPath) + if err == nil { + t.Fatal("expected error for float page_size") + } + if !strings.Contains(err.Error(), "not a valid integer") { + t.Errorf("expected 'not a valid integer' error, got: %v", err) } } -func TestValidateQueryParamRange_IntValue(t *testing.T) { - spec := map[string]interface{}{"location": "query", "min": "1", "max": "20"} - err := validateQueryParamRange("page_size", 50, spec, "svc.res.list") +func TestValidateQueryParamRange_StringFloatRejected(t *testing.T) { + // "1.5" is not an integer and must be rejected. + err := validateQueryParamRange("page_size", "1.5", nil, testSchemaPath) if err == nil { - t.Fatal("expected error for int page_size exceeding max") + t.Fatal("expected error for string float page_size") } - if !strings.Contains(err.Error(), "exceeds the maximum") { - t.Errorf("expected 'exceeds the maximum' error, got: %v", err) + if !strings.Contains(err.Error(), "not a valid integer") { + t.Errorf("expected 'not a valid integer' error, got: %v", err) } } -func TestValidateQueryParamRange_OnlyMax(t *testing.T) { - spec := map[string]interface{}{"location": "query", "max": "100"} - if err := validateQueryParamRange("limit", float64(50), spec, "svc.res.list"); err != nil { - t.Errorf("expected nil for value within max-only range, got: %v", err) - } - err := validateQueryParamRange("limit", float64(200), spec, "svc.res.list") +func TestValidateQueryParamRange_StringNonNumericRejected(t *testing.T) { + // "abc" is not a valid integer. + err := validateQueryParamRange("page_size", "abc", nil, testSchemaPath) if err == nil { - t.Fatal("expected error for value exceeding max-only constraint") + t.Fatal("expected error for non-numeric string page_size") + } + if !strings.Contains(err.Error(), "not a valid integer") { + t.Errorf("expected 'not a valid integer' error, got: %v", err) } } -func TestValidateQueryParamRange_OnlyMin(t *testing.T) { - spec := map[string]interface{}{"location": "query", "min": "1"} - if err := validateQueryParamRange("limit", float64(5), spec, "svc.res.list"); err != nil { - t.Errorf("expected nil for value above min-only constraint, got: %v", err) +func TestValidateQueryParamRange_IntTypePasses(t *testing.T) { + if err := validateQueryParamRange("page_size", 10, nil, testSchemaPath); err != nil { + t.Errorf("expected nil for int value within range, got: %v", err) } - err := validateQueryParamRange("limit", float64(0), spec, "svc.res.list") - if err == nil { - t.Fatal("expected error for value below min-only constraint") +} + +func TestValidateQueryParamRange_NonTargetServiceNotBlocked(t *testing.T) { + // A parameter with min/max in metadata from a non-target service command + // must NOT be blocked — proves the allowlist is effective. + if err := validateQueryParamRange("limit", float64(200), nil, "calendar.events.list"); err != nil { + t.Errorf("expected nil for non-target service command, got: %v", err) } } @@ -891,13 +909,12 @@ func TestServiceMethod_PageSizeExceedsMax(t *testing.T) { }, "page_size": map[string]interface{}{ "type": "integer", "location": "query", "required": true, - "min": "1", "max": "20", }, }, } f, _, _, _ := cmdutil.TestFactory(t, testConfig) cmd := NewCmdServiceMethod(f, spec, method, "list", "user_mailbox.messages", nil) - cmd.SetArgs([]string{"--params", `{"user_mailbox_id":"me","page_size":50}`, "--dry-run"}) + cmd.SetArgs([]string{"--params", `{"user_mailbox_id":"me","page_size":21}`, "--dry-run"}) err := cmd.Execute() if err == nil { @@ -924,7 +941,6 @@ func TestServiceMethod_PageSizeWithinMax(t *testing.T) { }, "page_size": map[string]interface{}{ "type": "integer", "location": "query", "required": false, - "min": "1", "max": "20", }, }, } @@ -940,3 +956,37 @@ func TestServiceMethod_PageSizeWithinMax(t *testing.T) { t.Error("expected dry-run output") } } + +func TestServiceMethod_PageAllSkipsRequiredCheck(t *testing.T) { + // --page-all should skip required check for page_size and page_token. + spec := map[string]interface{}{ + "name": "mail", "servicePath": "/open-apis/mail/v1", + } + method := map[string]interface{}{ + "path": "user_mailboxes/{user_mailbox_id}/messages", + "httpMethod": "GET", + "parameters": map[string]interface{}{ + "user_mailbox_id": map[string]interface{}{ + "type": "string", "location": "path", "required": true, + }, + "page_size": map[string]interface{}{ + "type": "integer", "location": "query", "required": true, + }, + "page_token": map[string]interface{}{ + "type": "string", "location": "query", "required": true, + }, + }, + } + f, stdout, _, _ := cmdutil.TestFactory(t, testConfig) + cmd := NewCmdServiceMethod(f, spec, method, "list", "user_mailbox.messages", nil) + // --page-all without providing page_size or page_token should not fail required check. + cmd.SetArgs([]string{"--params", `{"user_mailbox_id":"me"}`, "--page-all", "--page-limit", "1", "--dry-run"}) + + err := cmd.Execute() + if err != nil { + t.Fatalf("expected no error for --page-all skipping required params, got: %v", err) + } + if !strings.Contains(stdout.String(), "Dry Run") { + t.Error("expected dry-run output") + } +}