Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions cmd/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

import (
"context"
"encoding/json"
"fmt"
"io"
"strconv"
"strings"

"github.com/larksuite/cli/errs"
Expand Down Expand Up @@ -426,11 +428,17 @@
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

Check warning on line 440 in cmd/service/service.go

View check run for this annotation

Codecov / codecov/patch

cmd/service/service.go#L439-L440

Added lines #L439 - L440 were not covered by tests
}
queryParams[name] = value
}
}
Expand Down Expand Up @@ -533,3 +541,68 @@
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)

Check warning on line 565 in cmd/service/service.go

View check run for this annotation

Codecov / codecov/patch

cmd/service/service.go#L564-L565

Added lines #L564 - L565 were not covered by tests
case int:
numValue = float64(v)
case int64:
numValue = float64(v)
case json.Number:
f, ok := parseFloat64(v.String())
if !ok {
return nil // not a number, skip validation

Check warning on line 573 in cmd/service/service.go

View check run for this annotation

Codecov / codecov/patch

cmd/service/service.go#L568-L573

Added lines #L568 - L573 were not covered by tests
}
numValue = f

Check warning on line 575 in cmd/service/service.go

View check run for this annotation

Codecov / codecov/patch

cmd/service/service.go#L575

Added line #L575 was not covered by tests
case string:
f, ok := parseFloat64(v)
if !ok {
return nil // not a number, skip validation
}
numValue = f
default:
return nil // non-numeric type, skip validation

Check warning on line 583 in cmd/service/service.go

View check run for this annotation

Codecov / codecov/patch

cmd/service/service.go#L582-L583

Added lines #L582 - L583 were not covered by tests
}

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
}

func parseFloat64(value string) (float64, bool) {
result, err := strconv.ParseFloat(value, 64)
return result, err == nil
}
175 changes: 175 additions & 0 deletions cmd/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Loading