diff --git a/AGENTS.md b/AGENTS.md index 6e2b38d45..87c6892b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,6 +105,20 @@ Signatures that are easy to guess wrong: Program output (JSON envelopes) goes to stdout. Progress, warnings, hints go to stderr. Mixing them corrupts pipe chains. +### Typed data over loose maps + +Parse `map[string]interface{}` into a typed struct at the boundary — one projection function per shape — and let everything downstream consume struct fields, not string keys. A typo'd map key compiles fine and fails at runtime, which an agent then debugs blind. + +Use distinct types when two values could be swapped silently: see `internal/meta.Token` — a bare string compiles on either side of a string/string signature, a distinct type does not. + +Legacy loose-map code exists in older paths. Match its call sites when touching it, but do not copy the pattern into new code. + +### Transcribe faithfully — no silent fallbacks + +When code echoes input onward (request previews, transformations, proxies), transcribe verbatim. A `default:` branch that coerces unrecognized input into a plausible value ("unknown HTTP verb → GET") makes the output lie, and an agent reasons from the lie. + +The same rule applies to flag combinations and internal wiring: if a requested option cannot be honored, return a typed validation error — never silently substitute another behavior and exit 0. Silent guesses (defaulting a missing identity, discarding writes on a nil writer) are bugs even when every current caller happens to avoid them. + ### Use `vfs.*` instead of `os.*` All filesystem access goes through `internal/vfs`. This enables test mocking. @@ -116,6 +130,7 @@ CLI arguments are untrusted (they come from AI agents). Call `validate.SafeInput ### Tests - Every behavior change needs a test alongside the change. +- A contract test must fail if the implementation is reverted. If you can undo the code change and the suite stays green, the contract is not pinned — assert the new field/behavior directly, not a happy-path substring. - `cmdutil.TestFactory(t, config)` for test factories. - `t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())` to isolate config state. diff --git a/cmd/api/api.go b/cmd/api/api.go index 20ab7bdad..368fd6cfd 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -130,6 +130,13 @@ func buildAPIRequest(opts *APIOptions) (client.RawApiRequest, *cmdutil.FileUploa stdin := opts.Factory.IOStreams.In fileIO := opts.Factory.ResolveFileIO(opts.Ctx) + if opts.Method == "" { + return client.RawApiRequest{}, nil, errs.NewValidationError(errs.SubtypeInvalidArgument, + "HTTP method must not be empty"). + WithHint("pass the verb as the first argument, e.g. lark-cli api GET /open-apis/..."). + WithParam("") + } + // Validate --file mutual exclusions first. if err := cmdutil.ValidateFileFlag(opts.File, opts.Params, opts.Data, opts.Output, opts.PageAll, opts.Method); err != nil { return client.RawApiRequest{}, nil, err @@ -243,9 +250,9 @@ func apiRun(opts *APIOptions) error { if opts.DryRun { if fileMeta != nil { - return cmdutil.PrintDryRunWithFile(f.IOStreams.Out, request, config, opts.Format, fileMeta.FieldName, fileMeta.FilePath, fileMeta.FormFields) + return cmdutil.PrintDryRunWithFile(request, config, dryRunOutputOptions(f, opts), *fileMeta) } - return apiDryRun(f, request, config, opts.Format) + return apiDryRun(f, request, config, opts) } // Identity info is now included in the JSON envelope; skip stderr printing. // cmdutil.PrintIdentity(f.IOStreams.ErrOut, opts.As, config, f.IdentityAutoDetected) @@ -297,8 +304,19 @@ func apiRun(opts *APIOptions) error { return nil } -func apiDryRun(f *cmdutil.Factory, request client.RawApiRequest, config *core.CliConfig, format string) error { - return cmdutil.PrintDryRun(f.IOStreams.Out, request, config, format) +func apiDryRun(f *cmdutil.Factory, request client.RawApiRequest, config *core.CliConfig, opts *APIOptions) error { + return cmdutil.PrintDryRun(request, config, dryRunOutputOptions(f, opts)) +} + +func dryRunOutputOptions(f *cmdutil.Factory, opts *APIOptions) cmdutil.DryRunOutputOptions { + return cmdutil.DryRunOutputOptions{ + Format: opts.Format, + JqExpr: opts.JqExpr, + CommandPath: opts.Cmd.CommandPath(), + Identity: opts.As, + Out: f.IOStreams.Out, + ErrOut: f.IOStreams.ErrOut, + } } func apiPaginate(ctx context.Context, ac *client.APIClient, request client.RawApiRequest, format output.Format, jqExpr string, out, errOut io.Writer, commandPath string, pagOpts client.PaginationOptions) error { diff --git a/cmd/api/api_test.go b/cmd/api/api_test.go index 1ea4c99f2..ae0628f95 100644 --- a/cmd/api/api_test.go +++ b/cmd/api/api_test.go @@ -69,7 +69,7 @@ func TestApiCmd_FlagParsing(t *testing.T) { } func TestApiCmd_DryRun(t *testing.T) { - f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{ + f, stdout, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{ AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, }) @@ -79,12 +79,42 @@ func TestApiCmd_DryRun(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - output := stdout.String() - if !strings.Contains(output, "Dry Run") { - t.Error("expected dry run output") + var got map[string]interface{} + if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { + t.Fatalf("dry-run stdout is not JSON: %v\nstdout:\n%s\nstderr:\n%s", err, stdout.String(), stderr.String()) } - if !strings.Contains(output, "/open-apis/test") { - t.Error("expected path in dry run output") + if got["ok"] != true || got["identity"] != "bot" || got["dry_run"] != true { + t.Fatalf("unexpected dry-run envelope: %#v", got) + } + data, ok := got["data"].(map[string]interface{}) + if !ok { + t.Fatalf("data = %#v, want object", got["data"]) + } + api, ok := data["api"].([]interface{}) + if !ok || len(api) != 1 { + t.Fatalf("api = %#v, want one call", data["api"]) + } + call, ok := api[0].(map[string]interface{}) + if !ok || call["url"] != "/open-apis/test" { + t.Fatalf("api[0] = %#v", api[0]) + } + if strings.Contains(stdout.String(), "=== Dry Run ===") { + t.Fatalf("stdout should not contain dry-run banner: %s", stdout.String()) + } +} + +func TestApiCmd_DryRunWithJq(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{ + AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, + }) + + cmd := newTestApiCmd(f, nil) + cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--dry-run", "--jq", ".data.api[0].url"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := strings.TrimSpace(stdout.String()); got != "/open-apis/test" { + t.Fatalf("jq output = %q, want /open-apis/test", got) } } @@ -152,6 +182,22 @@ func TestApiCmd_MissingArgs(t *testing.T) { } } +func TestApiCmd_EmptyMethodRejected(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{ + AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, + }) + + cmd := newTestApiCmd(f, nil) + cmd.SetArgs([]string{"", "/open-apis/test", "--as", "bot", "--dry-run"}) + err := cmd.Execute() + if err == nil { + t.Fatal("expected validation error for empty HTTP method") + } + if !strings.Contains(err.Error(), "method") { + t.Fatalf("error should name the method argument, got: %v", err) + } +} + func TestApiCmd_InvalidParamsJSON(t *testing.T) { f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{ AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, @@ -1000,11 +1046,23 @@ func TestApiCmd_DryRunWithFile(t *testing.T) { t.Fatalf("unexpected error: %v", err) } out := stdout.String() - if !strings.Contains(out, "image") { - t.Errorf("expected dry-run output to mention file field, got: %s", out) + var env map[string]interface{} + if err := json.Unmarshal(stdout.Bytes(), &env); err != nil { + t.Fatalf("dry-run stdout is not JSON: %v\n%s", err, out) } - if !strings.Contains(out, "Dry Run") { - t.Errorf("expected dry-run header, got: %s", out) + if env["dry_run"] != true { + t.Fatalf("dry_run = %#v, want true", env["dry_run"]) + } + data := env["data"].(map[string]interface{}) + api := data["api"].([]interface{}) + call := api[0].(map[string]interface{}) + body := call["body"].(map[string]interface{}) + file := body["file"].(map[string]interface{}) + if file["field"] != "image" || file["path"] != tmpFile { + t.Fatalf("unexpected file dry-run body: %#v", body) + } + if strings.Contains(out, "=== Dry Run ===") { + t.Fatalf("stdout should not contain dry-run banner: %s", out) } } diff --git a/cmd/service/service.go b/cmd/service/service.go index 3cb6ab5d2..f08a25249 100644 --- a/cmd/service/service.go +++ b/cmd/service/service.go @@ -403,9 +403,9 @@ func serviceMethodRun(opts *ServiceMethodOptions) error { if opts.DryRun { if fileMeta != nil { - return cmdutil.PrintDryRunWithFile(f.IOStreams.Out, request, config, opts.Format, fileMeta.FieldName, fileMeta.FilePath, fileMeta.FormFields) + return cmdutil.PrintDryRunWithFile(request, config, serviceDryRunOutputOptions(f, opts), *fileMeta) } - return serviceDryRun(f, request, config, opts.Format) + return serviceDryRun(f, request, config, opts) } if opts.Method.Risk == cmdutil.RiskHighRiskWrite { @@ -667,8 +667,19 @@ func buildServiceRequest(opts *ServiceMethodOptions) (client.RawApiRequest, *cmd return request, nil, nil } -func serviceDryRun(f *cmdutil.Factory, request client.RawApiRequest, config *core.CliConfig, format string) error { - return cmdutil.PrintDryRun(f.IOStreams.Out, request, config, format) +func serviceDryRun(f *cmdutil.Factory, request client.RawApiRequest, config *core.CliConfig, opts *ServiceMethodOptions) error { + return cmdutil.PrintDryRun(request, config, serviceDryRunOutputOptions(f, opts)) +} + +func serviceDryRunOutputOptions(f *cmdutil.Factory, opts *ServiceMethodOptions) cmdutil.DryRunOutputOptions { + return cmdutil.DryRunOutputOptions{ + Format: opts.Format, + JqExpr: opts.JqExpr, + CommandPath: opts.Cmd.CommandPath(), + Identity: opts.As, + Out: f.IOStreams.Out, + ErrOut: f.IOStreams.ErrOut, + } } func servicePaginate(ctx context.Context, ac *client.APIClient, request client.RawApiRequest, format output.Format, jqExpr string, out, errOut io.Writer, commandPath string, pagOpts client.PaginationOptions, checkErr func(interface{}, core.Identity) error) error { diff --git a/cmd/service/service_test.go b/cmd/service/service_test.go index 1eb6260b9..79df1e6c5 100644 --- a/cmd/service/service_test.go +++ b/cmd/service/service_test.go @@ -224,13 +224,39 @@ func TestServiceMethod_DryRun_PathParam(t *testing.T) { if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error: %v", err) } - if !strings.Contains(stdout.String(), tt.wantInURL) { - t.Errorf("expected URL containing %q, got:\n%s", tt.wantInURL, stdout.String()) + var got map[string]interface{} + if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { + t.Fatalf("dry-run stdout is not JSON: %v\n%s", err, stdout.String()) + } + if got["ok"] != true || got["dry_run"] != true { + t.Fatalf("unexpected dry-run envelope: %#v", got) + } + data := got["data"].(map[string]interface{}) + api := data["api"].([]interface{}) + call := api[0].(map[string]interface{}) + if call["url"] != tt.wantInURL { + t.Errorf("url = %q, want %q\nstdout:\n%s", call["url"], tt.wantInURL, stdout.String()) } }) } } +func TestServiceMethod_DryRunWithJq(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, testConfig) + cmd := NewCmdServiceMethod(f, driveSpec(), driveMethod("GET", nil), "get", "files", nil) + cmd.SetArgs([]string{ + "--params", `{"file_token":"boxcn123abc"}`, + "--dry-run", + "--jq", ".data.api[0].url", + }) + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got, want := strings.TrimSpace(stdout.String()), "/open-apis/drive/v1/files/boxcn123abc/copy"; got != want { + t.Fatalf("jq output = %q, want %q", got, want) + } +} + func TestServiceMethod_PathParamRejectsTraversal(t *testing.T) { tests := []struct { name string @@ -318,8 +344,12 @@ func TestServiceMethod_PaginationParamSkippedWithPageAll(t *testing.T) { if err != nil { t.Fatalf("expected no error with --page-all skipping page_size, got: %v", err) } - if !strings.Contains(stdout.String(), "Dry Run") { - t.Error("expected dry-run output") + var got map[string]interface{} + if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { + t.Fatalf("dry-run stdout is not JSON: %v\n%s", err, stdout.String()) + } + if got["dry_run"] != true { + t.Fatalf("dry_run = %#v, want true", got["dry_run"]) } } @@ -1081,11 +1111,23 @@ func TestServiceMethod_FileUpload_DryRun(t *testing.T) { t.Fatalf("unexpected error: %v", err) } out := stdout.String() - if !strings.Contains(out, "image") { - t.Errorf("expected dry-run output to mention file field, got: %s", out) + var env map[string]interface{} + if err := json.Unmarshal(stdout.Bytes(), &env); err != nil { + t.Fatalf("dry-run stdout is not JSON: %v\n%s", err, out) } - if !strings.Contains(out, "Dry Run") { - t.Errorf("expected dry-run header, got: %s", out) + if env["dry_run"] != true { + t.Fatalf("dry_run = %#v, want true", env["dry_run"]) + } + data := env["data"].(map[string]interface{}) + api := data["api"].([]interface{}) + call := api[0].(map[string]interface{}) + body := call["body"].(map[string]interface{}) + file := body["file"].(map[string]interface{}) + if file["field"] != "image" || file["path"] != tmpFile { + t.Fatalf("unexpected file dry-run body: %#v", body) + } + if strings.Contains(out, "=== Dry Run ===") { + t.Fatalf("stdout should not contain dry-run banner: %s", out) } } diff --git a/internal/cmdutil/dryrun.go b/internal/cmdutil/dryrun.go index f047d3fb2..4afa6f75f 100644 --- a/internal/cmdutil/dryrun.go +++ b/internal/cmdutil/dryrun.go @@ -8,15 +8,29 @@ import ( "fmt" "io" "net/url" + "regexp" "sort" "strings" + "github.com/larksuite/cli/errs" "github.com/larksuite/cli/internal/client" "github.com/larksuite/cli/internal/core" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/internal/util" ) +var dryRunURLPlaceholderRE = regexp.MustCompile(`:([A-Za-z_][A-Za-z0-9_]*)`) + +// DryRunOutputOptions controls dry-run stdout/stderr rendering. +type DryRunOutputOptions struct { + Format string + JqExpr string + CommandPath string + Identity core.Identity + Out io.Writer + ErrOut io.Writer +} + // DryRunAPICall describes a single API call in dry-run output. type DryRunAPICall struct { Desc string `json:"desc,omitempty"` @@ -26,12 +40,21 @@ type DryRunAPICall struct { Body interface{} `json:"body,omitempty"` } +// DryRunContext is the execution context shared by every dry-run preview: +// which app would make the call and, when known, as which user. The identity +// itself lives at the envelope top level, not here. +type DryRunContext struct { + AppID string `json:"app_id,omitempty"` + UserOpenID string `json:"user_open_id,omitempty"` +} + // DryRunAPI is the builder and result type for dry-run output. // URL templates use :param placeholders; Set stores actual values; MarshalJSON and Format resolve them. type DryRunAPI struct { - desc string - calls []DryRunAPICall - extra map[string]interface{} + desc string + calls []DryRunAPICall + context *DryRunContext + extra map[string]interface{} } func NewDryRunAPI() *DryRunAPI { @@ -40,30 +63,22 @@ func NewDryRunAPI() *DryRunAPI { // --- HTTP method builders (add a call, return self for chaining) --- -func (d *DryRunAPI) GET(url string) *DryRunAPI { - d.calls = append(d.calls, DryRunAPICall{Method: "GET", URL: url}) +// call appends a request with the method transcribed verbatim, so previews +// never misreport what the real client would send. +func (d *DryRunAPI) call(method, url string) *DryRunAPI { + d.calls = append(d.calls, DryRunAPICall{Method: method, URL: url}) return d } -func (d *DryRunAPI) POST(url string) *DryRunAPI { - d.calls = append(d.calls, DryRunAPICall{Method: "POST", URL: url}) - return d -} +func (d *DryRunAPI) GET(url string) *DryRunAPI { return d.call("GET", url) } -func (d *DryRunAPI) PUT(url string) *DryRunAPI { - d.calls = append(d.calls, DryRunAPICall{Method: "PUT", URL: url}) - return d -} +func (d *DryRunAPI) POST(url string) *DryRunAPI { return d.call("POST", url) } -func (d *DryRunAPI) DELETE(url string) *DryRunAPI { - d.calls = append(d.calls, DryRunAPICall{Method: "DELETE", URL: url}) - return d -} +func (d *DryRunAPI) PUT(url string) *DryRunAPI { return d.call("PUT", url) } -func (d *DryRunAPI) PATCH(url string) *DryRunAPI { - d.calls = append(d.calls, DryRunAPICall{Method: "PATCH", URL: url}) - return d -} +func (d *DryRunAPI) DELETE(url string) *DryRunAPI { return d.call("DELETE", url) } + +func (d *DryRunAPI) PATCH(url string) *DryRunAPI { return d.call("PATCH", url) } // Body sets the request body on the last added call. func (d *DryRunAPI) Body(body interface{}) *DryRunAPI { @@ -98,12 +113,26 @@ func (d *DryRunAPI) Set(key string, value interface{}) *DryRunAPI { return d } +// Context records the calling app/user under data.context; empty values are +// omitted, and a fully empty context is not emitted at all. +func (d *DryRunAPI) Context(appID, userOpenID string) *DryRunAPI { + if appID == "" && userOpenID == "" { + return d + } + d.context = &DryRunContext{AppID: appID, UserOpenID: userOpenID} + return d +} + // resolveURL replaces :key placeholders in url with path-escaped values from extra. func (d *DryRunAPI) resolveURL(rawURL string) string { - for k, v := range d.extra { - rawURL = strings.ReplaceAll(rawURL, ":"+k, url.PathEscape(fmt.Sprintf("%v", v))) - } - return rawURL + return dryRunURLPlaceholderRE.ReplaceAllStringFunc(rawURL, func(token string) string { + name := token[1:] + value, ok := d.extra[name] + if !ok { + return token + } + return url.PathEscape(fmt.Sprintf("%v", value)) + }) } // MarshalJSON serializes as {"description": "...", "api": [...calls with resolved URLs], ...extra}. @@ -118,13 +147,17 @@ func (d *DryRunAPI) MarshalJSON() ([]byte, error) { Body: c.Body, } } - m := make(map[string]interface{}, len(d.extra)+2) + m := make(map[string]interface{}, len(d.extra)+3) + for k, v := range d.extra { + m[k] = v + } + // Typed fields win over same-named extra keys. if d.desc != "" { m["description"] = d.desc } m["api"] = resolved - for k, v := range d.extra { - m[k] = v + if d.context != nil { + m["context"] = d.context } return json.Marshal(m) } @@ -154,11 +187,7 @@ func (d *DryRunAPI) Format() string { u += "?" + encodeParams(c.Params) } - method := c.Method - if method == "" { - method = "GET" - } - b.WriteString(method) + b.WriteString(c.Method) b.WriteByte(' ') b.WriteString(u) b.WriteByte('\n') @@ -215,83 +244,74 @@ func encodeParams(params map[string]interface{}) string { return vals.Encode() } -// PrintDryRunWithFile outputs a dry-run summary for file upload requests. -// Instead of serializing the Formdata body, it shows file metadata. -func PrintDryRunWithFile(w io.Writer, request client.RawApiRequest, config *core.CliConfig, format, fileField, filePath string, formFields any) error { - dr := NewDryRunAPI() - switch request.Method { - case "POST": - dr.POST(request.URL) - case "PUT": - dr.PUT(request.URL) - case "PATCH": - dr.PATCH(request.URL) - case "DELETE": - dr.DELETE(request.URL) - default: - dr.GET(request.URL) - } +// buildDryRunPreview assembles the shared preview skeleton: HTTP method, URL, +// query params, and the app/user context common to every dry-run. +func buildDryRunPreview(request client.RawApiRequest, config *core.CliConfig) *DryRunAPI { + dr := NewDryRunAPI().call(request.Method, request.URL) if len(request.Params) > 0 { dr.Params(request.Params) } - filePathDisplay := filePath + // Identity is reported at the envelope top level, not duplicated here. + dr.Context(config.AppID, config.UserOpenId) + return dr +} + +// PrintDryRunWithFile outputs a dry-run summary for file upload requests. +// Instead of serializing the Formdata body, it shows file metadata. +func PrintDryRunWithFile(request client.RawApiRequest, config *core.CliConfig, opts DryRunOutputOptions, file FileUploadMeta) error { + dr := buildDryRunPreview(request, config) + filePathDisplay := file.FilePath if filePathDisplay == "" { filePathDisplay = "" } fileInfo := map[string]any{ - "file": map[string]string{"field": fileField, "path": filePathDisplay}, + "file": map[string]string{"field": file.FieldName, "path": filePathDisplay}, } - if formFields != nil { - fileInfo["form_fields"] = formFields + if file.FormFields != nil { + fileInfo["form_fields"] = file.FormFields } fileInfo["options"] = []string{"WithFileUpload"} dr.Body(fileInfo) - dr.Set("as", string(request.As)) - dr.Set("appId", config.AppID) - if config.UserOpenId != "" { - dr.Set("userOpenId", config.UserOpenId) - } - fmt.Fprintln(w, "=== Dry Run ===") - if format == "pretty" { - fmt.Fprint(w, dr.Format()) - } else { - output.PrintJson(w, dr) - } - return nil + return WriteDryRun(dr, opts) } // PrintDryRun outputs a standardised dry-run summary using DryRunAPI. // When format is "pretty", outputs human-readable text; otherwise JSON. -func PrintDryRun(w io.Writer, request client.RawApiRequest, config *core.CliConfig, format string) error { - dr := NewDryRunAPI() - switch request.Method { - case "POST": - dr.POST(request.URL) - case "PUT": - dr.PUT(request.URL) - case "PATCH": - dr.PATCH(request.URL) - case "DELETE": - dr.DELETE(request.URL) - default: - dr.GET(request.URL) - } - if len(request.Params) > 0 { - dr.Params(request.Params) - } +func PrintDryRun(request client.RawApiRequest, config *core.CliConfig, opts DryRunOutputOptions) error { + dr := buildDryRunPreview(request, config) if !util.IsNil(request.Data) { dr.Body(request.Data) } - dr.Set("as", string(request.As)) - dr.Set("appId", config.AppID) - if config.UserOpenId != "" { - dr.Set("userOpenId", config.UserOpenId) - } - fmt.Fprintln(w, "=== Dry Run ===") - if format == "pretty" { - fmt.Fprint(w, dr.Format()) - } else { - output.PrintJson(w, dr) - } - return nil + return WriteDryRun(dr, opts) +} + +// WriteDryRun emits a DryRunAPI using the shared dry-run output contract. +// Identity may be empty; the envelope omits it rather than guessing. +func WriteDryRun(dr *DryRunAPI, opts DryRunOutputOptions) error { + if dr == nil { + return errs.NewInternalError(errs.SubtypeUnknown, "dry-run produced no request preview") + } + // The JqExpr guard is defensive: every entry point already rejects --jq + // combined with --format pretty via output.ValidateJqFlags. + if opts.Format == "pretty" && opts.JqExpr == "" { + // A nil ErrOut only skips the banner decoration (mirroring + // WriteSuccessEnvelope's warning path); the payload write to Out + // must fail loudly rather than be silently discarded. + if opts.ErrOut != nil { + fmt.Fprintln(opts.ErrOut, "=== Dry Run ===") + } + // stdout carries its own marker so logs that drop stderr still show + // this was a preview, not an executed request. + fmt.Fprintln(opts.Out, "# dry-run: request not sent") + fmt.Fprint(opts.Out, dr.Format()) + return nil + } + return output.WriteSuccessEnvelope(dr, output.SuccessEnvelopeOptions{ + CommandPath: opts.CommandPath, + Identity: string(opts.Identity), + DryRun: true, + JqExpr: opts.JqExpr, + Out: opts.Out, + ErrOut: opts.ErrOut, + }) } diff --git a/internal/cmdutil/dryrun_test.go b/internal/cmdutil/dryrun_test.go index 35470d577..6056bce18 100644 --- a/internal/cmdutil/dryrun_test.go +++ b/internal/cmdutil/dryrun_test.go @@ -6,9 +6,12 @@ package cmdutil import ( "bytes" "encoding/json" + "errors" + "io" "strings" "testing" + "github.com/larksuite/cli/errs" "github.com/larksuite/cli/internal/client" "github.com/larksuite/cli/internal/core" ) @@ -66,11 +69,31 @@ func TestDryRunAPI_ResolveURL(t *testing.T) { } } +func TestDryRunAPI_ResolveURLMatchesFullPlaceholderOnly(t *testing.T) { + dr := NewDryRunAPI(). + GET("/open-apis/task/v2/tasks/:assignee_id"). + Set("assignee", "ou_bot") + + text := dr.Format() + if strings.Contains(text, "ou_bot_id") { + t.Fatalf("prefix placeholder key corrupted longer token: %s", text) + } + if !strings.Contains(text, ":assignee_id") { + t.Fatalf("missing unresolved placeholder, got: %s", text) + } + + dr.Set("assignee_id", "ou_abc/123") + text = dr.Format() + if !strings.Contains(text, "/open-apis/task/v2/tasks/ou_abc%2F123") { + t.Fatalf("expected full placeholder replacement with path escaping, got: %s", text) + } +} + func TestDryRunAPI_MarshalJSON(t *testing.T) { dr := NewDryRunAPI(). Desc("test api"). GET("/open-apis/test"). - Set("as", "user") + Set("note", "audit") data, err := json.Marshal(dr) if err != nil { @@ -83,8 +106,8 @@ func TestDryRunAPI_MarshalJSON(t *testing.T) { if m["description"] != "test api" { t.Errorf("expected description, got: %v", m["description"]) } - if m["as"] != "user" { - t.Errorf("expected as=user, got: %v", m["as"]) + if m["note"] != "audit" { + t.Errorf("expected note=audit, got: %v", m["note"]) } api, ok := m["api"].([]interface{}) if !ok || len(api) != 1 { @@ -123,31 +146,67 @@ func TestDryRunAPI_ExtraFieldsOnly(t *testing.T) { func TestPrintDryRun_JSON(t *testing.T) { var buf bytes.Buffer - err := PrintDryRun(&buf, client.RawApiRequest{ + var errBuf bytes.Buffer + err := PrintDryRun(client.RawApiRequest{ Method: "GET", URL: "/open-apis/test", As: "user", - }, &core.CliConfig{AppID: "app123"}, "json") + }, &core.CliConfig{AppID: "app123"}, DryRunOutputOptions{ + Format: "json", + CommandPath: "lark-cli api", + Identity: core.AsUser, + Out: &buf, + ErrOut: &errBuf, + }) if err != nil { t.Fatalf("PrintDryRun failed: %v", err) } out := buf.String() - if !strings.Contains(out, "=== Dry Run ===") { - t.Errorf("expected header, got: %s", out) + if strings.Contains(out, "=== Dry Run ===") { + t.Fatalf("JSON stdout must not contain banner, got: %s", out) } - if !strings.Contains(out, "app123") { - t.Errorf("expected appId in output, got: %s", out) + var env map[string]interface{} + if err := json.Unmarshal(buf.Bytes(), &env); err != nil { + t.Fatalf("dry-run stdout is not JSON: %v\n%s", err, out) + } + if env["ok"] != true || env["identity"] != "user" || env["dry_run"] != true { + t.Fatalf("unexpected envelope: %#v", env) + } + data, ok := env["data"].(map[string]interface{}) + if !ok { + t.Fatalf("unexpected data: %#v", env["data"]) + } + dctx, ok := data["context"].(map[string]interface{}) + if !ok || dctx["app_id"] != "app123" { + t.Fatalf("unexpected data.context: %#v", data["context"]) + } + if _, exists := data["as"]; exists { + t.Fatalf("data.as must not appear; identity lives at the envelope top level: %#v", data) + } + api, ok := data["api"].([]interface{}) + if !ok || len(api) != 1 { + t.Fatalf("api = %#v, want one call", data["api"]) + } + call, ok := api[0].(map[string]interface{}) + if !ok || call["url"] != "/open-apis/test" { + t.Fatalf("api[0] = %#v", api[0]) } } func TestPrintDryRun_Pretty(t *testing.T) { var buf bytes.Buffer - err := PrintDryRun(&buf, client.RawApiRequest{ + var errBuf bytes.Buffer + err := PrintDryRun(client.RawApiRequest{ Method: "POST", URL: "/open-apis/test", Data: map[string]interface{}{"key": "val"}, As: "bot", - }, &core.CliConfig{AppID: "app456"}, "pretty") + }, &core.CliConfig{AppID: "app456"}, DryRunOutputOptions{ + Format: "pretty", + Identity: core.AsBot, + Out: &buf, + ErrOut: &errBuf, + }) if err != nil { t.Fatalf("PrintDryRun failed: %v", err) } @@ -155,6 +214,136 @@ func TestPrintDryRun_Pretty(t *testing.T) { if !strings.Contains(out, "POST /open-apis/test") { t.Errorf("expected POST line in pretty output, got: %s", out) } + if !strings.HasPrefix(out, "# dry-run: request not sent\n") { + t.Fatalf("pretty stdout should start with the dry-run marker, got: %s", out) + } + if strings.Contains(out, "=== Dry Run ===") { + t.Fatalf("pretty stdout must not contain banner, got: %s", out) + } + if !strings.Contains(errBuf.String(), "=== Dry Run ===") { + t.Fatalf("pretty stderr should contain banner, got: %s", errBuf.String()) + } +} + +func TestPrintDryRun_WithJqUsesEnvelope(t *testing.T) { + var buf bytes.Buffer + err := PrintDryRun(client.RawApiRequest{ + Method: "GET", + URL: "/open-apis/test", + As: "bot", + }, &core.CliConfig{AppID: "app123"}, DryRunOutputOptions{ + Format: "json", + JqExpr: ".data.api[0].url", + Identity: core.AsBot, + Out: &buf, + ErrOut: io.Discard, + }) + if err != nil { + t.Fatalf("PrintDryRun failed: %v", err) + } + if got := strings.TrimSpace(buf.String()); got != "/open-apis/test" { + t.Fatalf("jq output = %q, want /open-apis/test", got) + } +} + +func TestPrintDryRunWithFile_JSONEnvelope(t *testing.T) { + var buf bytes.Buffer + err := PrintDryRunWithFile(client.RawApiRequest{ + Method: "POST", + URL: "/open-apis/drive/v1/files/upload_all", + As: "bot", + }, &core.CliConfig{AppID: "app123", UserOpenId: "ou_tester"}, DryRunOutputOptions{ + Format: "json", + Identity: core.AsBot, + Out: &buf, + ErrOut: io.Discard, + }, FileUploadMeta{FieldName: "file", FilePath: "report.txt", FormFields: map[string]any{"parent": "fld"}}) + if err != nil { + t.Fatalf("PrintDryRunWithFile failed: %v", err) + } + var env map[string]interface{} + if err := json.Unmarshal(buf.Bytes(), &env); err != nil { + t.Fatalf("dry-run stdout is not JSON: %v\n%s", err, buf.String()) + } + if env["dry_run"] != true { + t.Fatalf("dry_run = %#v, want true", env["dry_run"]) + } + data := env["data"].(map[string]interface{}) + api := data["api"].([]interface{}) + call := api[0].(map[string]interface{}) + body := call["body"].(map[string]interface{}) + file := body["file"].(map[string]interface{}) + if file["path"] != "report.txt" { + t.Fatalf("file body = %#v", body) + } + dctx, ok := data["context"].(map[string]interface{}) + if !ok || dctx["app_id"] != "app123" || dctx["user_open_id"] != "ou_tester" { + t.Fatalf("unexpected data.context: %#v", data["context"]) + } + for _, legacy := range []string{"as", "appId", "userOpenId"} { + if _, exists := data[legacy]; exists { + t.Fatalf("legacy key %q must not appear in data: %#v", legacy, data) + } + } +} + +func TestPrintDryRun_MethodTranscribedVerbatim(t *testing.T) { + var buf bytes.Buffer + err := PrintDryRun(client.RawApiRequest{ + Method: "OPTIONS", + URL: "/open-apis/test", + As: "bot", + }, &core.CliConfig{AppID: "app123"}, DryRunOutputOptions{ + Format: "json", + Identity: core.AsBot, + Out: &buf, + ErrOut: io.Discard, + }) + if err != nil { + t.Fatalf("PrintDryRun failed: %v", err) + } + var env map[string]interface{} + if err := json.Unmarshal(buf.Bytes(), &env); err != nil { + t.Fatalf("dry-run stdout is not JSON: %v\n%s", err, buf.String()) + } + call := env["data"].(map[string]interface{})["api"].([]interface{})[0].(map[string]interface{}) + if call["method"] != "OPTIONS" { + t.Fatalf("method = %#v, want OPTIONS transcribed verbatim (not coerced to GET)", call["method"]) + } +} + +func TestPrintDryRun_EmptyConfigOmitsContext(t *testing.T) { + var buf bytes.Buffer + err := PrintDryRun(client.RawApiRequest{ + Method: "GET", + URL: "/open-apis/test", + }, &core.CliConfig{}, DryRunOutputOptions{ + Format: "json", + Out: &buf, + ErrOut: io.Discard, + }) + if err != nil { + t.Fatalf("PrintDryRun failed: %v", err) + } + var env map[string]interface{} + if err := json.Unmarshal(buf.Bytes(), &env); err != nil { + t.Fatalf("dry-run stdout is not JSON: %v\n%s", err, buf.String()) + } + data := env["data"].(map[string]interface{}) + if _, exists := data["context"]; exists { + t.Fatalf("empty app/user context must be omitted entirely, got: %#v", data["context"]) + } +} + +func TestWriteDryRun_NilPreviewIsInternalError(t *testing.T) { + err := WriteDryRun(nil, DryRunOutputOptions{Format: "json", Out: io.Discard}) + if err == nil { + t.Fatal("WriteDryRun(nil) should fail instead of emitting an empty preview") + } + var internal *errs.InternalError + if !errors.As(err, &internal) { + t.Fatalf("expected *errs.InternalError, got %T: %v", err, err) + } } func TestDryRunFormatValue(t *testing.T) { diff --git a/internal/output/envelope.go b/internal/output/envelope.go index d25730133..b62fc6d69 100644 --- a/internal/output/envelope.go +++ b/internal/output/envelope.go @@ -7,6 +7,7 @@ package output type Envelope struct { OK bool `json:"ok"` Identity string `json:"identity,omitempty"` + DryRun bool `json:"dry_run,omitempty"` Data interface{} `json:"data,omitempty"` Meta *Meta `json:"meta,omitempty"` ContentSafetyAlert interface{} `json:"_content_safety_alert,omitempty"` diff --git a/internal/output/envelope_success.go b/internal/output/envelope_success.go index 08649c336..e54802a07 100644 --- a/internal/output/envelope_success.go +++ b/internal/output/envelope_success.go @@ -9,6 +9,7 @@ import "io" type SuccessEnvelopeOptions struct { CommandPath string Identity string + DryRun bool JqExpr string Out io.Writer ErrOut io.Writer @@ -41,6 +42,7 @@ func WriteSuccessEnvelope(data interface{}, opts SuccessEnvelopeOptions) error { env := Envelope{ OK: true, Identity: opts.Identity, + DryRun: opts.DryRun, Data: data, Notice: GetNotice(), } diff --git a/internal/output/envelope_success_test.go b/internal/output/envelope_success_test.go index dac7c17f1..fecfb1889 100644 --- a/internal/output/envelope_success_test.go +++ b/internal/output/envelope_success_test.go @@ -104,6 +104,47 @@ func TestWriteSuccessEnvelope_JqUsesEnvelope(t *testing.T) { } } +func TestWriteSuccessEnvelope_DryRunMarker(t *testing.T) { + var out strings.Builder + + err := WriteSuccessEnvelope(map[string]interface{}{"api": []interface{}{}}, SuccessEnvelopeOptions{ + Identity: "bot", + DryRun: true, + Out: &out, + }) + if err != nil { + t.Fatalf("WriteSuccessEnvelope() error = %v", err) + } + + var env map[string]interface{} + if err := json.Unmarshal([]byte(out.String()), &env); err != nil { + t.Fatalf("invalid JSON output: %v\n%s", err, out.String()) + } + if env["ok"] != true || env["identity"] != "bot" || env["dry_run"] != true { + t.Fatalf("unexpected dry-run envelope: %#v", env) + } + if _, ok := env["data"].(map[string]interface{}); !ok { + t.Fatalf("data = %#v, want object", env["data"]) + } +} + +func TestWriteSuccessEnvelope_DryRunJqUsesEnvelope(t *testing.T) { + var out strings.Builder + + err := WriteSuccessEnvelope(map[string]interface{}{"api": []interface{}{}}, SuccessEnvelopeOptions{ + Identity: "bot", + DryRun: true, + JqExpr: ".dry_run", + Out: &out, + }) + if err != nil { + t.Fatalf("WriteSuccessEnvelope() error = %v", err) + } + if strings.TrimSpace(out.String()) != "true" { + t.Fatalf("jq output = %q, want true", out.String()) + } +} + func TestWriteSuccessEnvelope_JqWarnsWhenSafetyAlertFiltered(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONTENT_SAFETY_MODE", "warn") extcs.Register(&mockProvider{ diff --git a/internal/qualitygate/rules/dryrun.go b/internal/qualitygate/rules/dryrun.go index f4ac3d56c..18a76dfe0 100644 --- a/internal/qualitygate/rules/dryrun.go +++ b/internal/qualitygate/rules/dryrun.go @@ -878,16 +878,23 @@ func extractDryRunJSON(raw []byte) (facts.DryRunRequest, int, error) { var firstErr error for start >= 0 { var preview struct { - API []facts.DryRunRequest `json:"api"` + API []facts.DryRunRequest `json:"api"` + Data struct { + API []facts.DryRunRequest `json:"api"` + } `json:"data"` } dec := json.NewDecoder(bytes.NewReader(raw[start:])) if err := dec.Decode(&preview); err == nil { - if len(preview.API) == 0 { + api := preview.API + if len(api) == 0 { + api = preview.Data.API + } + if len(api) == 0 { if firstErr == nil { firstErr = errNoDryRunAPI } } else { - return preview.API[0], len(preview.API), nil + return api[0], len(api), nil } } else if firstErr == nil { firstErr = err diff --git a/internal/qualitygate/rules/dryrun_test.go b/internal/qualitygate/rules/dryrun_test.go index 8c109ca48..7a538ffda 100644 --- a/internal/qualitygate/rules/dryrun_test.go +++ b/internal/qualitygate/rules/dryrun_test.go @@ -33,6 +33,17 @@ func TestExtractDryRunJSONSkipsBanner(t *testing.T) { } } +func TestExtractDryRunJSONReadsSuccessEnvelope(t *testing.T) { + raw := `{"ok":true,"dry_run":true,"data":{"api":[{"method":"GET","url":"/open-apis/test"}]}}` + got, apiCallCount, err := extractDryRunJSON([]byte(raw)) + if err != nil { + t.Fatalf("extractDryRunJSON() error = %v", err) + } + if got.Method != "GET" || got.URL != "/open-apis/test" || apiCallCount != 1 { + t.Fatalf("got request=%#v apiCallCount=%d, want enveloped GET and count 1", got, apiCallCount) + } +} + func TestExtractDryRunJSONSkipsBannerWithBraces(t *testing.T) { raw := "banner {not json}\n{\"api\":[{\"method\":\"GET\",\"url\":\"/open-apis/test\"}]}\n" got, apiCallCount, err := extractDryRunJSON([]byte(raw)) diff --git a/shortcuts/application/slash_command_delete_test.go b/shortcuts/application/slash_command_delete_test.go index 9c8ee4597..38e1b004d 100644 --- a/shortcuts/application/slash_command_delete_test.go +++ b/shortcuts/application/slash_command_delete_test.go @@ -81,16 +81,19 @@ func TestSlashCommandDelete_ByNameDryRun(t *testing.T) { if err != nil { t.Fatalf("execute: %v", err) } - var got struct { - Description string `json:"description"` - API []struct { - Desc string `json:"desc"` - Method string `json:"method"` - } `json:"api"` + var envlp struct { + Data struct { + Description string `json:"description"` + API []struct { + Desc string `json:"desc"` + Method string `json:"method"` + } `json:"api"` + } `json:"data"` } - if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { + if err := json.Unmarshal(stdout.Bytes(), &envlp); err != nil { t.Fatalf("json: %v", err) } + got := envlp.Data if !strings.Contains(got.Description, "HIGH-RISK") || strings.Contains(got.Description, "resolve command_id") { t.Fatalf("top-level description must contain only the risk context: %q", got.Description) } diff --git a/shortcuts/application/slash_command_update_test.go b/shortcuts/application/slash_command_update_test.go index 2e5d912d2..07e4c5357 100644 --- a/shortcuts/application/slash_command_update_test.go +++ b/shortcuts/application/slash_command_update_test.go @@ -100,16 +100,19 @@ func TestSlashCommandUpdate_ByNameDryRunDescriptions(t *testing.T) { if err != nil { t.Fatalf("execute: %v", err) } - var got struct { - Description string `json:"description"` - API []struct { - Desc string `json:"desc"` - Method string `json:"method"` - } `json:"api"` + var envlp struct { + Data struct { + Description string `json:"description"` + API []struct { + Desc string `json:"desc"` + Method string `json:"method"` + } `json:"api"` + } `json:"data"` } - if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { + if err := json.Unmarshal(stdout.Bytes(), &envlp); err != nil { t.Fatalf("json: %v", err) } + got := envlp.Data if strings.Contains(got.Description, "resolve command_id") { t.Fatalf("resolve description must be attached to GET, not top-level: %q", got.Description) } @@ -128,15 +131,18 @@ func TestSlashCommandUpdate_ByIDDryRunEncodesTrimmedPathSegment(t *testing.T) { if err != nil { t.Fatalf("execute: %v", err) } - var got struct { - API []struct { - Desc string `json:"desc"` - URL string `json:"url"` - } `json:"api"` + var envlp struct { + Data struct { + API []struct { + Desc string `json:"desc"` + URL string `json:"url"` + } `json:"api"` + } `json:"data"` } - if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { + if err := json.Unmarshal(stdout.Bytes(), &envlp); err != nil { t.Fatalf("json: %v", err) } + got := envlp.Data wantURL := slashCommandBasePath + "/id%2Fwith%20space%3Fx" if len(got.API) != 1 || got.API[0].URL != wantURL || got.API[0].Desc == "" { t.Fatalf("dry-run call = %#v, want encoded URL %q with description", got.API, wantURL) diff --git a/shortcuts/apps/apps_analytics_test.go b/shortcuts/apps/apps_analytics_test.go index 3e8eeb5de..b8dc09b97 100644 --- a/shortcuts/apps/apps_analytics_test.go +++ b/shortcuts/apps/apps_analytics_test.go @@ -23,19 +23,21 @@ func TestAppsAnalyticsList_DryRunUsesNanoseconds(t *testing.T) { t.Fatalf("dry-run err=%v", err) } var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` + Data struct { + API []struct { + Method string `json:"method"` + URL string `json:"url"` + Body map[string]interface{} `json:"body"` + } `json:"api"` + } `json:"data"` } if err := json.Unmarshal(stdout.Bytes(), &env); err != nil { t.Fatalf("decode dry-run: %v\n%s", err, stdout.String()) } - if env.API[0].Method != "POST" || env.API[0].URL != "/open-apis/spark/v1/apps/app_x/query_analytics_data" { - t.Fatalf("method/url = %s %s", env.API[0].Method, env.API[0].URL) + if env.Data.API[0].Method != "POST" || env.Data.API[0].URL != "/open-apis/spark/v1/apps/app_x/query_analytics_data" { + t.Fatalf("method/url = %s %s", env.Data.API[0].Method, env.Data.API[0].URL) } - body := env.API[0].Body + body := env.Data.API[0].Body if _, ok := body["start_timestamp_ns"]; !ok { t.Fatalf("analytics dry-run missing start_timestamp_ns: %#v", body) } @@ -92,14 +94,16 @@ func TestAppsAnalyticsList_PageViewDesktopSeriesSetsDeviceFilter(t *testing.T) { t.Fatalf("dry-run err=%v", err) } var env struct { - API []struct { - Body map[string]interface{} `json:"body"` - } `json:"api"` + Data struct { + API []struct { + Body map[string]interface{} `json:"body"` + } `json:"api"` + } `json:"data"` } if err := json.Unmarshal(stdout.Bytes(), &env); err != nil { t.Fatalf("decode dry-run: %v\n%s", err, stdout.String()) } - filter := env.API[0].Body["filter"].(map[string]interface{}) + filter := env.Data.API[0].Body["filter"].(map[string]interface{}) deviceTypes := filter["device_types"].([]interface{}) if len(deviceTypes) != 1 || deviceTypes[0] != "desktop" { t.Fatalf("device_types = %#v", deviceTypes) diff --git a/shortcuts/apps/apps_db_audit_test.go b/shortcuts/apps/apps_db_audit_test.go index becf9b86f..a5ddb0af9 100644 --- a/shortcuts/apps/apps_db_audit_test.go +++ b/shortcuts/apps/apps_db_audit_test.go @@ -101,14 +101,16 @@ func TestAppsDBAuditEnable_DryRunAndSuccess(t *testing.T) { t.Fatalf("dry-run err=%v", err) } var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` + Data struct { + API []struct { + Method string `json:"method"` + URL string `json:"url"` + Body map[string]interface{} `json:"body"` + } `json:"api"` + } `json:"data"` } _ = json.Unmarshal([]byte(stdout.String()), &env) - a := env.API[0] + a := env.Data.API[0] if a.Method != "POST" || a.URL != dbAuditSetURL || a.Body["enabled"] != true || a.Body["retention"] != "30d" || a.Body["table"] != "orders" { t.Fatalf("dry-run = %s %s body=%v", a.Method, a.URL, a.Body) } @@ -136,13 +138,15 @@ func TestAppsDBAuditDisable_DryRunAndSuccess(t *testing.T) { t.Fatalf("dry-run err=%v", err) } var env struct { - API []struct { - Body map[string]interface{} `json:"body"` - } `json:"api"` + Data struct { + API []struct { + Body map[string]interface{} `json:"body"` + } `json:"api"` + } `json:"data"` } _ = json.Unmarshal([]byte(stdout.String()), &env) - if env.API[0].Body["enabled"] != false || env.API[0].Body["table"] != "orders" { - t.Fatalf("dry-run body=%v (want enabled:false)", env.API[0].Body) + if env.Data.API[0].Body["enabled"] != false || env.Data.API[0].Body["table"] != "orders" { + t.Fatalf("dry-run body=%v (want enabled:false)", env.Data.API[0].Body) } factory2, stdout2, reg := newAppsExecuteFactory(t) @@ -178,14 +182,16 @@ func TestAppsDBAuditList_DryRunJoinsTables(t *testing.T) { t.Fatalf("dry-run err=%v", err) } var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Params map[string]interface{} `json:"params"` - } `json:"api"` + Data struct { + API []struct { + Method string `json:"method"` + URL string `json:"url"` + Params map[string]interface{} `json:"params"` + } `json:"api"` + } `json:"data"` } _ = json.Unmarshal([]byte(stdout.String()), &env) - a := env.API[0] + a := env.Data.API[0] if a.Method != "GET" || a.URL != dbAuditListURL || a.Params["tables"] != "orders,users" { t.Fatalf("dry-run = %s %s tables=%v", a.Method, a.URL, a.Params["tables"]) } diff --git a/shortcuts/apps/apps_db_changelog_list_test.go b/shortcuts/apps/apps_db_changelog_list_test.go index a179b14e1..f515d318f 100644 --- a/shortcuts/apps/apps_db_changelog_list_test.go +++ b/shortcuts/apps/apps_db_changelog_list_test.go @@ -37,13 +37,7 @@ func TestAppsDBChangelogList_DryRunFiltersAndTimeNormalize(t *testing.T) { "--change-id", "01J", "--since", "2026-01-01", "--page-size", "5", "--dry-run", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Params map[string]interface{} `json:"params"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) a := env.API[0] if a.Method != "GET" || a.URL != dbChangelogURL { diff --git a/shortcuts/apps/apps_db_data_export_test.go b/shortcuts/apps/apps_db_data_export_test.go index f2c9121ac..aed3d02c4 100644 --- a/shortcuts/apps/apps_db_data_export_test.go +++ b/shortcuts/apps/apps_db_data_export_test.go @@ -71,13 +71,7 @@ func TestAppsDBDataExport_DryRunFormatFromOutput(t *testing.T) { if err := runAppsShortcut(t, AppsDBDataExport, args, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Params map[string]interface{} `json:"params"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) a := env.API[0] if a.Method != "GET" || a.URL != dbDataExportURL { diff --git a/shortcuts/apps/apps_db_data_import_test.go b/shortcuts/apps/apps_db_data_import_test.go index 2eb0388f6..ce3f8ffb2 100644 --- a/shortcuts/apps/apps_db_data_import_test.go +++ b/shortcuts/apps/apps_db_data_import_test.go @@ -97,14 +97,7 @@ func TestAppsDBDataImport_DryRunMultipartShape(t *testing.T) { []string{"+db-data-import", "--app-id", "app_x", "--file", "orders.csv", "--environment", "dev", "--dry-run", "--yes", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Params map[string]interface{} `json:"params"` - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) a := env.API[0] if a.Method != "POST" || a.URL != dbDataImportURL { @@ -131,12 +124,11 @@ func TestAppsDBDataImport_DryRunOmitsEnvWhenUnset(t *testing.T) { []string{"+db-data-import", "--app-id", "app_x", "--file", "orders.csv", "--dry-run", "--yes", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Params map[string]interface{} `json:"params"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) + if len(env.API) != 1 { + t.Fatalf("dry-run API calls = %d, want 1; stdout=%s", len(env.API), stdout.String()) + } p := env.API[0].Params if _, ok := p["env"]; ok { t.Fatalf("no --environment → env key must be omitted, got params=%v", p) @@ -174,11 +166,7 @@ func TestAppsDBDataImport_TableDefaultsToFileBasename(t *testing.T) { []string{"+db-data-import", "--app-id", "app_x", "--file", "customers.json", "--dry-run", "--yes", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Params map[string]interface{} `json:"params"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) if env.API[0].Params["table"] != "customers" { t.Fatalf("expected table=customers (from file basename) in params, got %v", env.API[0].Params) diff --git a/shortcuts/apps/apps_db_env_recovery_quota_test.go b/shortcuts/apps/apps_db_env_recovery_quota_test.go index 013f37bba..7fd97d2c3 100644 --- a/shortcuts/apps/apps_db_env_recovery_quota_test.go +++ b/shortcuts/apps/apps_db_env_recovery_quota_test.go @@ -30,13 +30,7 @@ func TestAppsDBEnvDiff_DryRunBody(t *testing.T) { []string{"+db-env-diff", "--app-id", "app_x", "--dry-run", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) a := env.API[0] if a.Method != "POST" || a.URL != dbEnvMigrateURL || a.Body["dry_run"] != true { @@ -91,11 +85,7 @@ func TestAppsDBEnvMigrate_DryRunBody(t *testing.T) { []string{"+db-env-migrate", "--app-id", "app_x", "--dry-run", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) if env.API[0].Body["dry_run"] != false { t.Fatalf("dry-run body=%v (want dry_run:false)", env.API[0].Body) @@ -180,13 +170,7 @@ func TestAppsDBRecoveryDiff_DryRunNormalizesTarget(t *testing.T) { []string{"+db-recovery-diff", "--app-id", "app_x", "--target", "2026-04-15", "--dry-run", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) a := env.API[0] if a.Method != "POST" || a.URL != dbRecoveryURL || a.Body["dry_run"] != true { @@ -331,14 +315,11 @@ func TestAppsDBQuotaGet_DryRunOmitsEnvWhenUnset(t *testing.T) { []string{"+db-quota-get", "--app-id", "app_x", "--dry-run", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Params map[string]interface{} `json:"params"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) + if len(env.API) != 1 { + t.Fatalf("dry-run API calls = %d, want 1; stdout=%s", len(env.API), stdout.String()) + } a := env.API[0] if a.Method != "GET" || a.URL != dbQuotaURL { t.Fatalf("dry-run = %s %s", a.Method, a.URL) diff --git a/shortcuts/apps/apps_db_execute_test.go b/shortcuts/apps/apps_db_execute_test.go index 7bb277e43..4df6763b8 100644 --- a/shortcuts/apps/apps_db_execute_test.go +++ b/shortcuts/apps/apps_db_execute_test.go @@ -165,14 +165,7 @@ func TestAppsDBExecute_DryRunSendsTransactionalFalse(t *testing.T) { factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - Params map[string]interface{} `json:"params"` - } `json:"api"` - } + var env dryRunAPIEnvelope if err := json.Unmarshal([]byte(stdout.String()), &env); err != nil { t.Fatalf("decode: %v\n%s", err, stdout.String()) } @@ -254,11 +247,7 @@ func TestAppsDBExecute_FileReadsSQLIntoBody(t *testing.T) { factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope if err := json.Unmarshal([]byte(stdout.String()), &env); err != nil { t.Fatalf("decode: %v\n%s", err, stdout.String()) } diff --git a/shortcuts/apps/apps_db_table_get_test.go b/shortcuts/apps/apps_db_table_get_test.go index bab56a848..954362b01 100644 --- a/shortcuts/apps/apps_db_table_get_test.go +++ b/shortcuts/apps/apps_db_table_get_test.go @@ -79,11 +79,7 @@ func TestAppsDBTableGet_NonPrettyFormatsOmitFormatQuery(t *testing.T) { if err := runAppsShortcut(t, AppsDBTableGet, args, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Params map[string]interface{} `json:"params"` - } `json:"api"` - } + var env dryRunAPIEnvelope if err := json.Unmarshal([]byte(stdout.String()), &env); err != nil { t.Fatalf("decode: %v", err) } diff --git a/shortcuts/apps/apps_db_table_list_test.go b/shortcuts/apps/apps_db_table_list_test.go index 9cdc12b43..d1cf6e531 100644 --- a/shortcuts/apps/apps_db_table_list_test.go +++ b/shortcuts/apps/apps_db_table_list_test.go @@ -165,13 +165,7 @@ func TestAppsDBTableList_DryRunSendsPaginationAndEnv(t *testing.T) { factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Params map[string]interface{} `json:"params"` - } `json:"api"` - } + var env dryRunAPIEnvelope if err := json.Unmarshal([]byte(stdout.String()), &env); err != nil { t.Fatalf("decode dry-run: %v\n%s", err, stdout.String()) } @@ -196,11 +190,7 @@ func TestAppsDBTableList_DoesNotSendIncludeStatsQuery(t *testing.T) { factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Params map[string]interface{} `json:"params"` - } `json:"api"` - } + var env dryRunAPIEnvelope if err := json.Unmarshal([]byte(stdout.String()), &env); err != nil { t.Fatalf("decode: %v", err) } diff --git a/shortcuts/apps/apps_env_test.go b/shortcuts/apps/apps_env_test.go index 4913bf5c4..aba1e400f 100644 --- a/shortcuts/apps/apps_env_test.go +++ b/shortcuts/apps/apps_env_test.go @@ -149,11 +149,7 @@ func TestAppsEnvVarList_DryRunIncludesScene(t *testing.T) { }, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var dryRun struct { - API []struct { - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var dryRun dryRunAPIEnvelope if err := json.Unmarshal(stdout.Bytes(), &dryRun); err != nil { t.Fatalf("decode dry-run: %v\n%s", err, stdout.String()) } @@ -228,11 +224,7 @@ func TestAppsEnvVarSet_OnlineDryRunDoesNotRequireYes(t *testing.T) { t.Fatalf("dry-run missing %q: %s", want, got) } } - var dryRun struct { - API []struct { - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var dryRun dryRunAPIEnvelope if err := json.Unmarshal([]byte(got), &dryRun); err != nil { t.Fatalf("decode dry-run: %v\n%s", err, got) } @@ -353,13 +345,7 @@ func TestAppsEnvVarDelete_OnlineDryRunDoesNotRequireYes(t *testing.T) { t.Fatalf("dry-run err=%v", err) } - var dryRun struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var dryRun dryRunAPIEnvelope got := stdout.String() if err := json.Unmarshal([]byte(got), &dryRun); err != nil { t.Fatalf("decode dry-run: %v\n%s", err, got) diff --git a/shortcuts/apps/apps_file_delete_test.go b/shortcuts/apps/apps_file_delete_test.go index edfce9241..08213fe49 100644 --- a/shortcuts/apps/apps_file_delete_test.go +++ b/shortcuts/apps/apps_file_delete_test.go @@ -48,13 +48,7 @@ func TestAppsFileDelete_DryRunSendsPaths(t *testing.T) { []string{"+file-delete", "--app-id", "app_x", "--path", "/a.png", "--path", "/b.png", "--yes", "--dry-run", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) a := env.API[0] if a.Method != "POST" || a.URL != fileDeleteURL { diff --git a/shortcuts/apps/apps_file_download_test.go b/shortcuts/apps/apps_file_download_test.go index 1d4297971..191bf0d36 100644 --- a/shortcuts/apps/apps_file_download_test.go +++ b/shortcuts/apps/apps_file_download_test.go @@ -41,12 +41,7 @@ func TestAppsFileDownload_DryRunSignsFirst(t *testing.T) { []string{"+file-download", "--app-id", "app_x", "--path", "/x.png", "--dry-run", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) if env.API[0].Method != "POST" || env.API[0].URL != fileSignURLForDownload { t.Fatalf("dry-run = %s %s (want POST sign)", env.API[0].Method, env.API[0].URL) diff --git a/shortcuts/apps/apps_file_get_test.go b/shortcuts/apps/apps_file_get_test.go index ec78811a2..5b5d30a2e 100644 --- a/shortcuts/apps/apps_file_get_test.go +++ b/shortcuts/apps/apps_file_get_test.go @@ -46,13 +46,7 @@ func TestAppsFileGet_DryRunSendsPathQuery(t *testing.T) { []string{"+file-get", "--app-id", "app_x", "--path", "/x.png", "--dry-run", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Params map[string]interface{} `json:"params"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) if env.API[0].Method != "GET" || env.API[0].URL != fileGetURL || env.API[0].Params["path"] != "/x.png" { t.Fatalf("dry-run = %s %s params=%v", env.API[0].Method, env.API[0].URL, env.API[0].Params) diff --git a/shortcuts/apps/apps_file_list_test.go b/shortcuts/apps/apps_file_list_test.go index c6616e4ab..dcfdd3d12 100644 --- a/shortcuts/apps/apps_file_list_test.go +++ b/shortcuts/apps/apps_file_list_test.go @@ -95,13 +95,7 @@ func TestAppsFileList_DryRunSendsFiltersAndPagination(t *testing.T) { factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Params map[string]interface{} `json:"params"` - } `json:"api"` - } + var env dryRunAPIEnvelope if err := json.Unmarshal([]byte(stdout.String()), &env); err != nil { t.Fatalf("decode dry-run: %v\n%s", err, stdout.String()) } @@ -139,11 +133,7 @@ func TestAppsFileList_DryRunOmitsEmptyFilters(t *testing.T) { []string{"+file-list", "--app-id", "app_x", "--dry-run", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Params map[string]interface{} `json:"params"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) for _, banned := range []string{"name", "path", "type", "size_gt", "size_lt", "uploaded_since", "uploaded_until", "page_token"} { if _, ok := env.API[0].Params[banned]; ok { diff --git a/shortcuts/apps/apps_file_sign_test.go b/shortcuts/apps/apps_file_sign_test.go index 84ebbaa79..c461bf8bc 100644 --- a/shortcuts/apps/apps_file_sign_test.go +++ b/shortcuts/apps/apps_file_sign_test.go @@ -22,13 +22,7 @@ func TestAppsFileSign_DryRunBody(t *testing.T) { []string{"+file-sign", "--app-id", "app_x", "--path", "/x.png", "--expires-in", "3600", "--dry-run", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) a := env.API[0] if a.Method != "POST" || a.URL != fileSignURL || a.Body["path"] != "/x.png" { diff --git a/shortcuts/apps/apps_file_upload_test.go b/shortcuts/apps/apps_file_upload_test.go index 06dab27e8..c82d8bcae 100644 --- a/shortcuts/apps/apps_file_upload_test.go +++ b/shortcuts/apps/apps_file_upload_test.go @@ -76,13 +76,7 @@ func TestAppsFileUpload_DryRunPreUpload(t *testing.T) { []string{"+file-upload", "--app-id", "app_x", "--file", "logo.png", "--dry-run", "--as", "user"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope _ = json.Unmarshal([]byte(stdout.String()), &env) a := env.API[0] if a.Method != "POST" || a.URL != "/open-apis/spark/v1/apps/app_x/storage/file_pre_upload" { diff --git a/shortcuts/apps/apps_init_test.go b/shortcuts/apps/apps_init_test.go index 9a706855a..26684b193 100644 --- a/shortcuts/apps/apps_init_test.go +++ b/shortcuts/apps/apps_init_test.go @@ -737,15 +737,12 @@ func newAppsExecuteFactoryWithStderr(t *testing.T) (*cmdutil.Factory, *bytes.Buf } func TestAppsInit_Req1_Wording(t *testing.T) { - // The --dry-run output is a flat object (DryRunAPI marshals to top-level keys - // description/scaffold/api/...), NOT wrapped in {"data":...}, so parse stdout - // directly rather than via parseEnvelopeData. factory, stdout, _ := newAppsExecuteFactoryWithStderr(t) if err := runAppsShortcut(t, AppsInit, []string{"+init", "--app-id", "app_x", "--as", "user", "--dry-run"}, factory, stdout); err != nil { t.Fatalf("dry-run err=%v", err) } - var data map[string]interface{} - if err := json.Unmarshal(stdout.Bytes(), &data); err != nil { + data, err := decodeDryRunDataMap(stdout.Bytes()) + if err != nil { t.Fatalf("decode dry-run output: %v (raw=%q)", err, stdout.String()) } desc, _ := data["description"].(string) @@ -1447,8 +1444,8 @@ func TestAppsInit_DryRun_DescribesEnvPull(t *testing.T) { if err := runAppsShortcut(t, AppsInit, []string{"+init", "--app-id", "app_x", "--dir", dir, "--as", "user", "--dry-run"}, factory, stdout); err != nil { t.Fatalf("unexpected error: %v", err) } - var m map[string]interface{} - if err := json.Unmarshal(stdout.Bytes(), &m); err != nil { + m, err := decodeDryRunDataMap(stdout.Bytes()) + if err != nil { t.Fatalf("decode dry-run: %v (raw=%q)", err, stdout.String()) } ep, _ := m["env_pull"].(string) diff --git a/shortcuts/apps/apps_logs_test.go b/shortcuts/apps/apps_logs_test.go index e456aa2f6..fe17cf46b 100644 --- a/shortcuts/apps/apps_logs_test.go +++ b/shortcuts/apps/apps_logs_test.go @@ -25,13 +25,7 @@ func TestAppsLogList_DryRunBuildsSearchLogsBody(t *testing.T) { if err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope if err := json.Unmarshal(stdout.Bytes(), &env); err != nil { t.Fatalf("decode dry-run: %v\n%s", err, stdout.String()) } diff --git a/shortcuts/apps/apps_metrics_test.go b/shortcuts/apps/apps_metrics_test.go index 3fa032491..9f4889923 100644 --- a/shortcuts/apps/apps_metrics_test.go +++ b/shortcuts/apps/apps_metrics_test.go @@ -39,13 +39,7 @@ func TestAppsMetricList_DryRunUsesSeconds(t *testing.T) { if err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope if err := json.Unmarshal(stdout.Bytes(), &env); err != nil { t.Fatalf("decode dry-run: %v\n%s", err, stdout.String()) } @@ -90,11 +84,7 @@ func TestAppsMetricList_AutoDownSampleByRange(t *testing.T) { if err != nil { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope if err := json.Unmarshal(stdout.Bytes(), &env); err != nil { t.Fatalf("decode dry-run: %v\n%s", err, stdout.String()) } diff --git a/shortcuts/apps/apps_traces_test.go b/shortcuts/apps/apps_traces_test.go index 4768b1f93..559001c8c 100644 --- a/shortcuts/apps/apps_traces_test.go +++ b/shortcuts/apps/apps_traces_test.go @@ -24,13 +24,7 @@ func TestAppsTraceList_DryRunBuildsSearchTracesBody(t *testing.T) { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope if err := json.Unmarshal(stdout.Bytes(), &env); err != nil { t.Fatalf("decode dry-run: %v\n%s", err, stdout.String()) } @@ -73,13 +67,7 @@ func TestAppsTraceGet_DryRunBuildsGetTraceBody(t *testing.T) { t.Fatalf("dry-run err=%v", err) } - var env struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` - } + var env dryRunAPIEnvelope if err := json.Unmarshal(stdout.Bytes(), &env); err != nil { t.Fatalf("decode dry-run: %v\n%s", err, stdout.String()) } diff --git a/shortcuts/apps/dryrun_test.go b/shortcuts/apps/dryrun_test.go new file mode 100644 index 000000000..9651b7d3a --- /dev/null +++ b/shortcuts/apps/dryrun_test.go @@ -0,0 +1,46 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package apps + +import ( + "encoding/json" + "fmt" +) + +type dryRunAPICall struct { + Method string `json:"method"` + URL string `json:"url"` + Params map[string]interface{} `json:"params"` + Body map[string]interface{} `json:"body"` +} + +type dryRunAPIEnvelope struct { + API []dryRunAPICall +} + +func (e *dryRunAPIEnvelope) UnmarshalJSON(data []byte) error { + var raw struct { + Data struct { + API []dryRunAPICall `json:"api"` + } `json:"data"` + } + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + e.API = raw.Data.API + return nil +} + +func decodeDryRunDataMap(data []byte) (map[string]interface{}, error) { + var raw struct { + Data map[string]interface{} `json:"data"` + } + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + if raw.Data == nil { + return nil, fmt.Errorf("dry-run stdout is not a success envelope: %s", data) + } + return raw.Data, nil +} diff --git a/shortcuts/apps/git_credential_test.go b/shortcuts/apps/git_credential_test.go index b48b96b6f..5c5c0c1d2 100644 --- a/shortcuts/apps/git_credential_test.go +++ b/shortcuts/apps/git_credential_test.go @@ -38,25 +38,27 @@ func TestAppsGitCredentialInitDryRunRequestShape(t *testing.T) { t.Fatalf("dry-run err=%v", err) } var payload struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Params map[string]interface{} `json:"params"` - Body interface{} `json:"body"` - } `json:"api"` - Mode string `json:"mode"` - Action string `json:"action"` - AppID string `json:"app_id"` - MetadataFile string `json:"metadata_file"` - LocalEffects []string `json:"local_effects"` + Data struct { + API []struct { + Method string `json:"method"` + URL string `json:"url"` + Params map[string]interface{} `json:"params"` + Body interface{} `json:"body"` + } `json:"api"` + Mode string `json:"mode"` + Action string `json:"action"` + AppID string `json:"app_id"` + MetadataFile string `json:"metadata_file"` + LocalEffects []string `json:"local_effects"` + } `json:"data"` } if err := json.Unmarshal([]byte(stdout.String()), &payload); err != nil { t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String()) } - if len(payload.API) != 1 { - t.Fatalf("api len = %d, want 1", len(payload.API)) + if len(payload.Data.API) != 1 { + t.Fatalf("api len = %d, want 1", len(payload.Data.API)) } - call := payload.API[0] + call := payload.Data.API[0] if call.Method != "GET" { t.Fatalf("method = %q, want GET", call.Method) } @@ -69,19 +71,19 @@ func TestAppsGitCredentialInitDryRunRequestShape(t *testing.T) { if call.Body != nil { t.Fatalf("body = %#v, want nil", call.Body) } - if payload.Mode != "api-plus-local-setup" { - t.Fatalf("mode = %q", payload.Mode) + if payload.Data.Mode != "api-plus-local-setup" { + t.Fatalf("mode = %q", payload.Data.Mode) } - if payload.Action != "initialize_local_git_credential" { - t.Fatalf("action = %q", payload.Action) + if payload.Data.Action != "initialize_local_git_credential" { + t.Fatalf("action = %q", payload.Data.Action) } - if payload.AppID != "app_xxx" { - t.Fatalf("app_id = %q", payload.AppID) + if payload.Data.AppID != "app_xxx" { + t.Fatalf("app_id = %q", payload.Data.AppID) } - if !strings.HasSuffix(payload.MetadataFile, filepath.Join("spark", "app_xxx", "git.json")) { - t.Fatalf("metadata_file = %q", payload.MetadataFile) + if !strings.HasSuffix(payload.Data.MetadataFile, filepath.Join("spark", "app_xxx", "git.json")) { + t.Fatalf("metadata_file = %q", payload.Data.MetadataFile) } - assertStringSliceEqual(t, payload.LocalEffects, []string{ + assertStringSliceEqual(t, payload.Data.LocalEffects, []string{ "save the issued PAT in the local system credential store", "write app-scoped git credential metadata", "configure a URL-scoped Git credential helper in global git config when possible", @@ -96,32 +98,34 @@ func TestAppsGitCredentialListDryRunDescribesLocalReads(t *testing.T) { t.Fatalf("dry-run err=%v", err) } var payload struct { - Description string `json:"description"` - API []interface{} `json:"api"` - Mode string `json:"mode"` - Action string `json:"action"` - StorageRoot string `json:"storage_root"` - Reads []string `json:"reads"` + Data struct { + Description string `json:"description"` + API []interface{} `json:"api"` + Mode string `json:"mode"` + Action string `json:"action"` + StorageRoot string `json:"storage_root"` + Reads []string `json:"reads"` + } `json:"data"` } if err := json.Unmarshal([]byte(stdout.String()), &payload); err != nil { t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String()) } - if payload.Description != "Preview local Git credential listing (no API call, read-only local state)." { - t.Fatalf("description = %q", payload.Description) + if payload.Data.Description != "Preview local Git credential listing (no API call, read-only local state)." { + t.Fatalf("description = %q", payload.Data.Description) } - if len(payload.API) != 0 { - t.Fatalf("api len = %d, want 0", len(payload.API)) + if len(payload.Data.API) != 0 { + t.Fatalf("api len = %d, want 0", len(payload.Data.API)) } - if payload.Mode != "local-read-only" { - t.Fatalf("mode = %q", payload.Mode) + if payload.Data.Mode != "local-read-only" { + t.Fatalf("mode = %q", payload.Data.Mode) } - if payload.Action != "list_local_git_credentials" { - t.Fatalf("action = %q", payload.Action) + if payload.Data.Action != "list_local_git_credentials" { + t.Fatalf("action = %q", payload.Data.Action) } - if !strings.HasSuffix(payload.StorageRoot, filepath.Join("spark")) { - t.Fatalf("storage_root = %q", payload.StorageRoot) + if !strings.HasSuffix(payload.Data.StorageRoot, filepath.Join("spark")) { + t.Fatalf("storage_root = %q", payload.Data.StorageRoot) } - assertStringSliceEqual(t, payload.Reads, []string{ + assertStringSliceEqual(t, payload.Data.Reads, []string{ "scan app-scoped git credential metadata under the CLI config directory", "derive per-app repository URLs and local credential status from local metadata", }) @@ -135,36 +139,38 @@ func TestAppsGitCredentialRemoveDryRunDescribesLocalCleanup(t *testing.T) { t.Fatalf("dry-run err=%v", err) } var payload struct { - Description string `json:"description"` - API []interface{} `json:"api"` - Mode string `json:"mode"` - Action string `json:"action"` - AppID string `json:"app_id"` - MetadataFile string `json:"metadata_file"` - Effects []string `json:"effects"` + Data struct { + Description string `json:"description"` + API []interface{} `json:"api"` + Mode string `json:"mode"` + Action string `json:"action"` + AppID string `json:"app_id"` + MetadataFile string `json:"metadata_file"` + Effects []string `json:"effects"` + } `json:"data"` } if err := json.Unmarshal([]byte(stdout.String()), &payload); err != nil { t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String()) } - if payload.Description != "Preview local Git credential cleanup (no API call; would clean up local-only state)." { - t.Fatalf("description = %q", payload.Description) + if payload.Data.Description != "Preview local Git credential cleanup (no API call; would clean up local-only state)." { + t.Fatalf("description = %q", payload.Data.Description) } - if len(payload.API) != 0 { - t.Fatalf("api len = %d, want 0", len(payload.API)) + if len(payload.Data.API) != 0 { + t.Fatalf("api len = %d, want 0", len(payload.Data.API)) } - if payload.Mode != "local-cleanup-only" { - t.Fatalf("mode = %q", payload.Mode) + if payload.Data.Mode != "local-cleanup-only" { + t.Fatalf("mode = %q", payload.Data.Mode) } - if payload.Action != "remove_local_git_credential" { - t.Fatalf("action = %q", payload.Action) + if payload.Data.Action != "remove_local_git_credential" { + t.Fatalf("action = %q", payload.Data.Action) } - if payload.AppID != "app_xxx" { - t.Fatalf("app_id = %q", payload.AppID) + if payload.Data.AppID != "app_xxx" { + t.Fatalf("app_id = %q", payload.Data.AppID) } - if !strings.HasSuffix(payload.MetadataFile, filepath.Join("spark", "app_xxx", "git.json")) { - t.Fatalf("metadata_file = %q", payload.MetadataFile) + if !strings.HasSuffix(payload.Data.MetadataFile, filepath.Join("spark", "app_xxx", "git.json")) { + t.Fatalf("metadata_file = %q", payload.Data.MetadataFile) } - assertStringSliceEqual(t, payload.Effects, []string{ + assertStringSliceEqual(t, payload.Data.Effects, []string{ "read app-scoped git credential metadata", "remove the saved PAT from the local system credential store", "remove the app-scoped Git helper from global git config when present", diff --git a/shortcuts/common/runner.go b/shortcuts/common/runner.go index 8b36d56b7..047dc2e3d 100644 --- a/shortcuts/common/runner.go +++ b/shortcuts/common/runner.go @@ -1153,14 +1153,19 @@ func handleShortcutDryRun(f *cmdutil.Factory, rctx *RuntimeContext, s *Shortcut) return ValidationErrorf("--dry-run is not supported for %s %s", s.Service, s.Command). WithParam("--dry-run") } - fmt.Fprintln(f.IOStreams.ErrOut, "=== Dry Run ===") dryResult := s.DryRun(rctx.ctx, rctx) - if rctx.Format == "pretty" { - fmt.Fprint(f.IOStreams.Out, dryResult.Format()) - } else { - output.PrintJson(f.IOStreams.Out, dryResult) + if dryResult != nil { + // Same data.context contract as the service/api dry-run paths. + dryResult.Context(rctx.Config.AppID, rctx.UserOpenId()) } - return nil + return cmdutil.WriteDryRun(dryResult, cmdutil.DryRunOutputOptions{ + Format: rctx.Format, + JqExpr: rctx.JqExpr, + CommandPath: rctx.Cmd.CommandPath(), + Identity: rctx.As(), + Out: f.IOStreams.Out, + ErrOut: f.IOStreams.ErrOut, + }) } // rejectPositionalArgs returns a cobra.PositionalArgs that rejects any diff --git a/shortcuts/common/runner_jq_test.go b/shortcuts/common/runner_jq_test.go index d9da6996c..cbc6ceb26 100644 --- a/shortcuts/common/runner_jq_test.go +++ b/shortcuts/common/runner_jq_test.go @@ -6,6 +6,7 @@ package common import ( "bytes" "context" + "encoding/json" "io" "strings" "testing" @@ -229,6 +230,75 @@ func TestRunShortcut_JqRuntimeError_PropagatesError(t *testing.T) { } } +func TestRunShortcut_DryRunJSONUsesEnvelope(t *testing.T) { + s := &Shortcut{ + Service: "test", + Command: "test-shortcut", + AuthTypes: []string{"bot"}, + DryRun: func(ctx context.Context, rctx *RuntimeContext) *cmdutil.DryRunAPI { + return cmdutil.NewDryRunAPI().GET("/open-apis/test") + }, + Execute: func(ctx context.Context, rctx *RuntimeContext) error { + t.Fatal("Execute should not run in dry-run") + return nil + }, + } + f := newTestFactory() + cmd := newTestShortcutCmd(s, f) + cmd.Flags().Set("dry-run", "true") + cmd.Flags().Set("as", "bot") + + if err := runShortcut(cmd, f, s, false); err != nil { + t.Fatalf("runShortcut() error = %v", err) + } + stdout := f.IOStreams.Out.(*bytes.Buffer) + var env map[string]interface{} + if err := json.Unmarshal(stdout.Bytes(), &env); err != nil { + t.Fatalf("dry-run stdout is not JSON: %v\n%s", err, stdout.String()) + } + if env["ok"] != true || env["identity"] != "bot" || env["dry_run"] != true { + t.Fatalf("unexpected dry-run envelope: %#v", env) + } + data := env["data"].(map[string]interface{}) + api := data["api"].([]interface{}) + call := api[0].(map[string]interface{}) + if call["url"] != "/open-apis/test" { + t.Fatalf("api[0] = %#v", call) + } + dctx, ok := data["context"].(map[string]interface{}) + if !ok || dctx["app_id"] != "test" { + t.Fatalf("runner must inject data.context like the service/api paths, got: %#v", data["context"]) + } +} + +func TestRunShortcut_DryRunWithJq(t *testing.T) { + s := &Shortcut{ + Service: "test", + Command: "test-shortcut", + AuthTypes: []string{"bot"}, + DryRun: func(ctx context.Context, rctx *RuntimeContext) *cmdutil.DryRunAPI { + return cmdutil.NewDryRunAPI().GET("/open-apis/test") + }, + Execute: func(ctx context.Context, rctx *RuntimeContext) error { + t.Fatal("Execute should not run in dry-run") + return nil + }, + } + f := newTestFactory() + cmd := newTestShortcutCmd(s, f) + cmd.Flags().Set("dry-run", "true") + cmd.Flags().Set("jq", ".dry_run") + cmd.Flags().Set("as", "bot") + + if err := runShortcut(cmd, f, s, false); err != nil { + t.Fatalf("runShortcut() error = %v", err) + } + stdout := f.IOStreams.Out.(*bytes.Buffer) + if got := strings.TrimSpace(stdout.String()); got != "true" { + t.Fatalf("jq output = %q, want true", got) + } +} + func TestRuntimeContext_Out_WithoutJq_NormalOutput(t *testing.T) { rctx, stdout, _ := newJqTestContext("", "") diff --git a/shortcuts/drive/drive_add_comment_test.go b/shortcuts/drive/drive_add_comment_test.go index 8c07df214..3b9f998ce 100644 --- a/shortcuts/drive/drive_add_comment_test.go +++ b/shortcuts/drive/drive_add_comment_test.go @@ -45,6 +45,16 @@ func decodeJSONMap(t *testing.T, raw string) map[string]interface{} { return data } +func dryRunDataMap(t *testing.T, raw string) map[string]interface{} { + t.Helper() + out := decodeJSONMap(t, raw) + data, ok := out["data"].(map[string]interface{}) + if !ok { + t.Fatalf("dry-run data is %T, want map[string]interface{}\nstdout:\n%s", out["data"], raw) + } + return data +} + func mustMapValue(t *testing.T, value interface{}, path string) map[string]interface{} { t.Helper() @@ -1628,8 +1638,8 @@ func TestDryRunSlidesDirectURL(t *testing.T) { if !strings.Contains(stdout.String(), "slide block comment") { t.Fatalf("dry-run output missing slide block comment: %s", stdout.String()) } - out := decodeJSONMap(t, stdout.String()) - api := mustSliceValue(t, out["api"], "api") + out := dryRunDataMap(t, stdout.String()) + api := mustSliceValue(t, out["api"], "data.api") call := mustMapValue(t, api[0], "api[0]") body := mustMapValue(t, call["body"], "api[0].body") anchor := mustMapValue(t, body["anchor"], "api[0].body.anchor") @@ -1656,8 +1666,8 @@ func TestDryRunBaseDirectURL(t *testing.T) { if !strings.Contains(stdout.String(), "record-local comment") { t.Fatalf("dry-run output missing record-local comment: %s", stdout.String()) } - out := decodeJSONMap(t, stdout.String()) - api := mustSliceValue(t, out["api"], "api") + out := dryRunDataMap(t, stdout.String()) + api := mustSliceValue(t, out["api"], "data.api") call := mustMapValue(t, api[0], "api[0]") body := mustMapValue(t, call["body"], "api[0].body") anchor := mustMapValue(t, body["anchor"], "api[0].body.anchor") @@ -1699,8 +1709,8 @@ func TestDryRunWikiResolvesToSlides(t *testing.T) { if !strings.Contains(stdout.String(), "slide block comment") { t.Fatalf("dry-run output missing slide block comment: %s", stdout.String()) } - out := decodeJSONMap(t, stdout.String()) - api := mustSliceValue(t, out["api"], "api") + out := dryRunDataMap(t, stdout.String()) + api := mustSliceValue(t, out["api"], "data.api") call := mustMapValue(t, api[0], "api[0]") body := mustMapValue(t, call["body"], "api[0].body") anchor := mustMapValue(t, body["anchor"], "api[0].body.anchor") @@ -1736,8 +1746,8 @@ func TestDryRunWikiSlidesInvalidBlockIDSurfaces(t *testing.T) { if !strings.Contains(stdout.String(), "slide --block-id must be") || !strings.Contains(stdout.String(), "shape_2") { t.Fatalf("dry-run output missing block-id format error: %s", stdout.String()) } - out := decodeJSONMap(t, stdout.String()) - api := mustSliceValue(t, out["api"], "api") + out := dryRunDataMap(t, stdout.String()) + api := mustSliceValue(t, out["api"], "data.api") if len(api) != 0 { t.Fatalf("dry-run should not preview API calls with malformed block-id: %s", stdout.String()) } @@ -1821,8 +1831,8 @@ func TestDryRunFileDirectURL(t *testing.T) { if !strings.Contains(stdout.String(), "verify supported file metadata") { t.Fatalf("dry-run output missing supported file metadata verification step: %s", stdout.String()) } - out := decodeJSONMap(t, stdout.String()) - api := mustSliceValue(t, out["api"], "api") + out := dryRunDataMap(t, stdout.String()) + api := mustSliceValue(t, out["api"], "data.api") if len(api) != 2 { t.Fatalf("expected 2 dry-run api calls, got %d\nstdout:\n%s", len(api), stdout.String()) } diff --git a/shortcuts/drive/drive_member_add_test.go b/shortcuts/drive/drive_member_add_test.go index 662780444..652670b66 100644 --- a/shortcuts/drive/drive_member_add_test.go +++ b/shortcuts/drive/drive_member_add_test.go @@ -518,15 +518,17 @@ func TestDriveMemberAdd_PermDefaultsToView(t *testing.T) { } var got struct { - API []struct { - Body map[string]interface{} `json:"body"` - } `json:"api"` + Data struct { + API []struct { + Body map[string]interface{} `json:"body"` + } `json:"api"` + } `json:"data"` } if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String()) } - if got.API[0].Body["perm"] != "view" { - t.Fatalf("perm = %v, want view", got.API[0].Body["perm"]) + if got.Data.API[0].Body["perm"] != "view" { + t.Fatalf("perm = %v, want view", got.Data.API[0].Body["perm"]) } } @@ -625,18 +627,20 @@ func TestDriveMemberAdd_DryRunAcceptsAppID(t *testing.T) { } var got struct { - API []struct { - Body map[string]interface{} `json:"body"` - } `json:"api"` + Data struct { + API []struct { + Body map[string]interface{} `json:"body"` + } `json:"api"` + } `json:"data"` } if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String()) } - if got.API[0].Body["member_type"] != "appid" { - t.Fatalf("member_type = %v, want appid", got.API[0].Body["member_type"]) + if got.Data.API[0].Body["member_type"] != "appid" { + t.Fatalf("member_type = %v, want appid", got.Data.API[0].Body["member_type"]) } - if _, ok := got.API[0].Body["type"]; ok { - t.Fatalf("type = %v, want omitted for appid", got.API[0].Body["type"]) + if _, ok := got.Data.API[0].Body["type"]; ok { + t.Fatalf("type = %v, want omitted for appid", got.Data.API[0].Body["type"]) } } @@ -660,15 +664,17 @@ func TestDriveMemberAdd_DryRunAcceptsWikiSpaceID(t *testing.T) { } var got struct { - API []struct { - Body map[string]interface{} `json:"body"` - } `json:"api"` + Data struct { + API []struct { + Body map[string]interface{} `json:"body"` + } `json:"api"` + } `json:"data"` } if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String()) } - if got.API[0].Body["member_type"] != "wikispaceid" || got.API[0].Body["type"] != "wiki_space_viewer" { - t.Fatalf("body = %#v, want wikispaceid + wiki_space_viewer", got.API[0].Body) + if got.Data.API[0].Body["member_type"] != "wikispaceid" || got.Data.API[0].Body["type"] != "wiki_space_viewer" { + t.Fatalf("body = %#v, want wikispaceid + wiki_space_viewer", got.Data.API[0].Body) } } @@ -792,20 +798,22 @@ func TestDriveMemberAdd_DryRunInfersTypeAndDefaultsWikiPermType(t *testing.T) { } var got struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Params map[string]interface{} `json:"params"` - Body map[string]interface{} `json:"body"` - } `json:"api"` + Data struct { + API []struct { + Method string `json:"method"` + URL string `json:"url"` + Params map[string]interface{} `json:"params"` + Body map[string]interface{} `json:"body"` + } `json:"api"` + } `json:"data"` } if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String()) } - if len(got.API) != 1 { - t.Fatalf("api count = %d, want 1; stdout=%s", len(got.API), stdout.String()) + if len(got.Data.API) != 1 { + t.Fatalf("api count = %d, want 1; stdout=%s", len(got.Data.API), stdout.String()) } - api := got.API[0] + api := got.Data.API[0] if api.Method != "POST" || api.URL != "/open-apis/drive/v1/permissions/wikTok/members" { t.Fatalf("api = %#v", api) } @@ -836,19 +844,21 @@ func TestDriveMemberAdd_DryRunAcceptsUppercaseEnumsForDocx(t *testing.T) { } var got struct { - API []struct { - Params map[string]interface{} `json:"params"` - Body map[string]interface{} `json:"body"` - } `json:"api"` + Data struct { + API []struct { + Params map[string]interface{} `json:"params"` + Body map[string]interface{} `json:"body"` + } `json:"api"` + } `json:"data"` } if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String()) } - if got.API[0].Params["type"] != "docx" { - t.Fatalf("params.type = %v, want docx", got.API[0].Params["type"]) + if got.Data.API[0].Params["type"] != "docx" { + t.Fatalf("params.type = %v, want docx", got.Data.API[0].Params["type"]) } - if got.API[0].Body["member_type"] != "openid" || got.API[0].Body["perm"] != "edit" { - t.Fatalf("body = %#v, want canonical lowercase enum values", got.API[0].Body) + if got.Data.API[0].Body["member_type"] != "openid" || got.Data.API[0].Body["perm"] != "edit" { + t.Fatalf("body = %#v, want canonical lowercase enum values", got.Data.API[0].Body) } } @@ -872,19 +882,21 @@ func TestDriveMemberAdd_DryRunAcceptsUppercaseWikiPermType(t *testing.T) { } var got struct { - API []struct { - Params map[string]interface{} `json:"params"` - Body map[string]interface{} `json:"body"` - } `json:"api"` + Data struct { + API []struct { + Params map[string]interface{} `json:"params"` + Body map[string]interface{} `json:"body"` + } `json:"api"` + } `json:"data"` } if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String()) } - if got.API[0].Params["type"] != "wiki" { - t.Fatalf("params.type = %v, want wiki", got.API[0].Params["type"]) + if got.Data.API[0].Params["type"] != "wiki" { + t.Fatalf("params.type = %v, want wiki", got.Data.API[0].Params["type"]) } - if got.API[0].Body["member_type"] != "openid" || got.API[0].Body["perm"] != "edit" || got.API[0].Body["perm_type"] != "container" { - t.Fatalf("body = %#v, want canonical lowercase enum values", got.API[0].Body) + if got.Data.API[0].Body["member_type"] != "openid" || got.Data.API[0].Body["perm"] != "edit" || got.Data.API[0].Body["perm_type"] != "container" { + t.Fatalf("body = %#v, want canonical lowercase enum values", got.Data.API[0].Body) } } @@ -949,19 +961,21 @@ func TestDriveMemberAdd_DryRunBatch(t *testing.T) { } var got struct { - API []struct { - Method string `json:"method"` - URL string `json:"url"` - Body map[string]interface{} `json:"body"` - } `json:"api"` + Data struct { + API []struct { + Method string `json:"method"` + URL string `json:"url"` + Body map[string]interface{} `json:"body"` + } `json:"api"` + } `json:"data"` } if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { t.Fatalf("decode dry-run output: %v\n%s", err, stdout.String()) } - if len(got.API) != 1 { - t.Fatalf("api count = %d, want 1", len(got.API)) + if len(got.Data.API) != 1 { + t.Fatalf("api count = %d, want 1", len(got.Data.API)) } - api := got.API[0] + api := got.Data.API[0] if api.Method != "POST" || api.URL != "/open-apis/drive/v1/permissions/shtcnTok/members/batch_create" { t.Fatalf("api = %#v", api) } diff --git a/shortcuts/sheets/helpers_test.go b/shortcuts/sheets/helpers_test.go index e17149a21..39aef181b 100644 --- a/shortcuts/sheets/helpers_test.go +++ b/shortcuts/sheets/helpers_test.go @@ -197,7 +197,7 @@ func TestSheetHelpersValidationMetadata(t *testing.T) { // api call's body. The dry-run output format is: // // === Dry Run === -// { "api": [{...}], ... } +// { "ok": true, "dry_run": true, "data": { "api": [{...}], ... } } // // Tests use this to assert the One-OpenAPI wire body is constructed // correctly without exercising the real endpoint. @@ -220,7 +220,7 @@ func parseDryRunAPI(t *testing.T, sc common.Shortcut, args []string) []interface t.Fatalf("dry-run failed: %v\noutput=%s", err, out) } dryRun := decodeDryRunRaw(t, out) - calls, _ := dryRun["api"].([]interface{}) + calls, _ := dryRunAPIEntries(dryRun) return calls } @@ -240,7 +240,7 @@ func decodeDryRunRaw(t *testing.T, out string) map[string]interface{} { func decodeDryRunFirstCall(t *testing.T, out string) map[string]interface{} { t.Helper() dryRun := decodeDryRunRaw(t, out) - calls, ok := dryRun["api"].([]interface{}) + calls, ok := dryRunAPIEntries(dryRun) if !ok || len(calls) == 0 { t.Fatalf("dry-run api array empty or wrong shape: %#v", dryRun) } @@ -252,6 +252,15 @@ func decodeDryRunFirstCall(t *testing.T, out string) map[string]interface{} { return body } +func dryRunAPIEntries(dryRun map[string]interface{}) ([]interface{}, bool) { + if data, ok := dryRun["data"].(map[string]interface{}); ok { + calls, ok := data["api"].([]interface{}) + return calls, ok + } + calls, ok := dryRun["api"].([]interface{}) + return calls, ok +} + // decodeToolInput parses the JSON-string `input` field embedded in a // dry-run body whose tool_name matches `expected`. Returns the decoded // tool input map so tests can assert on specific input fields. diff --git a/shortcuts/sheets/lark_sheet_history_test.go b/shortcuts/sheets/lark_sheet_history_test.go index 00df24a63..09cf4fb19 100644 --- a/shortcuts/sheets/lark_sheet_history_test.go +++ b/shortcuts/sheets/lark_sheet_history_test.go @@ -158,7 +158,7 @@ func dryRunFirstCallURL(t *testing.T, sc common.Shortcut, args []string) string t.Fatalf("dry-run failed: %v\noutput=%s", err, out) } dryRun := decodeDryRunRaw(t, out) - calls, ok := dryRun["api"].([]interface{}) + calls, ok := dryRunAPIEntries(dryRun) if !ok || len(calls) == 0 { t.Fatalf("dry-run api array empty or wrong shape: %#v", dryRun) } diff --git a/shortcuts/slides/slides_replace_pages_test.go b/shortcuts/slides/slides_replace_pages_test.go index fd1c51584..e62169d8e 100644 --- a/shortcuts/slides/slides_replace_pages_test.go +++ b/shortcuts/slides/slides_replace_pages_test.go @@ -270,10 +270,11 @@ func TestReplacePagesDryRunPlansOnly(t *testing.T) { if err := json.Unmarshal(stdout.Bytes(), &out); err != nil { t.Fatalf("decode dry-run: %v\nraw=%s", err, stdout.String()) } - if out["xml_presentation_id"] != "pres_abc" { - t.Fatalf("xml_presentation_id = %v", out["xml_presentation_id"]) + data, _ := out["data"].(map[string]interface{}) + if data["xml_presentation_id"] != "pres_abc" { + t.Fatalf("xml_presentation_id = %v", data["xml_presentation_id"]) } - plan, _ := out["plan"].([]interface{}) + plan, _ := data["plan"].([]interface{}) if len(plan) != 1 { t.Fatalf("plan len = %d, want 1", len(plan)) } @@ -281,7 +282,7 @@ func TestReplacePagesDryRunPlansOnly(t *testing.T) { if item["old_slide_id"] != "old2" || item["action"] != "create_before_then_delete_old" { t.Fatalf("plan item = %#v", item) } - api, _ := out["api"].([]interface{}) + api, _ := data["api"].([]interface{}) if len(api) != 2 { t.Fatalf("api len = %d, want create/delete plan", len(api)) } diff --git a/shortcuts/task/task_upload_attachment_test.go b/shortcuts/task/task_upload_attachment_test.go index 7be0f96eb..8fbd56948 100644 --- a/shortcuts/task/task_upload_attachment_test.go +++ b/shortcuts/task/task_upload_attachment_test.go @@ -508,7 +508,8 @@ func TestUploadAttachmentTask_DryRun(t *testing.T) { if err := json.Unmarshal([]byte(out), &dry); err != nil { t.Fatalf("dry-run output is not JSON: %v\n%s", err, out) } - calls, _ := dry["api"].([]interface{}) + data, _ := dry["data"].(map[string]interface{}) + calls, _ := data["api"].([]interface{}) if len(calls) != 1 { t.Fatalf("expected 1 api call in dry-run, got %d: %v", len(calls), calls) } diff --git a/tests/cli_e2e/application/slash_command_dryrun_test.go b/tests/cli_e2e/application/slash_command_dryrun_test.go index 9a9f8e795..8443505a8 100644 --- a/tests/cli_e2e/application/slash_command_dryrun_test.go +++ b/tests/cli_e2e/application/slash_command_dryrun_test.go @@ -47,8 +47,8 @@ func TestSlashCommandList_DryRunShowsGetPath(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - assert.Equal(t, "GET", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - assert.Equal(t, slashCommandBasePath, gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) + assert.Equal(t, "GET", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + assert.Equal(t, slashCommandBasePath, clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) } // TestSlashCommandCreate_DryRunShowsPostBody pins the POST body shape for @@ -77,14 +77,14 @@ func TestSlashCommandCreate_DryRunShowsPostBody(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - assert.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - assert.Equal(t, slashCommandBasePath, gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) - assert.Equal(t, "greet", gjson.Get(out, "api.0.body.command").String(), "stdout:\n%s", out) - assert.Equal(t, "say hi", gjson.Get(out, "api.0.body.description.default_value").String(), "stdout:\n%s", out) - assert.Equal(t, "你好", gjson.Get(out, "api.0.body.description.i18n.zh_cn").String(), "stdout:\n%s", out) + assert.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + assert.Equal(t, slashCommandBasePath, clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) + assert.Equal(t, "greet", clie2e.DryRunGet(out, "api.0.body.command").String(), "stdout:\n%s", out) + assert.Equal(t, "say hi", clie2e.DryRunGet(out, "api.0.body.description.default_value").String(), "stdout:\n%s", out) + assert.Equal(t, "你好", clie2e.DryRunGet(out, "api.0.body.description.i18n.zh_cn").String(), "stdout:\n%s", out) // icon is a top-level key, sibling of description. - assert.Equal(t, "skill_outlined", gjson.Get(out, "api.0.body.icon.icon_key").String(), "stdout:\n%s", out) - assert.False(t, gjson.Get(out, "api.0.body.description.icon").Exists(), "icon must not be nested inside description:\n%s", out) + assert.Equal(t, "skill_outlined", clie2e.DryRunGet(out, "api.0.body.icon.icon_key").String(), "stdout:\n%s", out) + assert.False(t, clie2e.DryRunGet(out, "api.0.body.description.icon").Exists(), "icon must not be nested inside description:\n%s", out) } // TestSlashCommandUpdate_DryRunShowsPatchPath pins the PATCH shape for @@ -108,9 +108,9 @@ func TestSlashCommandUpdate_DryRunShowsPatchPath(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - assert.Equal(t, "PATCH", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - assert.Equal(t, slashCommandBasePath+"/id%2Fwith%20space%3Fx", gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) - assert.Equal(t, "updated description", gjson.Get(out, "api.0.body.description.default_value").String(), "stdout:\n%s", out) + assert.Equal(t, "PATCH", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + assert.Equal(t, slashCommandBasePath+"/id%2Fwith%20space%3Fx", clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) + assert.Equal(t, "updated description", clie2e.DryRunGet(out, "api.0.body.description.default_value").String(), "stdout:\n%s", out) } // TestSlashCommandDelete_DryRunShowsDeletePath pins the DELETE shape for @@ -137,8 +137,8 @@ func TestSlashCommandDelete_DryRunShowsDeletePath(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - assert.Equal(t, "DELETE", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - assert.Equal(t, slashCommandBasePath+"/id%2Fwith%20space%3Fx", gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) + assert.Equal(t, "DELETE", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + assert.Equal(t, slashCommandBasePath+"/id%2Fwith%20space%3Fx", clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) } // TestSlashCommandDelete_WithoutYesRequiresConfirmation asserts the diff --git a/tests/cli_e2e/apps/apps_access_scope_get_dryrun_test.go b/tests/cli_e2e/apps/apps_access_scope_get_dryrun_test.go index f22aaa960..14310dc1b 100644 --- a/tests/cli_e2e/apps/apps_access_scope_get_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_access_scope_get_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestAppsAccessScopeGetDryRun pins URL shape and --app-id requirement for the @@ -35,11 +34,11 @@ func TestAppsAccessScopeGetDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "GET", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/spark/v1/apps/app_x/access-scope", gjson.Get(result.Stdout, "api.0.url").String()) + assert.Equal(t, "GET", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/spark/v1/apps/app_x/access-scope", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) // GET request: no body and no query params. - assert.False(t, gjson.Get(result.Stdout, "api.0.body").Exists()) - assert.False(t, gjson.Get(result.Stdout, "api.0.params").Exists()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body").Exists()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.params").Exists()) }) t.Run("RejectsMissingAppID", func(t *testing.T) { diff --git a/tests/cli_e2e/apps/apps_access_scope_set_dryrun_test.go b/tests/cli_e2e/apps/apps_access_scope_set_dryrun_test.go index 0b94e1ce0..684c16fa7 100644 --- a/tests/cli_e2e/apps/apps_access_scope_set_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_access_scope_set_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestAppsAccessScopeSetDryRun pins the user-facing scope-string -> server-enum @@ -37,14 +36,14 @@ func TestAppsAccessScopeSetDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "PUT", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/spark/v1/apps/app_x/access-scope", gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, "Range", gjson.Get(result.Stdout, "api.0.body.scope").String()) - assert.Equal(t, "ou_x", gjson.Get(result.Stdout, "api.0.body.users.0").String()) - assert.Equal(t, "oc_x", gjson.Get(result.Stdout, "api.0.body.chats.0").String()) - assert.False(t, gjson.Get(result.Stdout, "api.0.body.departments").Exists(), + assert.Equal(t, "PUT", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/spark/v1/apps/app_x/access-scope", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.Equal(t, "Range", clie2e.DryRunGet(result.Stdout, "api.0.body.scope").String()) + assert.Equal(t, "ou_x", clie2e.DryRunGet(result.Stdout, "api.0.body.users.0").String()) + assert.Equal(t, "oc_x", clie2e.DryRunGet(result.Stdout, "api.0.body.chats.0").String()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body.departments").Exists(), "empty department list must be omitted") - assert.False(t, gjson.Get(result.Stdout, "api.0.body.apply_config").Exists()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body.apply_config").Exists()) }) t.Run("SpecificWithApplyConfig", func(t *testing.T) { @@ -66,8 +65,8 @@ func TestAppsAccessScopeSetDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.True(t, gjson.Get(result.Stdout, "api.0.body.apply_config.enabled").Bool()) - assert.Equal(t, "ou_y", gjson.Get(result.Stdout, "api.0.body.apply_config.approvers.0").String()) + assert.True(t, clie2e.DryRunGet(result.Stdout, "api.0.body.apply_config.enabled").Bool()) + assert.Equal(t, "ou_y", clie2e.DryRunGet(result.Stdout, "api.0.body.apply_config.approvers.0").String()) }) t.Run("PublicMapsToAll", func(t *testing.T) { @@ -87,10 +86,10 @@ func TestAppsAccessScopeSetDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "All", gjson.Get(result.Stdout, "api.0.body.scope").String()) - assert.False(t, gjson.Get(result.Stdout, "api.0.body.require_login").Bool()) - assert.False(t, gjson.Get(result.Stdout, "api.0.body.users").Exists()) - assert.False(t, gjson.Get(result.Stdout, "api.0.body.apply_config").Exists()) + assert.Equal(t, "All", clie2e.DryRunGet(result.Stdout, "api.0.body.scope").String()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body.require_login").Bool()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body.users").Exists()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body.apply_config").Exists()) }) t.Run("TenantMapsToTenant", func(t *testing.T) { @@ -109,10 +108,10 @@ func TestAppsAccessScopeSetDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "Tenant", gjson.Get(result.Stdout, "api.0.body.scope").String()) + assert.Equal(t, "Tenant", clie2e.DryRunGet(result.Stdout, "api.0.body.scope").String()) // scope is the only body field in tenant mode. - assert.False(t, gjson.Get(result.Stdout, "api.0.body.require_login").Exists()) - assert.False(t, gjson.Get(result.Stdout, "api.0.body.users").Exists()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body.require_login").Exists()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body.users").Exists()) }) t.Run("RejectsSpecificMissingTargets", func(t *testing.T) { diff --git a/tests/cli_e2e/apps/apps_create_dryrun_test.go b/tests/cli_e2e/apps/apps_create_dryrun_test.go index 49857d8a5..dd182519d 100644 --- a/tests/cli_e2e/apps/apps_create_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_create_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestAppsCreateDryRun pins the request shape and Validate behavior for @@ -36,13 +35,13 @@ func TestAppsCreateDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "POST", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/spark/v1/apps", gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, "Demo", gjson.Get(result.Stdout, "api.0.body.name").String()) - assert.Equal(t, "html", gjson.Get(result.Stdout, "api.0.body.app_type").String()) + assert.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/spark/v1/apps", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.Equal(t, "Demo", clie2e.DryRunGet(result.Stdout, "api.0.body.name").String()) + assert.Equal(t, "html", clie2e.DryRunGet(result.Stdout, "api.0.body.app_type").String()) // Optional fields stay omitted when not provided. - assert.False(t, gjson.Get(result.Stdout, "api.0.body.description").Exists()) - assert.False(t, gjson.Get(result.Stdout, "api.0.body.icon_url").Exists()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body.description").Exists()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body.icon_url").Exists()) }) t.Run("AllFields", func(t *testing.T) { @@ -63,10 +62,10 @@ func TestAppsCreateDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "Demo", gjson.Get(result.Stdout, "api.0.body.name").String()) - assert.Equal(t, "html", gjson.Get(result.Stdout, "api.0.body.app_type").String()) - assert.Equal(t, "survey app", gjson.Get(result.Stdout, "api.0.body.description").String()) - assert.Equal(t, "https://example.com/icon.svg", gjson.Get(result.Stdout, "api.0.body.icon_url").String()) + assert.Equal(t, "Demo", clie2e.DryRunGet(result.Stdout, "api.0.body.name").String()) + assert.Equal(t, "html", clie2e.DryRunGet(result.Stdout, "api.0.body.app_type").String()) + assert.Equal(t, "survey app", clie2e.DryRunGet(result.Stdout, "api.0.body.description").String()) + assert.Equal(t, "https://example.com/icon.svg", clie2e.DryRunGet(result.Stdout, "api.0.body.icon_url").String()) }) t.Run("RejectsMissingName", func(t *testing.T) { diff --git a/tests/cli_e2e/apps/apps_db_env_create_dryrun_test.go b/tests/cli_e2e/apps/apps_db_env_create_dryrun_test.go index 2581ae9b6..9766b303f 100644 --- a/tests/cli_e2e/apps/apps_db_env_create_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_db_env_create_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestAppsDBEnvCreateDryRun pins +db-env-create URL `/apps/{app_id}/db_dev_init` 和 sync_data body 透传。 @@ -30,9 +29,9 @@ func TestAppsDBEnvCreateDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "POST", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/spark/v1/apps/app_x/db_dev_init", gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, "false", gjson.Get(result.Stdout, "api.0.body.sync_data").String()) + assert.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/spark/v1/apps/app_x/db_dev_init", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.Equal(t, "false", clie2e.DryRunGet(result.Stdout, "api.0.body.sync_data").String()) }) t.Run("SyncDataTrue", func(t *testing.T) { @@ -45,6 +44,6 @@ func TestAppsDBEnvCreateDryRun(t *testing.T) { }) require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "true", gjson.Get(result.Stdout, "api.0.body.sync_data").String()) + assert.Equal(t, "true", clie2e.DryRunGet(result.Stdout, "api.0.body.sync_data").String()) }) } diff --git a/tests/cli_e2e/apps/apps_db_execute_dryrun_test.go b/tests/cli_e2e/apps/apps_db_execute_dryrun_test.go index f801ebd29..79ff36a78 100644 --- a/tests/cli_e2e/apps/apps_db_execute_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_db_execute_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestAppsDBExecuteDryRun pins +db-execute 复用存量 URL,CLI 永远走 DBA 模式 @@ -30,14 +29,14 @@ func TestAppsDBExecuteDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "POST", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/spark/v1/apps/app_x/sql_commands", gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, "SELECT 1", gjson.Get(result.Stdout, "api.0.body.sql").String()) - assert.Equal(t, "false", gjson.Get(result.Stdout, "api.0.params.transactional").String(), + assert.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/spark/v1/apps/app_x/sql_commands", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.Equal(t, "SELECT 1", clie2e.DryRunGet(result.Stdout, "api.0.body.sql").String()) + assert.Equal(t, "false", clie2e.DryRunGet(result.Stdout, "api.0.params.transactional").String(), "CLI is DBA mode → must send transactional=false in query") - assert.False(t, gjson.Get(result.Stdout, "api.0.body.transactional").Exists(), + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body.transactional").Exists(), "transactional should be in query, not body") - assert.False(t, gjson.Get(result.Stdout, "api.0.params.env").Exists(), + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.params.env").Exists(), "default: no --environment → env key must be omitted (server picks workspace default branch)") }) @@ -51,7 +50,7 @@ func TestAppsDBExecuteDryRun(t *testing.T) { }) require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "online", gjson.Get(result.Stdout, "api.0.params.env").String()) + assert.Equal(t, "online", clie2e.DryRunGet(result.Stdout, "api.0.params.env").String()) }) t.Run("RejectsEmptySQL", func(t *testing.T) { diff --git a/tests/cli_e2e/apps/apps_db_table_get_dryrun_test.go b/tests/cli_e2e/apps/apps_db_table_get_dryrun_test.go index 47bd340f8..5bc5e66fe 100644 --- a/tests/cli_e2e/apps/apps_db_table_get_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_db_table_get_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestAppsDBTableGetDryRun pins +db-table-get 复用存量 URL。 @@ -33,9 +32,9 @@ func TestAppsDBTableGetDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "GET", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/spark/v1/apps/app_x/tables/orders", gjson.Get(result.Stdout, "api.0.url").String()) - assert.False(t, gjson.Get(result.Stdout, "api.0.params.format").Exists(), + assert.Equal(t, "GET", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/spark/v1/apps/app_x/tables/orders", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.params.format").Exists(), "default (json) should omit format query") }) @@ -65,7 +64,7 @@ func TestAppsDBTableGetDryRun(t *testing.T) { }) require.NoError(t, err) result.AssertExitCode(t, 0) - assert.False(t, gjson.Get(result.Stdout, "api.0.params.format").Exists()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.params.format").Exists()) }) t.Run("RequiresTableFlag", func(t *testing.T) { diff --git a/tests/cli_e2e/apps/apps_db_table_list_dryrun_test.go b/tests/cli_e2e/apps/apps_db_table_list_dryrun_test.go index 28e1e2eae..f02fe0b76 100644 --- a/tests/cli_e2e/apps/apps_db_table_list_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_db_table_list_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestAppsDBTableListDryRun pins +db-table-list 复用存量 URL(/apps/{app_id}/tables, @@ -30,14 +29,14 @@ func TestAppsDBTableListDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "GET", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/spark/v1/apps/app_x/tables", gjson.Get(result.Stdout, "api.0.url").String()) - assert.False(t, gjson.Get(result.Stdout, "api.0.params.env").Exists(), + assert.Equal(t, "GET", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/spark/v1/apps/app_x/tables", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.params.env").Exists(), "default: no --environment → env key must be omitted (server picks workspace default branch)") - assert.Equal(t, "20", gjson.Get(result.Stdout, "api.0.params.page_size").String()) - assert.False(t, gjson.Get(result.Stdout, "api.0.params.page_token").Exists(), + assert.Equal(t, "20", clie2e.DryRunGet(result.Stdout, "api.0.params.page_size").String()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.params.page_token").Exists(), "empty page_token must be omitted") - assert.False(t, gjson.Get(result.Stdout, "api.0.params.include_stats").Exists(), + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.params.include_stats").Exists(), "CLI should not send include_stats query (server returns stats by default)") }) @@ -55,9 +54,9 @@ func TestAppsDBTableListDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "dev", gjson.Get(result.Stdout, "api.0.params.env").String()) - assert.Equal(t, "50", gjson.Get(result.Stdout, "api.0.params.page_size").String()) - assert.Equal(t, "cursor-abc", gjson.Get(result.Stdout, "api.0.params.page_token").String()) + assert.Equal(t, "dev", clie2e.DryRunGet(result.Stdout, "api.0.params.env").String()) + assert.Equal(t, "50", clie2e.DryRunGet(result.Stdout, "api.0.params.page_size").String()) + assert.Equal(t, "cursor-abc", clie2e.DryRunGet(result.Stdout, "api.0.params.page_token").String()) }) t.Run("RejectsBlankAppID", func(t *testing.T) { diff --git a/tests/cli_e2e/apps/apps_env_pull_dryrun_test.go b/tests/cli_e2e/apps/apps_env_pull_dryrun_test.go index 513bdbf9f..b709d9b77 100644 --- a/tests/cli_e2e/apps/apps_env_pull_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_env_pull_dryrun_test.go @@ -12,7 +12,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestAppsEnvPullDryRun(t *testing.T) { @@ -33,14 +32,14 @@ func TestAppsEnvPullDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "POST", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/spark/v1/apps/app_x/env_vars", gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, "dev", gjson.Get(result.Stdout, "api.0.body.env").String()) - assert.False(t, gjson.Get(result.Stdout, "api.0.body.include_values").Exists()) - assert.False(t, gjson.Get(result.Stdout, "api.0.params").Exists()) - assert.True(t, gjson.Get(result.Stdout, "project_path").Exists()) - assert.Contains(t, gjson.Get(result.Stdout, "env_file").String(), ".env.local") - assert.False(t, gjson.Get(result.Stdout, "env_keys").Exists()) + assert.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/spark/v1/apps/app_x/env_vars", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.Equal(t, "dev", clie2e.DryRunGet(result.Stdout, "api.0.body.env").String()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body.include_values").Exists()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.params").Exists()) + assert.True(t, clie2e.DryRunGet(result.Stdout, "project_path").Exists()) + assert.Contains(t, clie2e.DryRunGet(result.Stdout, "env_file").String(), ".env.local") + assert.False(t, clie2e.DryRunGet(result.Stdout, "env_keys").Exists()) }) t.Run("CustomProjectPath", func(t *testing.T) { @@ -60,8 +59,8 @@ func TestAppsEnvPullDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, projectDir, gjson.Get(result.Stdout, "project_path").String()) - assert.Equal(t, filepath.Join(projectDir, ".env.local"), gjson.Get(result.Stdout, "env_file").String()) + assert.Equal(t, projectDir, clie2e.DryRunGet(result.Stdout, "project_path").String()) + assert.Equal(t, filepath.Join(projectDir, ".env.local"), clie2e.DryRunGet(result.Stdout, "env_file").String()) }) t.Run("MissingAppID", func(t *testing.T) { diff --git a/tests/cli_e2e/apps/apps_git_credential_dryrun_test.go b/tests/cli_e2e/apps/apps_git_credential_dryrun_test.go index 67718a7e4..f7f9be6d1 100644 --- a/tests/cli_e2e/apps/apps_git_credential_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_git_credential_dryrun_test.go @@ -13,7 +13,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestAppsGitCredentialInitDryRun(t *testing.T) { @@ -34,17 +33,17 @@ func TestAppsGitCredentialInitDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "GET", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/spark/v1/apps/app_xxx/git_info", gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, "app_xxx", gjson.Get(result.Stdout, "api.0.params.app_id").String()) - assert.False(t, gjson.Get(result.Stdout, "api.0.body").Exists()) - assert.Equal(t, "api-plus-local-setup", gjson.Get(result.Stdout, "mode").String()) - assert.Equal(t, "initialize_local_git_credential", gjson.Get(result.Stdout, "action").String()) - assert.True(t, strings.HasSuffix(gjson.Get(result.Stdout, "metadata_file").String(), filepath.Join("spark", "app_xxx", "git.json"))) - assert.Equal(t, int64(3), gjson.Get(result.Stdout, "local_effects.#").Int()) - assert.Equal(t, "save the issued PAT in the local system credential store", gjson.Get(result.Stdout, "local_effects.0").String()) - assert.Equal(t, "write app-scoped git credential metadata", gjson.Get(result.Stdout, "local_effects.1").String()) - assert.Equal(t, "configure a URL-scoped Git credential helper in global git config when possible", gjson.Get(result.Stdout, "local_effects.2").String()) + assert.Equal(t, "GET", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/spark/v1/apps/app_xxx/git_info", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.Equal(t, "app_xxx", clie2e.DryRunGet(result.Stdout, "api.0.params.app_id").String()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body").Exists()) + assert.Equal(t, "api-plus-local-setup", clie2e.DryRunGet(result.Stdout, "mode").String()) + assert.Equal(t, "initialize_local_git_credential", clie2e.DryRunGet(result.Stdout, "action").String()) + assert.True(t, strings.HasSuffix(clie2e.DryRunGet(result.Stdout, "metadata_file").String(), filepath.Join("spark", "app_xxx", "git.json"))) + assert.Equal(t, int64(3), clie2e.DryRunGet(result.Stdout, "local_effects.#").Int()) + assert.Equal(t, "save the issued PAT in the local system credential store", clie2e.DryRunGet(result.Stdout, "local_effects.0").String()) + assert.Equal(t, "write app-scoped git credential metadata", clie2e.DryRunGet(result.Stdout, "local_effects.1").String()) + assert.Equal(t, "configure a URL-scoped Git credential helper in global git config when possible", clie2e.DryRunGet(result.Stdout, "local_effects.2").String()) } func TestAppsGitCredentialListDryRun(t *testing.T) { @@ -61,12 +60,12 @@ func TestAppsGitCredentialListDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "Preview local Git credential listing (no API call, read-only local state).", gjson.Get(result.Stdout, "description").String()) - assert.Equal(t, "local-read-only", gjson.Get(result.Stdout, "mode").String()) - assert.Equal(t, "list_local_git_credentials", gjson.Get(result.Stdout, "action").String()) - assert.Equal(t, int64(0), gjson.Get(result.Stdout, "api.#").Int()) - assert.Contains(t, gjson.Get(result.Stdout, "storage_root").String(), filepath.Join("", "spark")) - assert.Equal(t, "scan app-scoped git credential metadata under the CLI config directory", gjson.Get(result.Stdout, "reads.0").String()) + assert.Equal(t, "Preview local Git credential listing (no API call, read-only local state).", clie2e.DryRunGet(result.Stdout, "description").String()) + assert.Equal(t, "local-read-only", clie2e.DryRunGet(result.Stdout, "mode").String()) + assert.Equal(t, "list_local_git_credentials", clie2e.DryRunGet(result.Stdout, "action").String()) + assert.Equal(t, int64(0), clie2e.DryRunGet(result.Stdout, "api.#").Int()) + assert.Contains(t, clie2e.DryRunGet(result.Stdout, "storage_root").String(), filepath.Join("", "spark")) + assert.Equal(t, "scan app-scoped git credential metadata under the CLI config directory", clie2e.DryRunGet(result.Stdout, "reads.0").String()) } func TestAppsGitCredentialRemoveDryRun(t *testing.T) { @@ -83,11 +82,11 @@ func TestAppsGitCredentialRemoveDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "Preview local Git credential cleanup (no API call; would clean up local-only state).", gjson.Get(result.Stdout, "description").String()) - assert.Equal(t, "local-cleanup-only", gjson.Get(result.Stdout, "mode").String()) - assert.Equal(t, "remove_local_git_credential", gjson.Get(result.Stdout, "action").String()) - assert.Equal(t, "app_xxx", gjson.Get(result.Stdout, "app_id").String()) - assert.Equal(t, int64(0), gjson.Get(result.Stdout, "api.#").Int()) - assert.True(t, strings.HasSuffix(gjson.Get(result.Stdout, "metadata_file").String(), filepath.Join("spark", "app_xxx", "git.json"))) - assert.Equal(t, "read app-scoped git credential metadata", gjson.Get(result.Stdout, "effects.0").String()) + assert.Equal(t, "Preview local Git credential cleanup (no API call; would clean up local-only state).", clie2e.DryRunGet(result.Stdout, "description").String()) + assert.Equal(t, "local-cleanup-only", clie2e.DryRunGet(result.Stdout, "mode").String()) + assert.Equal(t, "remove_local_git_credential", clie2e.DryRunGet(result.Stdout, "action").String()) + assert.Equal(t, "app_xxx", clie2e.DryRunGet(result.Stdout, "app_id").String()) + assert.Equal(t, int64(0), clie2e.DryRunGet(result.Stdout, "api.#").Int()) + assert.True(t, strings.HasSuffix(clie2e.DryRunGet(result.Stdout, "metadata_file").String(), filepath.Join("spark", "app_xxx", "git.json"))) + assert.Equal(t, "read app-scoped git credential metadata", clie2e.DryRunGet(result.Stdout, "effects.0").String()) } diff --git a/tests/cli_e2e/apps/apps_html_publish_dryrun_test.go b/tests/cli_e2e/apps/apps_html_publish_dryrun_test.go index 124204cb0..bfea4d595 100644 --- a/tests/cli_e2e/apps/apps_html_publish_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_html_publish_dryrun_test.go @@ -13,7 +13,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestAppsHTMLPublishDryRun exercises the walker / manifest layer without @@ -50,13 +49,13 @@ func TestAppsHTMLPublishDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "POST", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/spark/v1/apps/app_x/upload_and_release_html_code", gjson.Get(result.Stdout, "api.0.url").String()) + assert.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/spark/v1/apps/app_x/upload_and_release_html_code", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) // file_count / files / total_size_bytes sit at envelope top level // (not under api.0.body — manifest is dry-run metadata, not the HTTP body). - assert.Equal(t, int64(2), gjson.Get(result.Stdout, "file_count").Int()) - assert.Greater(t, gjson.Get(result.Stdout, "total_size_bytes").Int(), int64(0)) - files := gjson.Get(result.Stdout, "files").Array() + assert.Equal(t, int64(2), clie2e.DryRunGet(result.Stdout, "file_count").Int()) + assert.Greater(t, clie2e.DryRunGet(result.Stdout, "total_size_bytes").Int(), int64(0)) + files := clie2e.DryRunGet(result.Stdout, "files").Array() require.Len(t, files, 2) names := []string{files[0].String(), files[1].String()} assert.Contains(t, names, "index.html") @@ -83,8 +82,8 @@ func TestAppsHTMLPublishDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, int64(1), gjson.Get(result.Stdout, "file_count").Int()) - assert.Equal(t, "page.html", gjson.Get(result.Stdout, "files.0").String()) + assert.Equal(t, int64(1), clie2e.DryRunGet(result.Stdout, "file_count").Int()) + assert.Equal(t, "page.html", clie2e.DryRunGet(result.Stdout, "files.0").String()) }) t.Run("HiddenFilesIncludedExceptGit", func(t *testing.T) { @@ -115,9 +114,9 @@ func TestAppsHTMLPublishDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) // index.html + .DS_Store kept; .git/HEAD filtered out → 2 files. - assert.Equal(t, int64(2), gjson.Get(result.Stdout, "file_count").Int(), + assert.Equal(t, int64(2), clie2e.DryRunGet(result.Stdout, "file_count").Int(), "walker must keep non-.git hidden files but drop .git; got: %s", result.Stdout) - names := gjson.Get(result.Stdout, "files").Array() + names := clie2e.DryRunGet(result.Stdout, "files").Array() var got []string for _, n := range names { got = append(got, n.String()) @@ -145,9 +144,9 @@ func TestAppsHTMLPublishDryRun(t *testing.T) { }) require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, int64(0), gjson.Get(result.Stdout, "file_count").Int()) - assert.Equal(t, int64(0), gjson.Get(result.Stdout, "total_size_bytes").Int()) - assert.Contains(t, gjson.Get(result.Stdout, "validation_error").String(), "index.html", + assert.Equal(t, int64(0), clie2e.DryRunGet(result.Stdout, "file_count").Int()) + assert.Equal(t, int64(0), clie2e.DryRunGet(result.Stdout, "total_size_bytes").Int()) + assert.Contains(t, clie2e.DryRunGet(result.Stdout, "validation_error").String(), "index.html", "empty dir should report index.html validation_error: %s", result.Stdout) }) @@ -171,9 +170,9 @@ func TestAppsHTMLPublishDryRun(t *testing.T) { }) require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, int64(1), gjson.Get(result.Stdout, "file_count").Int()) - assert.Equal(t, "page.html", gjson.Get(result.Stdout, "files.0").String()) - assert.Contains(t, gjson.Get(result.Stdout, "validation_error").String(), "index.html") + assert.Equal(t, int64(1), clie2e.DryRunGet(result.Stdout, "file_count").Int()) + assert.Equal(t, "page.html", clie2e.DryRunGet(result.Stdout, "files.0").String()) + assert.Contains(t, clie2e.DryRunGet(result.Stdout, "validation_error").String(), "index.html") }) t.Run("RejectsMissingAppID", func(t *testing.T) { @@ -269,7 +268,7 @@ func TestAppsHTMLPublishDryRun(t *testing.T) { }) require.NoError(t, err) result.AssertExitCode(t, 0) - waived := gjson.Get(result.Stdout, "sensitive_waived").Array() + waived := clie2e.DryRunGet(result.Stdout, "sensitive_waived").Array() require.Len(t, waived, 1, "expected sensitive_waived to list the file, got: %s", result.Stdout) assert.Equal(t, ".env.example", waived[0].String()) }) @@ -295,7 +294,7 @@ func TestAppsHTMLPublishDryRun(t *testing.T) { }) require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, int64(1), gjson.Get(result.Stdout, "file_count").Int()) + assert.Equal(t, int64(1), clie2e.DryRunGet(result.Stdout, "file_count").Int()) }) t.Run("TrimsAppIDAndPath", func(t *testing.T) { @@ -319,8 +318,8 @@ func TestAppsHTMLPublishDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) assert.Equal(t, "/open-apis/spark/v1/apps/app_x/upload_and_release_html_code", - gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, int64(1), gjson.Get(result.Stdout, "file_count").Int(), + clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.Equal(t, int64(1), clie2e.DryRunGet(result.Stdout, "file_count").Int(), "path trimming must produce the same manifest as untrimmed input") }) } diff --git a/tests/cli_e2e/apps/apps_list_dryrun_test.go b/tests/cli_e2e/apps/apps_list_dryrun_test.go index 882975bbc..656995232 100644 --- a/tests/cli_e2e/apps/apps_list_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_list_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestAppsListDryRun pins cursor-pagination params: default page_size=20 is @@ -31,10 +30,10 @@ func TestAppsListDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "GET", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/spark/v1/apps", gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, "20", gjson.Get(result.Stdout, "api.0.params.page_size").String()) - assert.False(t, gjson.Get(result.Stdout, "api.0.params.page_token").Exists(), + assert.Equal(t, "GET", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/spark/v1/apps", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.Equal(t, "20", clie2e.DryRunGet(result.Stdout, "api.0.params.page_size").String()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.params.page_token").Exists(), "empty page_token must be omitted") }) @@ -48,7 +47,7 @@ func TestAppsListDryRun(t *testing.T) { }) require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "50", gjson.Get(result.Stdout, "api.0.params.page_size").String()) + assert.Equal(t, "50", clie2e.DryRunGet(result.Stdout, "api.0.params.page_size").String()) }) t.Run("WithPageToken", func(t *testing.T) { @@ -61,8 +60,8 @@ func TestAppsListDryRun(t *testing.T) { }) require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "cursor_abc", gjson.Get(result.Stdout, "api.0.params.page_token").String()) - assert.Equal(t, "20", gjson.Get(result.Stdout, "api.0.params.page_size").String()) + assert.Equal(t, "cursor_abc", clie2e.DryRunGet(result.Stdout, "api.0.params.page_token").String()) + assert.Equal(t, "20", clie2e.DryRunGet(result.Stdout, "api.0.params.page_size").String()) }) t.Run("WithKeywordOwnershipAppType", func(t *testing.T) { @@ -77,9 +76,9 @@ func TestAppsListDryRun(t *testing.T) { }) require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "survey", gjson.Get(result.Stdout, "api.0.params.keyword").String()) - assert.Equal(t, "mine", gjson.Get(result.Stdout, "api.0.params.ownership").String()) - assert.Equal(t, "html", gjson.Get(result.Stdout, "api.0.params.app_type").String()) + assert.Equal(t, "survey", clie2e.DryRunGet(result.Stdout, "api.0.params.keyword").String()) + assert.Equal(t, "mine", clie2e.DryRunGet(result.Stdout, "api.0.params.ownership").String()) + assert.Equal(t, "html", clie2e.DryRunGet(result.Stdout, "api.0.params.app_type").String()) }) t.Run("OmitsEmptyFilters", func(t *testing.T) { @@ -93,7 +92,7 @@ func TestAppsListDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) for _, p := range []string{"keyword", "ownership", "app_type"} { - assert.False(t, gjson.Get(result.Stdout, "api.0.params."+p).Exists(), + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.params."+p).Exists(), "empty %s must be omitted", p) } }) @@ -134,6 +133,6 @@ func TestAppsListDryRun(t *testing.T) { }) require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "-1", gjson.Get(result.Stdout, "api.0.params.page_size").String()) + assert.Equal(t, "-1", clie2e.DryRunGet(result.Stdout, "api.0.params.page_size").String()) }) } diff --git a/tests/cli_e2e/apps/apps_update_dryrun_test.go b/tests/cli_e2e/apps/apps_update_dryrun_test.go index dc5d535c2..a82cb87b2 100644 --- a/tests/cli_e2e/apps/apps_update_dryrun_test.go +++ b/tests/cli_e2e/apps/apps_update_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestAppsUpdateDryRun pins partial-update semantics: PATCH with only the @@ -35,10 +34,10 @@ func TestAppsUpdateDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "PATCH", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/spark/v1/apps/app_x", gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, "v2", gjson.Get(result.Stdout, "api.0.body.name").String()) - assert.False(t, gjson.Get(result.Stdout, "api.0.body.description").Exists(), + assert.Equal(t, "PATCH", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/spark/v1/apps/app_x", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.Equal(t, "v2", clie2e.DryRunGet(result.Stdout, "api.0.body.name").String()) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api.0.body.description").Exists(), "description must be omitted when not provided") }) @@ -59,8 +58,8 @@ func TestAppsUpdateDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "v2", gjson.Get(result.Stdout, "api.0.body.name").String()) - assert.Equal(t, "updated", gjson.Get(result.Stdout, "api.0.body.description").String()) + assert.Equal(t, "v2", clie2e.DryRunGet(result.Stdout, "api.0.body.name").String()) + assert.Equal(t, "updated", clie2e.DryRunGet(result.Stdout, "api.0.body.description").String()) }) t.Run("RejectsMissingAppID", func(t *testing.T) { diff --git a/tests/cli_e2e/base/base_attachment_dryrun_test.go b/tests/cli_e2e/base/base_attachment_dryrun_test.go index 006ffdda3..877b5ccdd 100644 --- a/tests/cli_e2e/base/base_attachment_dryrun_test.go +++ b/tests/cli_e2e/base/base_attachment_dryrun_test.go @@ -12,7 +12,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestBase_AttachmentDryRun(t *testing.T) { @@ -42,10 +41,10 @@ func TestBase_AttachmentDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_att", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "/open-apis/drive/v1/medias/upload_all", gjson.Get(out, "api.1.url").String(), out) - require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/append_attachments", gjson.Get(out, "api.2.url").String(), out) - require.Equal(t, "", gjson.Get(out, "api.2.body.attachments.rec_x.fld_att.0.file_token").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_att", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "/open-apis/drive/v1/medias/upload_all", clie2e.DryRunGet(out, "api.1.url").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/append_attachments", clie2e.DryRunGet(out, "api.2.url").String(), out) + require.Equal(t, "", clie2e.DryRunGet(out, "api.2.body.attachments.rec_x.fld_att.0.file_token").String(), out) }) t.Run("download", func(t *testing.T) { @@ -65,9 +64,9 @@ func TestBase_AttachmentDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/get_attachments", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "/open-apis/drive/v1/medias/%3Cfile_token%3E/download", gjson.Get(out, "api.1.url").String(), out) - require.Equal(t, "", gjson.Get(out, "api.1.params.extra").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/get_attachments", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "/open-apis/drive/v1/medias/%3Cfile_token%3E/download", clie2e.DryRunGet(out, "api.1.url").String(), out) + require.Equal(t, "", clie2e.DryRunGet(out, "api.1.params.extra").String(), out) }) t.Run("download all", func(t *testing.T) { @@ -86,8 +85,8 @@ func TestBase_AttachmentDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/get_attachments", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "/open-apis/drive/v1/medias/%3Cfile_token%3E/download", gjson.Get(out, "api.1.url").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/get_attachments", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "/open-apis/drive/v1/medias/%3Cfile_token%3E/download", clie2e.DryRunGet(out, "api.1.url").String(), out) }) t.Run("remove", func(t *testing.T) { @@ -107,8 +106,8 @@ func TestBase_AttachmentDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/remove_attachments", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "box_a", gjson.Get(out, "api.0.body.attachments.rec_x.fld_att.0.file_token").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/remove_attachments", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "box_a", clie2e.DryRunGet(out, "api.0.body.attachments.rec_x.fld_att.0.file_token").String(), out) }) } diff --git a/tests/cli_e2e/base/base_block_dryrun_test.go b/tests/cli_e2e/base/base_block_dryrun_test.go index fab8c4235..16856761f 100644 --- a/tests/cli_e2e/base/base_block_dryrun_test.go +++ b/tests/cli_e2e/base/base_block_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestBaseBlockDryRun(t *testing.T) { @@ -31,9 +30,9 @@ func TestBaseBlockDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/list", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out) - require.False(t, gjson.Get(out, "api.0.body.parent_id").Exists(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/list", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), out) + require.False(t, clie2e.DryRunGet(out, "api.0.body.parent_id").Exists(), out) }) t.Run("list folder", func(t *testing.T) { @@ -50,9 +49,9 @@ func TestBaseBlockDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/list", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "blk_folder", gjson.Get(out, "api.0.body.parent_id").String(), out) - require.False(t, gjson.Get(out, "api.0.body.type").Exists(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/list", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "blk_folder", clie2e.DryRunGet(out, "api.0.body.parent_id").String(), out) + require.False(t, clie2e.DryRunGet(out, "api.0.body.type").Exists(), out) }) t.Run("create", func(t *testing.T) { @@ -70,11 +69,11 @@ func TestBaseBlockDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out) - require.Equal(t, "docx", gjson.Get(out, "api.0.body.type").String(), out) - require.Equal(t, "Spec", gjson.Get(out, "api.0.body.name").String(), out) - require.Equal(t, "blk_folder", gjson.Get(out, "api.0.body.parent_id").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), out) + require.Equal(t, "docx", clie2e.DryRunGet(out, "api.0.body.type").String(), out) + require.Equal(t, "Spec", clie2e.DryRunGet(out, "api.0.body.name").String(), out) + require.Equal(t, "blk_folder", clie2e.DryRunGet(out, "api.0.body.parent_id").String(), out) }) t.Run("move root", func(t *testing.T) { @@ -90,10 +89,10 @@ func TestBaseBlockDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/blk_a/move", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out) - require.True(t, gjson.Get(out, "api.0.body.parent_id").Exists(), out) - require.Equal(t, "Null", gjson.Get(out, "api.0.body.parent_id").Type.String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/blk_a/move", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), out) + require.True(t, clie2e.DryRunGet(out, "api.0.body.parent_id").Exists(), out) + require.Equal(t, "Null", clie2e.DryRunGet(out, "api.0.body.parent_id").Type.String(), out) }) t.Run("move after", func(t *testing.T) { @@ -111,9 +110,9 @@ func TestBaseBlockDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/blk_a/move", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "blk_folder", gjson.Get(out, "api.0.body.parent_id").String(), out) - require.Equal(t, "blk_b", gjson.Get(out, "api.0.body.after_id").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/blk_a/move", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "blk_folder", clie2e.DryRunGet(out, "api.0.body.parent_id").String(), out) + require.Equal(t, "blk_b", clie2e.DryRunGet(out, "api.0.body.after_id").String(), out) }) t.Run("rename", func(t *testing.T) { @@ -130,9 +129,9 @@ func TestBaseBlockDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/blk_a/rename", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out) - require.Equal(t, "Renamed", gjson.Get(out, "api.0.body.name").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/blk_a/rename", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), out) + require.Equal(t, "Renamed", clie2e.DryRunGet(out, "api.0.body.name").String(), out) }) t.Run("delete", func(t *testing.T) { @@ -148,7 +147,7 @@ func TestBaseBlockDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/blk_a", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "DELETE", gjson.Get(out, "api.0.method").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/blocks/blk_a", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "DELETE", clie2e.DryRunGet(out, "api.0.method").String(), out) }) } diff --git a/tests/cli_e2e/base/base_create_dryrun_test.go b/tests/cli_e2e/base/base_create_dryrun_test.go index 6469b5bde..539a3576a 100644 --- a/tests/cli_e2e/base/base_create_dryrun_test.go +++ b/tests/cli_e2e/base/base_create_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestBaseCreateDryRun(t *testing.T) { @@ -34,24 +33,24 @@ func TestBaseCreateDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out) - require.Equal(t, "Project Tracker", gjson.Get(out, "api.0.body.name").String(), out) - require.Equal(t, "Asia/Shanghai", gjson.Get(out, "api.0.body.time_zone").String(), out) + require.Equal(t, "/open-apis/base/v3/bases", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), out) + require.Equal(t, "Project Tracker", clie2e.DryRunGet(out, "api.0.body.name").String(), out) + require.Equal(t, "Asia/Shanghai", clie2e.DryRunGet(out, "api.0.body.time_zone").String(), out) - require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables", gjson.Get(out, "api.1.url").String(), out) - require.Equal(t, "GET", gjson.Get(out, "api.1.method").String(), out) - require.Equal(t, int64(0), gjson.Get(out, "api.1.params.offset").Int(), out) - require.Equal(t, int64(100), gjson.Get(out, "api.1.params.limit").Int(), out) + require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables", clie2e.DryRunGet(out, "api.1.url").String(), out) + require.Equal(t, "GET", clie2e.DryRunGet(out, "api.1.method").String(), out) + require.Equal(t, int64(0), clie2e.DryRunGet(out, "api.1.params.offset").Int(), out) + require.Equal(t, int64(100), clie2e.DryRunGet(out, "api.1.params.limit").Int(), out) - require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables", gjson.Get(out, "api.2.url").String(), out) - require.Equal(t, "POST", gjson.Get(out, "api.2.method").String(), out) - require.Equal(t, "Tasks", gjson.Get(out, "api.2.body.name").String(), out) - require.Equal(t, "Title", gjson.Get(out, "api.2.body.fields.0.name").String(), out) - require.Equal(t, "Status", gjson.Get(out, "api.2.body.fields.1.name").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables", clie2e.DryRunGet(out, "api.2.url").String(), out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.2.method").String(), out) + require.Equal(t, "Tasks", clie2e.DryRunGet(out, "api.2.body.name").String(), out) + require.Equal(t, "Title", clie2e.DryRunGet(out, "api.2.body.fields.0.name").String(), out) + require.Equal(t, "Status", clie2e.DryRunGet(out, "api.2.body.fields.1.name").String(), out) - require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables/%3Cdefault_table_id%3E", gjson.Get(out, "api.3.url").String(), out) - require.Equal(t, "DELETE", gjson.Get(out, "api.3.method").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables/%3Cdefault_table_id%3E", clie2e.DryRunGet(out, "api.3.url").String(), out) + require.Equal(t, "DELETE", clie2e.DryRunGet(out, "api.3.method").String(), out) } func TestBaseCreateDryRunTableNameOnlyRenamesDefaultTable(t *testing.T) { @@ -73,17 +72,17 @@ func TestBaseCreateDryRunTableNameOnlyRenamesDefaultTable(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out) - require.Equal(t, "Project Tracker", gjson.Get(out, "api.0.body.name").String(), out) + require.Equal(t, "/open-apis/base/v3/bases", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), out) + require.Equal(t, "Project Tracker", clie2e.DryRunGet(out, "api.0.body.name").String(), out) - require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables", gjson.Get(out, "api.1.url").String(), out) - require.Equal(t, "GET", gjson.Get(out, "api.1.method").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables", clie2e.DryRunGet(out, "api.1.url").String(), out) + require.Equal(t, "GET", clie2e.DryRunGet(out, "api.1.method").String(), out) - require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables/%3Cdefault_table_id%3E", gjson.Get(out, "api.2.url").String(), out) - require.Equal(t, "PATCH", gjson.Get(out, "api.2.method").String(), out) - require.Equal(t, "Tasks", gjson.Get(out, "api.2.body.name").String(), out) - require.False(t, gjson.Get(out, "api.3").Exists(), out) + require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables/%3Cdefault_table_id%3E", clie2e.DryRunGet(out, "api.2.url").String(), out) + require.Equal(t, "PATCH", clie2e.DryRunGet(out, "api.2.method").String(), out) + require.Equal(t, "Tasks", clie2e.DryRunGet(out, "api.2.body.name").String(), out) + require.False(t, clie2e.DryRunGet(out, "api.3").Exists(), out) } func TestBaseCreateDryRunFieldsOnlyUsesDefaultTableName(t *testing.T) { @@ -105,8 +104,8 @@ func TestBaseCreateDryRunFieldsOnlyUsesDefaultTableName(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables", gjson.Get(out, "api.2.url").String(), out) - require.Equal(t, "POST", gjson.Get(out, "api.2.method").String(), out) - require.Equal(t, "Table 1", gjson.Get(out, "api.2.body.name").String(), out) - require.Equal(t, "Title", gjson.Get(out, "api.2.body.fields.0.name").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/%3Ccreated_base_token%3E/tables", clie2e.DryRunGet(out, "api.2.url").String(), out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.2.method").String(), out) + require.Equal(t, "Table 1", clie2e.DryRunGet(out, "api.2.body.name").String(), out) + require.Equal(t, "Title", clie2e.DryRunGet(out, "api.2.body.fields.0.name").String(), out) } diff --git a/tests/cli_e2e/base/base_field_dryrun_test.go b/tests/cli_e2e/base/base_field_dryrun_test.go index 9db5aff83..a98f3fc09 100644 --- a/tests/cli_e2e/base/base_field_dryrun_test.go +++ b/tests/cli_e2e/base/base_field_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestBaseFieldCreateDryRunArrayCompat(t *testing.T) { @@ -33,13 +32,13 @@ func TestBaseFieldCreateDryRunArrayCompat(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields", gjson.Get(out, "api.0.url").String(), out) - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), out) - require.Equal(t, "A", gjson.Get(out, "api.0.body.name").String(), out) - require.Equal(t, "text", gjson.Get(out, "api.0.body.type").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields", clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), out) + require.Equal(t, "A", clie2e.DryRunGet(out, "api.0.body.name").String(), out) + require.Equal(t, "text", clie2e.DryRunGet(out, "api.0.body.type").String(), out) - require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields", gjson.Get(out, "api.1.url").String(), out) - require.Equal(t, "POST", gjson.Get(out, "api.1.method").String(), out) - require.Equal(t, "B", gjson.Get(out, "api.1.body.name").String(), out) - require.Equal(t, "text", gjson.Get(out, "api.1.body.type").String(), out) + require.Equal(t, "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields", clie2e.DryRunGet(out, "api.1.url").String(), out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.1.method").String(), out) + require.Equal(t, "B", clie2e.DryRunGet(out, "api.1.body.name").String(), out) + require.Equal(t, "text", clie2e.DryRunGet(out, "api.1.body.type").String(), out) } diff --git a/tests/cli_e2e/base/base_limit_dryrun_test.go b/tests/cli_e2e/base/base_limit_dryrun_test.go index 0fdbd3653..fe740d94e 100644 --- a/tests/cli_e2e/base/base_limit_dryrun_test.go +++ b/tests/cli_e2e/base/base_limit_dryrun_test.go @@ -56,9 +56,9 @@ func TestBaseListDryRunAcceptsPageSizeAliasForLimit(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - require.Equal(t, int64(0), gjson.Get(result.Stdout, "api.0.params.offset").Int(), result.Stdout) - require.Equal(t, int64(40), gjson.Get(result.Stdout, "api.0.params.limit").Int(), result.Stdout) - require.False(t, gjson.Get(result.Stdout, "api.0.params.page_size").Exists(), result.Stdout) + require.Equal(t, int64(0), clie2e.DryRunGet(result.Stdout, "api.0.params.offset").Int(), result.Stdout) + require.Equal(t, int64(40), clie2e.DryRunGet(result.Stdout, "api.0.params.limit").Int(), result.Stdout) + require.False(t, clie2e.DryRunGet(result.Stdout, "api.0.params.page_size").Exists(), result.Stdout) } func TestBaseListDryRunRejectsLimitPageSizeConflict(t *testing.T) { diff --git a/tests/cli_e2e/calendar/calendar_update_dryrun_test.go b/tests/cli_e2e/calendar/calendar_update_dryrun_test.go index b0f56ff6a..3d95eaa0c 100644 --- a/tests/cli_e2e/calendar/calendar_update_dryrun_test.go +++ b/tests/cli_e2e/calendar/calendar_update_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestCalendar_UpdateDryRun(t *testing.T) { @@ -41,19 +40,19 @@ func TestCalendar_UpdateDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "PATCH", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - require.Equal(t, "/open-apis/calendar/v4/calendars/cal_dry/events/evt_dry", gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) - require.Equal(t, "updated dry-run", gjson.Get(out, "api.0.body.summary").String(), "stdout:\n%s", out) - require.False(t, gjson.Get(out, "api.0.body.need_notification").Bool(), "stdout:\n%s", out) + require.Equal(t, "PATCH", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + require.Equal(t, "/open-apis/calendar/v4/calendars/cal_dry/events/evt_dry", clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) + require.Equal(t, "updated dry-run", clie2e.DryRunGet(out, "api.0.body.summary").String(), "stdout:\n%s", out) + require.False(t, clie2e.DryRunGet(out, "api.0.body.need_notification").Bool(), "stdout:\n%s", out) - require.Equal(t, "POST", gjson.Get(out, "api.1.method").String(), "stdout:\n%s", out) - require.Equal(t, "/open-apis/calendar/v4/calendars/cal_dry/events/evt_dry/attendees/batch_delete", gjson.Get(out, "api.1.url").String(), "stdout:\n%s", out) - require.Equal(t, "ou_old", gjson.Get(out, `api.1.body.delete_ids.#(type=="user").user_id`).String(), "stdout:\n%s", out) - require.Equal(t, "omm_oldroom", gjson.Get(out, `api.1.body.delete_ids.#(type=="resource").room_id`).String(), "stdout:\n%s", out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.1.method").String(), "stdout:\n%s", out) + require.Equal(t, "/open-apis/calendar/v4/calendars/cal_dry/events/evt_dry/attendees/batch_delete", clie2e.DryRunGet(out, "api.1.url").String(), "stdout:\n%s", out) + require.Equal(t, "ou_old", clie2e.DryRunGet(out, `api.1.body.delete_ids.#(type=="user").user_id`).String(), "stdout:\n%s", out) + require.Equal(t, "omm_oldroom", clie2e.DryRunGet(out, `api.1.body.delete_ids.#(type=="resource").room_id`).String(), "stdout:\n%s", out) - require.Equal(t, "POST", gjson.Get(out, "api.2.method").String(), "stdout:\n%s", out) - require.Equal(t, "/open-apis/calendar/v4/calendars/cal_dry/events/evt_dry/attendees", gjson.Get(out, "api.2.url").String(), "stdout:\n%s", out) - require.Equal(t, "ou_new", gjson.Get(out, `api.2.body.attendees.#(type=="user").user_id`).String(), "stdout:\n%s", out) - require.Equal(t, "oc_group", gjson.Get(out, `api.2.body.attendees.#(type=="chat").chat_id`).String(), "stdout:\n%s", out) - require.Equal(t, "omm_newroom", gjson.Get(out, `api.2.body.attendees.#(type=="resource").room_id`).String(), "stdout:\n%s", out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.2.method").String(), "stdout:\n%s", out) + require.Equal(t, "/open-apis/calendar/v4/calendars/cal_dry/events/evt_dry/attendees", clie2e.DryRunGet(out, "api.2.url").String(), "stdout:\n%s", out) + require.Equal(t, "ou_new", clie2e.DryRunGet(out, `api.2.body.attendees.#(type=="user").user_id`).String(), "stdout:\n%s", out) + require.Equal(t, "oc_group", clie2e.DryRunGet(out, `api.2.body.attendees.#(type=="chat").chat_id`).String(), "stdout:\n%s", out) + require.Equal(t, "omm_newroom", clie2e.DryRunGet(out, `api.2.body.attendees.#(type=="resource").room_id`).String(), "stdout:\n%s", out) } diff --git a/tests/cli_e2e/core.go b/tests/cli_e2e/core.go index adc296234..cd9d7a739 100644 --- a/tests/cli_e2e/core.go +++ b/tests/cli_e2e/core.go @@ -75,6 +75,19 @@ func SkipWithoutUserToken(t *testing.T) { } } +// DryRunGet reads a field from the dry-run payload inside the standard success envelope. +func DryRunGet(stdout, path string) gjson.Result { + if path == "" { + return gjson.Get(stdout, "data") + } + return gjson.Get(stdout, "data."+path) +} + +// DryRunData returns the dry-run payload for tests that assert legacy raw paths. +func DryRunData(stdout string) string { + return gjson.Get(stdout, "data").Raw +} + // Request describes one lark-cli invocation. type Request struct { // Args are required and exclude the lark-cli binary name. diff --git a/tests/cli_e2e/docs/docs_fetch_dryrun_test.go b/tests/cli_e2e/docs/docs_fetch_dryrun_test.go index 2a4c513bc..46a5cd047 100644 --- a/tests/cli_e2e/docs/docs_fetch_dryrun_test.go +++ b/tests/cli_e2e/docs/docs_fetch_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestDocsFetchDryRunIgnoresAPIVersionCompatFlag(t *testing.T) { @@ -32,13 +31,13 @@ func TestDocsFetchDryRunIgnoresAPIVersionCompatFlag(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "POST" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "POST" { t.Fatalf("method=%q, want POST\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/docs_ai/v1/documents/doxcnDryRunCompat/fetch" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/docs_ai/v1/documents/doxcnDryRunCompat/fetch" { t.Fatalf("url=%q, want docs fetch endpoint\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.format").String(); got != "xml" { + if got := clie2e.DryRunGet(out, "api.0.body.format").String(); got != "xml" { t.Fatalf("format=%q, want xml\nstdout:\n%s", got, out) } } @@ -61,13 +60,13 @@ func TestDocsFetchDryRunSelectionAnchorFragmentBecomesRangeStart(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/docs_ai/v1/documents/wikcnDryRun/fetch" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/docs_ai/v1/documents/wikcnDryRun/fetch" { t.Fatalf("url=%q, want docs fetch endpoint\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.read_option.read_mode").String(); got != "range" { + if got := clie2e.DryRunGet(out, "api.0.body.read_option.read_mode").String(); got != "range" { t.Fatalf("read_mode=%q, want range\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.read_option.start_block_id").String(); got != "share-CUE3d6Ykno2fkexEvt8cGF8Wnse" { + if got := clie2e.DryRunGet(out, "api.0.body.read_option.start_block_id").String(); got != "share-CUE3d6Ykno2fkexEvt8cGF8Wnse" { t.Fatalf("start_block_id=%q, want selection anchor\nstdout:\n%s", got, out) } } @@ -90,7 +89,7 @@ func TestDocsFetchDryRunUnsupportedSelectionAnchorFragmentStaysFull(t *testing.T result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.body.read_option").Raw; got != "" { + if got := clie2e.DryRunGet(out, "api.0.body.read_option").Raw; got != "" { t.Fatalf("read_option=%s, want omitted for unsupported selection anchor\nstdout:\n%s", got, out) } } diff --git a/tests/cli_e2e/docs/docs_update_dryrun_test.go b/tests/cli_e2e/docs/docs_update_dryrun_test.go index 1a6aaa29a..5219fa9f5 100644 --- a/tests/cli_e2e/docs/docs_update_dryrun_test.go +++ b/tests/cli_e2e/docs/docs_update_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) { @@ -164,7 +163,7 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) { t.Fatalf("dry-run output should not ask for --api-version\nstdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) } if tt.wantURL != "" { - require.Equal(t, tt.wantURL, gjson.Get(result.Stdout, "api.0.url").String(), "stdout:\n%s", result.Stdout) + require.Equal(t, tt.wantURL, clie2e.DryRunGet(result.Stdout, "api.0.url").String(), "stdout:\n%s", result.Stdout) } for key, want := range tt.wantParams { assertDryRunField(t, result.Stdout, "api.0.params."+key, want) @@ -173,11 +172,11 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) { assertDryRunField(t, result.Stdout, "api.0.body."+key, want) } if tt.wantExtraParam != "" { - extraParam := gjson.Get(result.Stdout, "api.0.body.extra_param").String() + extraParam := clie2e.DryRunGet(result.Stdout, "api.0.body.extra_param").String() require.JSONEq(t, tt.wantExtraParam, extraParam, "stdout:\n%s", result.Stdout) } if tt.wantRefLabel != "" { - got := gjson.Get(result.Stdout, "api.0.body.reference_map.widget.r1.label").String() + got := clie2e.DryRunGet(result.Stdout, "api.0.body.reference_map.widget.r1.label").String() require.Equal(t, tt.wantRefLabel, got, "stdout:\n%s", result.Stdout) } }) @@ -187,7 +186,7 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) { func assertDryRunField(t *testing.T, stdout, path string, want any) { t.Helper() - got := gjson.Get(stdout, path) + got := clie2e.DryRunGet(stdout, path) require.True(t, got.Exists(), "%s missing in stdout:\n%s", path, stdout) switch want := want.(type) { case int: @@ -222,7 +221,7 @@ func TestDocs_CreateTitleDryRunPrependsContent(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "/open-apis/docs_ai/v1/documents", gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) - require.Equal(t, "markdown", gjson.Get(out, "api.0.body.format").String(), "stdout:\n%s", out) - require.Equal(t, "Dry Run & Title\n## Body", gjson.Get(out, "api.0.body.content").String(), "stdout:\n%s", out) + require.Equal(t, "/open-apis/docs_ai/v1/documents", clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) + require.Equal(t, "markdown", clie2e.DryRunGet(out, "api.0.body.format").String(), "stdout:\n%s", out) + require.Equal(t, "Dry Run & Title\n## Body", clie2e.DryRunGet(out, "api.0.body.content").String(), "stdout:\n%s", out) } diff --git a/tests/cli_e2e/drive/drive_add_comment_dryrun_test.go b/tests/cli_e2e/drive/drive_add_comment_dryrun_test.go index 50635aa0c..168b69fe6 100644 --- a/tests/cli_e2e/drive/drive_add_comment_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_add_comment_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestDriveAddCommentDryRun_File(t *testing.T) { @@ -32,23 +31,23 @@ func TestDriveAddCommentDryRun_File(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/metas/batch_query" { - t.Fatalf("api.0.url=%q, want metas/batch_query\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/drive/v1/metas/batch_query" { + t.Fatalf("data.api.0.url=%q, want metas/batch_query\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.request_docs.0.doc_type").String(); got != "file" { - t.Fatalf("api.0.body.request_docs.0.doc_type=%q, want file\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.0.body.request_docs.0.doc_type").String(); got != "file" { + t.Fatalf("data.api.0.body.request_docs.0.doc_type=%q, want file\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.url").String(); got != "/open-apis/drive/v1/files/fileDryRunComment/new_comments" { - t.Fatalf("api.1.url=%q, want new_comments\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.1.url").String(); got != "/open-apis/drive/v1/files/fileDryRunComment/new_comments" { + t.Fatalf("data.api.1.url=%q, want new_comments\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.body.file_type").String(); got != "file" { - t.Fatalf("api.1.body.file_type=%q, want file\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.1.body.file_type").String(); got != "file" { + t.Fatalf("data.api.1.body.file_type=%q, want file\nstdout:\n%s", got, out) } - if !gjson.Get(out, "api.1.body.anchor.block_id").Exists() { - t.Fatalf("api.1.body.anchor.block_id should exist for file comment\nstdout:\n%s", out) + if !clie2e.DryRunGet(out, "api.1.body.anchor.block_id").Exists() { + t.Fatalf("data.api.1.body.anchor.block_id should exist for file comment\nstdout:\n%s", out) } - if got := gjson.Get(out, "api.1.body.anchor.block_id").String(); got != "test" { - t.Fatalf("api.1.body.anchor.block_id=%q, want test\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.1.body.anchor.block_id").String(); got != "test" { + t.Fatalf("data.api.1.body.anchor.block_id=%q, want test\nstdout:\n%s", got, out) } } @@ -72,19 +71,19 @@ func TestDriveAddCommentDryRun_Base(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/files/baseDryRunComment/new_comments" { - t.Fatalf("api.0.url=%q, want new_comments\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/drive/v1/files/baseDryRunComment/new_comments" { + t.Fatalf("data.api.0.url=%q, want new_comments\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.file_type").String(); got != "bitable" { - t.Fatalf("api.0.body.file_type=%q, want bitable\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.0.body.file_type").String(); got != "bitable" { + t.Fatalf("data.api.0.body.file_type=%q, want bitable\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.anchor.block_id").String(); got != "tbl9mp6fj9kDKHQV" { - t.Fatalf("api.0.body.anchor.block_id=%q, want tbl9mp6fj9kDKHQV\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.0.body.anchor.block_id").String(); got != "tbl9mp6fj9kDKHQV" { + t.Fatalf("data.api.0.body.anchor.block_id=%q, want tbl9mp6fj9kDKHQV\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.anchor.base_record_id").String(); got != "recBIBgGmb" { - t.Fatalf("api.0.body.anchor.base_record_id=%q, want recBIBgGmb\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.0.body.anchor.base_record_id").String(); got != "recBIBgGmb" { + t.Fatalf("data.api.0.body.anchor.base_record_id=%q, want recBIBgGmb\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.anchor.base_view_id").String(); got != "vewc46MG1R" { - t.Fatalf("api.0.body.anchor.base_view_id=%q, want vewc46MG1R\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.0.body.anchor.base_view_id").String(); got != "vewc46MG1R" { + t.Fatalf("data.api.0.body.anchor.base_view_id=%q, want vewc46MG1R\nstdout:\n%s", got, out) } } diff --git a/tests/cli_e2e/drive/drive_apply_permission_dryrun_test.go b/tests/cli_e2e/drive/drive_apply_permission_dryrun_test.go index 9d9e17eab..b00ec2000 100644 --- a/tests/cli_e2e/drive/drive_apply_permission_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_apply_permission_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestDrive_ApplyPermissionDryRun locks in the request shape the shortcut @@ -127,20 +126,20 @@ func TestDrive_ApplyPermissionDryRun(t *testing.T) { out := result.Stdout // Dry-run output is the JSON envelope; gjson walks into api[0]. - if got := gjson.Get(out, "api.0.method").String(); got != "POST" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "POST" { t.Fatalf("method = %q, want POST\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != tt.wantURL { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != tt.wantURL { t.Fatalf("url = %q, want %q\nstdout:\n%s", got, tt.wantURL, out) } - if got := gjson.Get(out, "api.0.params.type").String(); got != tt.wantType { + if got := clie2e.DryRunGet(out, "api.0.params.type").String(); got != tt.wantType { t.Fatalf("params.type = %q, want %q\nstdout:\n%s", got, tt.wantType, out) } - if got := gjson.Get(out, "api.0.body.perm").String(); got != tt.wantPerm { + if got := clie2e.DryRunGet(out, "api.0.body.perm").String(); got != tt.wantPerm { t.Fatalf("body.perm = %q, want %q\nstdout:\n%s", got, tt.wantPerm, out) } for k, v := range tt.wantBody { - if got := gjson.Get(out, "api.0.body."+k).String(); got != v { + if got := clie2e.DryRunGet(out, "api.0.body."+k).String(); got != v { t.Fatalf("body.%s = %q, want %q\nstdout:\n%s", k, got, v, out) } } @@ -148,7 +147,7 @@ func TestDrive_ApplyPermissionDryRun(t *testing.T) { // remark field (the owner's request card would otherwise render // a blank note). if _, wantsRemark := tt.wantBody["remark"]; !wantsRemark { - if gjson.Get(out, "api.0.body.remark").Exists() { + if clie2e.DryRunGet(out, "api.0.body.remark").Exists() { t.Fatalf("body.remark should be omitted when --remark is empty, stdout:\n%s", out) } } diff --git a/tests/cli_e2e/drive/drive_export_dryrun_test.go b/tests/cli_e2e/drive/drive_export_dryrun_test.go index 6fb541c15..1f5f5465f 100644 --- a/tests/cli_e2e/drive/drive_export_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_export_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestDriveExportDryRun_FileNameMetadata(t *testing.T) { @@ -35,28 +34,28 @@ func TestDriveExportDryRun_FileNameMetadata(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "POST" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "POST" { t.Fatalf("method=%q, want POST\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/export_tasks" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/drive/v1/export_tasks" { t.Fatalf("url=%q, want export_tasks\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.token").String(); got != "docxDryRunExport" { + if got := clie2e.DryRunGet(out, "api.0.body.token").String(); got != "docxDryRunExport" { t.Fatalf("body.token=%q, want docxDryRunExport\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.type").String(); got != "docx" { + if got := clie2e.DryRunGet(out, "api.0.body.type").String(); got != "docx" { t.Fatalf("body.type=%q, want docx\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.file_extension").String(); got != "pdf" { + if got := clie2e.DryRunGet(out, "api.0.body.file_extension").String(); got != "pdf" { t.Fatalf("body.file_extension=%q, want pdf\nstdout:\n%s", got, out) } - if gjson.Get(out, "api.0.body.file_name").Exists() { + if clie2e.DryRunGet(out, "api.0.body.file_name").Exists() { t.Fatalf("file_name should stay local metadata, not export_tasks body\nstdout:\n%s", out) } - if got := gjson.Get(out, "file_name").String(); got != "custom-report.pdf" { + if got := clie2e.DryRunGet(out, "file_name").String(); got != "custom-report.pdf" { t.Fatalf("file_name=%q, want custom-report.pdf\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "output_dir").String(); got != "./exports" { + if got := clie2e.DryRunGet(out, "output_dir").String(); got != "./exports" { t.Fatalf("output_dir=%q, want ./exports\nstdout:\n%s", got, out) } } @@ -82,31 +81,31 @@ func TestDriveExportDryRun_WikiURLPlansResolveBeforeExportTask(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("api.0.method=%q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/wiki/v2/spaces/get_node" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/wiki/v2/spaces/get_node" { t.Fatalf("api.0.url=%q, want wiki get_node\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.token").String(); got != "wikiDryRunExport" { + if got := clie2e.DryRunGet(out, "api.0.params.token").String(); got != "wikiDryRunExport" { t.Fatalf("api.0.params.token=%q, want wiki token\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.method").String(); got != "POST" { + if got := clie2e.DryRunGet(out, "api.1.method").String(); got != "POST" { t.Fatalf("api.1.method=%q, want POST\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.url").String(); got != "/open-apis/drive/v1/export_tasks" { + if got := clie2e.DryRunGet(out, "api.1.url").String(); got != "/open-apis/drive/v1/export_tasks" { t.Fatalf("api.1.url=%q, want export_tasks\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.body.token").String(); got != "obj_token_from_step_0" { + if got := clie2e.DryRunGet(out, "api.1.body.token").String(); got != "obj_token_from_step_0" { t.Fatalf("api.1.body.token=%q, want resolved token placeholder\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.body.type").String(); got != "obj_type_from_step_0" { + if got := clie2e.DryRunGet(out, "api.1.body.type").String(); got != "obj_type_from_step_0" { t.Fatalf("api.1.body.type=%q, want resolved type placeholder\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "wiki_token").String(); got != "wikiDryRunExport" { + if got := clie2e.DryRunGet(out, "wiki_token").String(); got != "wikiDryRunExport" { t.Fatalf("wiki_token=%q, want source wiki token\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "file_name").String(); got != "wiki-report.pdf" { + if got := clie2e.DryRunGet(out, "file_name").String(); got != "wiki-report.pdf" { t.Fatalf("file_name=%q, want wiki-report.pdf\nstdout:\n%s", got, out) } } @@ -131,22 +130,22 @@ func TestDriveExportDryRun_WikiTokenTypePlansResolveBeforeExportTask(t *testing. result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("api.0.method=%q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/wiki/v2/spaces/get_node" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/wiki/v2/spaces/get_node" { t.Fatalf("api.0.url=%q, want wiki get_node\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.token").String(); got != "wikiDryRunExport" { + if got := clie2e.DryRunGet(out, "api.0.params.token").String(); got != "wikiDryRunExport" { t.Fatalf("api.0.params.token=%q, want wiki token\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.body.token").String(); got != "obj_token_from_step_0" { + if got := clie2e.DryRunGet(out, "api.1.body.token").String(); got != "obj_token_from_step_0" { t.Fatalf("api.1.body.token=%q, want resolved token placeholder\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.body.type").String(); got != "obj_type_from_step_0" { + if got := clie2e.DryRunGet(out, "api.1.body.type").String(); got != "obj_type_from_step_0" { t.Fatalf("api.1.body.type=%q, want resolved type placeholder\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "wiki_token").String(); got != "wikiDryRunExport" { + if got := clie2e.DryRunGet(out, "wiki_token").String(); got != "wikiDryRunExport" { t.Fatalf("wiki_token=%q, want source wiki token\nstdout:\n%s", got, out) } } @@ -173,22 +172,22 @@ func TestDriveExportDryRun_MarkdownFetchAPI(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "POST" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "POST" { t.Fatalf("method=%q, want POST\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/docs_ai/v1/documents/docxMdDryRun/fetch" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/docs_ai/v1/documents/docxMdDryRun/fetch" { t.Fatalf("url=%q, want docs_ai fetch\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.format").String(); got != "markdown" { + if got := clie2e.DryRunGet(out, "api.0.body.format").String(); got != "markdown" { t.Fatalf("body.format=%q, want markdown\nstdout:\n%s", got, out) } - if gjson.Get(out, "api.0.body.extra_param").Exists() { + if clie2e.DryRunGet(out, "api.0.body.extra_param").Exists() { t.Fatalf("markdown drive export must not enable docs fetch extra_param\nstdout:\n%s", out) } - if got := gjson.Get(out, "file_name").String(); got != "my-notes.md" { + if got := clie2e.DryRunGet(out, "file_name").String(); got != "my-notes.md" { t.Fatalf("file_name=%q, want my-notes.md\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "output_dir").String(); got != "./md-exports" { + if got := clie2e.DryRunGet(out, "output_dir").String(); got != "./md-exports" { t.Fatalf("output_dir=%q, want ./md-exports\nstdout:\n%s", got, out) } } @@ -214,22 +213,22 @@ func TestDriveExportDryRun_BitableBaseOnlySchema(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "POST" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "POST" { t.Fatalf("method=%q, want POST\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/export_tasks" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/drive/v1/export_tasks" { t.Fatalf("url=%q, want export_tasks\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.token").String(); got != "bitableDryRunExport" { + if got := clie2e.DryRunGet(out, "api.0.body.token").String(); got != "bitableDryRunExport" { t.Fatalf("body.token=%q, want bitableDryRunExport\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.type").String(); got != "bitable" { + if got := clie2e.DryRunGet(out, "api.0.body.type").String(); got != "bitable" { t.Fatalf("body.type=%q, want bitable\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.file_extension").String(); got != "base" { + if got := clie2e.DryRunGet(out, "api.0.body.file_extension").String(); got != "base" { t.Fatalf("body.file_extension=%q, want base\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.only_schema").Bool(); !got { + if got := clie2e.DryRunGet(out, "api.0.body.only_schema").Bool(); !got { t.Fatalf("body.only_schema=%v, want true\nstdout:\n%s", got, out) } } diff --git a/tests/cli_e2e/drive/drive_import_dryrun_test.go b/tests/cli_e2e/drive/drive_import_dryrun_test.go index baaabf6b3..e53b831cf 100644 --- a/tests/cli_e2e/drive/drive_import_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_import_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestDriveImportDryRunFolderTokenWikiProbe(t *testing.T) { @@ -40,19 +39,19 @@ func TestDriveImportDryRunFolderTokenWikiProbe(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { - t.Fatalf("api.0.method = %q, want GET\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { + t.Fatalf("data.api.0.method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/wiki/v2/spaces/get_node" { - t.Fatalf("api.0.url = %q, want wiki get_node\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/wiki/v2/spaces/get_node" { + t.Fatalf("data.api.0.url = %q, want wiki get_node\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.token").String(); got != "fldcnImportDryRunTarget" { - t.Fatalf("api.0.params.token = %q, want fldcnImportDryRunTarget\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.0.params.token").String(); got != "fldcnImportDryRunTarget" { + t.Fatalf("data.api.0.params.token = %q, want fldcnImportDryRunTarget\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.url").String(); got != "/open-apis/drive/v1/medias/upload_all" { - t.Fatalf("api.1.url = %q, want upload_all\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.1.url").String(); got != "/open-apis/drive/v1/medias/upload_all" { + t.Fatalf("data.api.1.url = %q, want upload_all\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.2.body.point.mount_key").String(); got != "fldcnImportDryRunTarget" { - t.Fatalf("api.2.body.point.mount_key = %q, want fldcnImportDryRunTarget\nstdout:\n%s", got, out) + if got := clie2e.DryRunGet(out, "api.2.body.point.mount_key").String(); got != "fldcnImportDryRunTarget" { + t.Fatalf("data.api.2.body.point.mount_key = %q, want fldcnImportDryRunTarget\nstdout:\n%s", got, out) } } diff --git a/tests/cli_e2e/drive/drive_inspect_dryrun_test.go b/tests/cli_e2e/drive/drive_inspect_dryrun_test.go index 8d375ad33..74e5ea4ea 100644 --- a/tests/cli_e2e/drive/drive_inspect_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_inspect_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // --- Happy path: all supported URL types --- @@ -100,13 +99,13 @@ func TestDriveInspectDryRun_WikiURL(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - require.Equal(t, int64(2), gjson.Get(result.Stdout, "api.#").Int(), + require.Equal(t, int64(2), clie2e.DryRunGet(result.Stdout, "api.#").Int(), "expected exactly 2 dry-run API steps for wiki URL, stdout:\n%s", result.Stdout) require.Equal(t, "/open-apis/wiki/v2/spaces/get_node", - gjson.Get(result.Stdout, "api.0.url").String(), + clie2e.DryRunGet(result.Stdout, "api.0.url").String(), "expected get_node as first step, stdout:\n%s", result.Stdout) require.Equal(t, "/open-apis/drive/v1/metas/batch_query", - gjson.Get(result.Stdout, "api.1.url").String(), + clie2e.DryRunGet(result.Stdout, "api.1.url").String(), "expected batch_query as second step, stdout:\n%s", result.Stdout) } @@ -239,10 +238,10 @@ func runInspectDryRun(t *testing.T, url string) *clie2e.Result { func assertOneStepBatchQuery(t *testing.T, result *clie2e.Result) { t.Helper() - require.Equal(t, int64(1), gjson.Get(result.Stdout, "api.#").Int(), + require.Equal(t, int64(1), clie2e.DryRunGet(result.Stdout, "api.#").Int(), "expected exactly 1 dry-run API step, stdout:\n%s", result.Stdout) require.Equal(t, "/open-apis/drive/v1/metas/batch_query", - gjson.Get(result.Stdout, "api.0.url").String(), + clie2e.DryRunGet(result.Stdout, "api.0.url").String(), "expected batch_query URL, stdout:\n%s", result.Stdout) } diff --git a/tests/cli_e2e/drive/drive_list_comments_dryrun_test.go b/tests/cli_e2e/drive/drive_list_comments_dryrun_test.go index ef4b8f869..99b78fb97 100644 --- a/tests/cli_e2e/drive/drive_list_comments_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_list_comments_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestDriveListCommentsDryRun_DocxDefaults(t *testing.T) { @@ -31,23 +30,23 @@ func TestDriveListCommentsDryRun_DocxDefaults(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/files/docxDryRunCommentList/comments" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/drive/v1/files/docxDryRunCommentList/comments" { t.Fatalf("api.0.url=%q, want comments list\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.file_type").String(); got != "docx" { + if got := clie2e.DryRunGet(out, "api.0.params.file_type").String(); got != "docx" { t.Fatalf("api.0.params.file_type=%q, want docx\nstdout:\n%s", got, out) } - isSolved := gjson.Get(out, "api.0.params.is_solved") + isSolved := clie2e.DryRunGet(out, "api.0.params.is_solved") if !isSolved.Exists() || isSolved.Bool() { t.Fatalf("api.0.params.is_solved=%v, want explicit false\nstdout:\n%s", isSolved.Value(), out) } - if gjson.Get(out, "api.0.params.is_whole").Exists() { + if clie2e.DryRunGet(out, "api.0.params.is_whole").Exists() { t.Fatalf("api.0.params.is_whole should be omitted by default\nstdout:\n%s", out) } - if got := gjson.Get(out, "api.0.params.page_size").Int(); got != 50 { + if got := clie2e.DryRunGet(out, "api.0.params.page_size").Int(); got != 50 { t.Fatalf("api.0.params.page_size=%d, want 50\nstdout:\n%s", got, out) } - if gjson.Get(out, "api.0.params.user_id_type").Exists() { + if clie2e.DryRunGet(out, "api.0.params.user_id_type").Exists() { t.Fatalf("api.0.params.user_id_type should be omitted\nstdout:\n%s", out) } } @@ -75,26 +74,26 @@ func TestDriveListCommentsDryRun_WikiToken(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/wiki/v2/spaces/get_node" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/wiki/v2/spaces/get_node" { t.Fatalf("api.0.url=%q, want wiki get_node\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.token").String(); got != "wikiDryRunCommentList" { + if got := clie2e.DryRunGet(out, "api.0.params.token").String(); got != "wikiDryRunCommentList" { t.Fatalf("api.0.params.token=%q, want wikiDryRunCommentList\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.url").String(); got != "/open-apis/drive/v1/files//comments" { + if got := clie2e.DryRunGet(out, "api.1.url").String(); got != "/open-apis/drive/v1/files//comments" { t.Fatalf("api.1.url=%q, want resolved comments list placeholder\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.params.file_type").String(); got != "" { + if got := clie2e.DryRunGet(out, "api.1.params.file_type").String(); got != "" { t.Fatalf("api.1.params.file_type=%q, want obj_type placeholder\nstdout:\n%s", got, out) } - if gjson.Get(out, "api.1.params.is_solved").Exists() { + if clie2e.DryRunGet(out, "api.1.params.is_solved").Exists() { t.Fatalf("api.1.params.is_solved should be omitted for solved-status all\nstdout:\n%s", out) } - isWhole := gjson.Get(out, "api.1.params.is_whole") + isWhole := clie2e.DryRunGet(out, "api.1.params.is_whole") if !isWhole.Exists() || isWhole.Bool() { t.Fatalf("api.1.params.is_whole=%v, want explicit false for partial\nstdout:\n%s", isWhole.Value(), out) } - if got := gjson.Get(out, "api.1.params.need_relation").String(); got != "" { + if got := clie2e.DryRunGet(out, "api.1.params.need_relation").String(); got != "" { t.Fatalf("api.1.params.need_relation=%q, want conditional placeholder\nstdout:\n%s", got, out) } } diff --git a/tests/cli_e2e/drive/drive_member_add_dryrun_test.go b/tests/cli_e2e/drive/drive_member_add_dryrun_test.go index 3d31d18bc..6b5536900 100644 --- a/tests/cli_e2e/drive/drive_member_add_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_member_add_dryrun_test.go @@ -287,16 +287,16 @@ func TestDrive_MemberAddDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "POST" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "POST" { t.Fatalf("method = %q, want POST\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != tt.wantURL { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != tt.wantURL { t.Fatalf("url = %q, want %q\nstdout:\n%s", got, tt.wantURL, out) } - if got := gjson.Get(out, "api.0.params.type").String(); got != tt.wantResourceType { + if got := clie2e.DryRunGet(out, "api.0.params.type").String(); got != tt.wantResourceType { t.Fatalf("params.type = %q, want %q\nstdout:\n%s", got, tt.wantResourceType, out) } - notification := gjson.Get(out, "api.0.params.need_notification") + notification := clie2e.DryRunGet(out, "api.0.params.need_notification") if tt.wantNeedNotification == "" { if notification.Exists() { t.Fatalf("need_notification should be omitted\nstdout:\n%s", out) @@ -304,10 +304,10 @@ func TestDrive_MemberAddDryRun(t *testing.T) { } else if got := notification.String(); got != tt.wantNeedNotification { t.Fatalf("need_notification = %q, want %q\nstdout:\n%s", got, tt.wantNeedNotification, out) } - bodyPath := "api.0.body" + bodyPath := "data.api.0.body" if tt.wantBatch { - bodyPath = "api.0.body.members.0" - if count := len(gjson.Get(out, "api.0.body.members").Array()); count != 2 { + bodyPath = "data.api.0.body.members.0" + if count := len(clie2e.DryRunGet(out, "api.0.body.members").Array()); count != 2 { t.Fatalf("body.members count = %d, want 2\nstdout:\n%s", count, out) } } diff --git a/tests/cli_e2e/drive/drive_preview_dryrun_test.go b/tests/cli_e2e/drive/drive_preview_dryrun_test.go index 363fb71bd..a0e3519a7 100644 --- a/tests/cli_e2e/drive/drive_preview_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_preview_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestDrivePreviewDryRun_ListOnly verifies preview dry-run request structure @@ -34,13 +33,13 @@ func TestDrivePreviewDryRun_ListOnly(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "POST" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "POST" { t.Fatalf("method=%q, want POST\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/medias/fileDryRunPreview/preview_result" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/drive/v1/medias/fileDryRunPreview/preview_result" { t.Fatalf("url=%q, want preview_result endpoint\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "mode").String(); got != "list" { + if got := clie2e.DryRunGet(out, "mode").String(); got != "list" { t.Fatalf("mode=%q, want list\nstdout:\n%s", got, out) } } @@ -68,28 +67,28 @@ func TestDrivePreviewDryRun_Download(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.#").Int(); got != 2 { + if got := clie2e.DryRunGet(out, "api.#").Int(); got != 2 { t.Fatalf("api count=%d, want 2\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.version").String(); got != "12" { + if got := clie2e.DryRunGet(out, "api.0.body.version").String(); got != "12" { t.Fatalf("version=%q, want 12\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.1.method").String(); got != "GET" { t.Fatalf("download method=%q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.url").String(); got != "/open-apis/drive/v1/medias/fileDryRunPreview/preview_download" { + if got := clie2e.DryRunGet(out, "api.1.url").String(); got != "/open-apis/drive/v1/medias/fileDryRunPreview/preview_download" { t.Fatalf("download url=%q, want preview_download endpoint\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.params.preview_type").String(); got != "" { + if got := clie2e.DryRunGet(out, "api.1.params.preview_type").String(); got != "" { t.Fatalf("preview_type=%q, want placeholder\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.params.version").String(); got != "12" { + if got := clie2e.DryRunGet(out, "api.1.params.version").String(); got != "12" { t.Fatalf("download version=%q, want 12\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "requested_type").String(); got != "pdf" { + if got := clie2e.DryRunGet(out, "requested_type").String(); got != "pdf" { t.Fatalf("requested_type=%q, want pdf\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "output").String(); got != "./artifacts/report" { + if got := clie2e.DryRunGet(out, "output").String(); got != "./artifacts/report" { t.Fatalf("output=%q, want ./artifacts/report\nstdout:\n%s", got, out) } } @@ -116,31 +115,31 @@ func TestDriveCoverDryRun_Download(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method=%q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/medias/fileDryRunCover/preview_download" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/drive/v1/medias/fileDryRunCover/preview_download" { t.Fatalf("url=%q, want preview_download endpoint\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.preview_type").String(); got != "1" { + if got := clie2e.DryRunGet(out, "api.0.params.preview_type").String(); got != "1" { t.Fatalf("preview_type=%q, want 1\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.bus_type").Exists(); got { + if got := clie2e.DryRunGet(out, "api.0.params.bus_type").Exists(); got { t.Fatalf("bus_type should be omitted for square crop flow\nstdout:\n%s", out) } - if got := gjson.Get(out, "api.0.params.platform").Exists(); got { + if got := clie2e.DryRunGet(out, "api.0.params.platform").Exists(); got { t.Fatalf("platform should be omitted when using default platform\nstdout:\n%s", out) } - if got := gjson.Get(out, "api.0.params.width").String(); got != "360" { + if got := clie2e.DryRunGet(out, "api.0.params.width").String(); got != "360" { t.Fatalf("width=%q, want 360\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.height").String(); got != "360" { + if got := clie2e.DryRunGet(out, "api.0.params.height").String(); got != "360" { t.Fatalf("height=%q, want 360\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.policy").String(); got != "near" { + if got := clie2e.DryRunGet(out, "api.0.params.policy").String(); got != "near" { t.Fatalf("policy=%q, want near\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "selected_spec").String(); got != "square" { + if got := clie2e.DryRunGet(out, "selected_spec").String(); got != "square" { t.Fatalf("selected_spec=%q, want square\nstdout:\n%s", got, out) } } diff --git a/tests/cli_e2e/drive/drive_pull_dryrun_test.go b/tests/cli_e2e/drive/drive_pull_dryrun_test.go index b637e0b66..b318311d3 100644 --- a/tests/cli_e2e/drive/drive_pull_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_pull_dryrun_test.go @@ -13,7 +13,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestDrive_PullDryRun locks in the request shape the +pull shortcut emits @@ -52,16 +51,16 @@ func TestDrive_PullDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/files" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/drive/v1/files" { t.Fatalf("url = %q, want /open-apis/drive/v1/files\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } - desc := gjson.Get(out, "description").String() + desc := clie2e.DryRunGet(out, "description").String() if !strings.Contains(desc, "list --folder-token") { t.Fatalf("description missing list phrase, got %q\nstdout:\n%s", desc, out) } @@ -203,10 +202,10 @@ func TestDrive_PullDryRunAcceptsDuplicateRemoteStrategies(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } }) @@ -242,10 +241,10 @@ func TestDrive_PullDryRunAcceptsIfExistsSmart(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } } diff --git a/tests/cli_e2e/drive/drive_push_dryrun_test.go b/tests/cli_e2e/drive/drive_push_dryrun_test.go index 8e5186019..78c87794a 100644 --- a/tests/cli_e2e/drive/drive_push_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_push_dryrun_test.go @@ -13,7 +13,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestDrive_PushDryRun locks in the request shape the +push shortcut emits @@ -52,16 +51,16 @@ func TestDrive_PushDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/files" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/drive/v1/files" { t.Fatalf("url = %q, want /open-apis/drive/v1/files\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } - desc := gjson.Get(out, "description").String() + desc := clie2e.DryRunGet(out, "description").String() if !strings.Contains(desc, "list --folder-token") { t.Fatalf("description missing list phrase, got %q\nstdout:\n%s", desc, out) } @@ -187,10 +186,10 @@ func TestDrive_PushDryRunAcceptsDeleteRemoteWithYes(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } // No structured error envelope on stdout/stderr — the conditional @@ -269,10 +268,10 @@ func TestDrive_PushDryRunAcceptsDuplicateRemoteStrategies(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } }) @@ -308,10 +307,10 @@ func TestDrive_PushDryRunAcceptsIfExistsSmart(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } } diff --git a/tests/cli_e2e/drive/drive_search_dryrun_test.go b/tests/cli_e2e/drive/drive_search_dryrun_test.go index f610664e9..db5561d34 100644 --- a/tests/cli_e2e/drive/drive_search_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_search_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestDriveSearchDryRun_RequestShape locks in the dry-run request body so @@ -129,34 +128,34 @@ func TestDriveSearchDryRun_RequestShape(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "POST" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "POST" { t.Fatalf("method=%q, want POST\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != tt.wantURL { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != tt.wantURL { t.Fatalf("url=%q, want %q\nstdout:\n%s", got, tt.wantURL, out) } - if got := gjson.Get(out, "api.0.body.query").String(); got != tt.wantQuery { + if got := clie2e.DryRunGet(out, "api.0.body.query").String(); got != tt.wantQuery { t.Fatalf("body.query=%q, want %q\nstdout:\n%s", got, tt.wantQuery, out) } - if tt.wantDocFilter && !gjson.Get(out, "api.0.body.doc_filter").Exists() { + if tt.wantDocFilter && !clie2e.DryRunGet(out, "api.0.body.doc_filter").Exists() { t.Fatalf("doc_filter missing\nstdout:\n%s", out) } - if !tt.wantDocFilter && gjson.Get(out, "api.0.body.doc_filter").Exists() { + if !tt.wantDocFilter && clie2e.DryRunGet(out, "api.0.body.doc_filter").Exists() { t.Fatalf("doc_filter should be omitted\nstdout:\n%s", out) } - if tt.wantWikiFilter && !gjson.Get(out, "api.0.body.wiki_filter").Exists() { + if tt.wantWikiFilter && !clie2e.DryRunGet(out, "api.0.body.wiki_filter").Exists() { t.Fatalf("wiki_filter missing\nstdout:\n%s", out) } - if !tt.wantWikiFilter && gjson.Get(out, "api.0.body.wiki_filter").Exists() { + if !tt.wantWikiFilter && clie2e.DryRunGet(out, "api.0.body.wiki_filter").Exists() { t.Fatalf("wiki_filter should be omitted\nstdout:\n%s", out) } for path, want := range tt.wantDocFilterFields { - if got := gjson.Get(out, "api.0.body.doc_filter."+path).String(); got != want { + if got := clie2e.DryRunGet(out, "api.0.body.doc_filter."+path).String(); got != want { t.Fatalf("doc_filter.%s=%q, want %q\nstdout:\n%s", path, got, want, out) } } for path, want := range tt.wantWikiFilterFields { - if got := gjson.Get(out, "api.0.body.wiki_filter."+path).String(); got != want { + if got := clie2e.DryRunGet(out, "api.0.body.wiki_filter."+path).String(); got != want { t.Fatalf("wiki_filter.%s=%q, want %q\nstdout:\n%s", path, got, want, out) } } @@ -185,13 +184,13 @@ func TestDriveSearchDryRun_BotIdentity(t *testing.T) { require.Contains(t, result.Args, "bot") out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "POST" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "POST" { t.Fatalf("method=%q, want POST\nstdout:\n%s\nstderr:\n%s", got, out, result.Stderr) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/search/v2/doc_wiki/search" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/search/v2/doc_wiki/search" { t.Fatalf("url=%q, want Search v2 doc_wiki/search\nstdout:\n%s\nstderr:\n%s", got, out, result.Stderr) } - if got := gjson.Get(out, "api.0.body.query").String(); got != "season report" { + if got := clie2e.DryRunGet(out, "api.0.body.query").String(); got != "season report" { t.Fatalf("body.query=%q, want season report\nstdout:\n%s", got, out) } @@ -259,8 +258,8 @@ func TestDriveSearchDryRun_OpenedClamping(t *testing.T) { // And the request body's open_time must reflect the clamped window // (start and end both present, span = 90 days exactly). body := result.Stdout - start := gjson.Get(body, "api.0.body.doc_filter.open_time.start").Int() - end := gjson.Get(body, "api.0.body.doc_filter.open_time.end").Int() + start := clie2e.DryRunGet(body, "api.0.body.doc_filter.open_time.start").Int() + end := clie2e.DryRunGet(body, "api.0.body.doc_filter.open_time.end").Int() if start == 0 || end == 0 { t.Fatalf("doc_filter.open_time.start/end missing\nstdout:\n%s", body) } @@ -296,10 +295,10 @@ func TestDriveSearchDryRun_RejectsOpenedOver1Year(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - if api := gjson.Get(result.Stdout, "api"); api.IsArray() && len(api.Array()) > 0 { + if api := clie2e.DryRunGet(result.Stdout, "api"); api.IsArray() && len(api.Array()) > 0 { t.Fatalf("dry-run api list must be empty when validation fails\nstdout:\n%s", result.Stdout) } - errMsg := gjson.Get(result.Stdout, "error").String() + errMsg := clie2e.DryRunGet(result.Stdout, "error").String() if !strings.Contains(errMsg, "365-day") { t.Fatalf("expected 365-day cap message in dry-run error, got %q\nstdout:\n%s", errMsg, result.Stdout) } @@ -359,10 +358,10 @@ func TestDriveSearchDryRun_RejectsBadDocType(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - if api := gjson.Get(result.Stdout, "api"); api.IsArray() && len(api.Array()) > 0 { + if api := clie2e.DryRunGet(result.Stdout, "api"); api.IsArray() && len(api.Array()) > 0 { t.Fatalf("dry-run api list must be empty when validation fails\nstdout:\n%s", result.Stdout) } - errMsg := gjson.Get(result.Stdout, "error").String() + errMsg := clie2e.DryRunGet(result.Stdout, "error").String() if !strings.Contains(errMsg, "--doc-types") { t.Fatalf("expected --doc-types error in dry-run, got %q\nstdout:\n%s", errMsg, result.Stdout) } diff --git a/tests/cli_e2e/drive/drive_secure_label_dryrun_test.go b/tests/cli_e2e/drive/drive_secure_label_dryrun_test.go index 2ebcba660..f964bfabf 100644 --- a/tests/cli_e2e/drive/drive_secure_label_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_secure_label_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestDrive_SecureLabelDryRun(t *testing.T) { @@ -38,13 +37,13 @@ func TestDrive_SecureLabelDryRun(t *testing.T) { wantMethod: "GET", wantURL: "/open-apis/drive/v2/my_secure_labels", assert: func(t *testing.T, out string) { - if got := gjson.Get(out, "api.0.params.page_size").Int(); got != 5 { + if got := clie2e.DryRunGet(out, "api.0.params.page_size").Int(); got != 5 { t.Fatalf("page_size = %d, want 5\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.page_token").String(); got != "page_1" { + if got := clie2e.DryRunGet(out, "api.0.params.page_token").String(); got != "page_1" { t.Fatalf("page_token = %q, want page_1\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.lang").String(); got != "zh" { + if got := clie2e.DryRunGet(out, "api.0.params.lang").String(); got != "zh" { t.Fatalf("lang = %q, want zh\nstdout:\n%s", got, out) } }, @@ -60,13 +59,13 @@ func TestDrive_SecureLabelDryRun(t *testing.T) { wantMethod: "PATCH", wantURL: "/open-apis/drive/v2/files/doxcnE2E001/secure_label", assert: func(t *testing.T, out string) { - if got := gjson.Get(out, "api.0.params.type").String(); got != "docx" { + if got := clie2e.DryRunGet(out, "api.0.params.type").String(); got != "docx" { t.Fatalf("type = %q, want docx\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.id").String(); got != "7217780879644737539" { + if got := clie2e.DryRunGet(out, "api.0.body.id").String(); got != "7217780879644737539" { t.Fatalf("body.id = %q, want label id\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "file_token").String(); got != "doxcnE2E001" { + if got := clie2e.DryRunGet(out, "file_token").String(); got != "doxcnE2E001" { t.Fatalf("file_token = %q, want doxcnE2E001\nstdout:\n%s", got, out) } }, @@ -86,10 +85,10 @@ func TestDrive_SecureLabelDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != tt.wantMethod { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != tt.wantMethod { t.Fatalf("method = %q, want %s\nstdout:\n%s", got, tt.wantMethod, out) } - if got := gjson.Get(out, "api.0.url").String(); got != tt.wantURL { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != tt.wantURL { t.Fatalf("url = %q, want %q\nstdout:\n%s", got, tt.wantURL, out) } tt.assert(t, out) diff --git a/tests/cli_e2e/drive/drive_status_dryrun_test.go b/tests/cli_e2e/drive/drive_status_dryrun_test.go index 434dcfa71..bbc4d275d 100644 --- a/tests/cli_e2e/drive/drive_status_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_status_dryrun_test.go @@ -13,7 +13,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestDrive_StatusDryRun locks in the request shape the +status shortcut @@ -55,16 +54,16 @@ func TestDrive_StatusDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/files" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/drive/v1/files" { t.Fatalf("url = %q, want /open-apis/drive/v1/files\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } - desc := gjson.Get(out, "description").String() + desc := clie2e.DryRunGet(out, "description").String() if !strings.Contains(desc, "Walk --local-dir") || !strings.Contains(desc, "SHA-256") { t.Fatalf("description missing key phrases, got %q\nstdout:\n%s", desc, out) } @@ -99,16 +98,16 @@ func TestDrive_StatusDryRunQuick(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/files" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/drive/v1/files" { t.Fatalf("url = %q, want /open-apis/drive/v1/files\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } - desc := gjson.Get(out, "description").String() + desc := clie2e.DryRunGet(out, "description").String() if !strings.Contains(desc, "modified_time") || strings.Contains(desc, "SHA-256") { t.Fatalf("quick description must mention modified_time and skip SHA-256 wording, got %q\nstdout:\n%s", desc, out) } diff --git a/tests/cli_e2e/drive/drive_sync_dryrun_test.go b/tests/cli_e2e/drive/drive_sync_dryrun_test.go index 00e70427b..7d521eab1 100644 --- a/tests/cli_e2e/drive/drive_sync_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_sync_dryrun_test.go @@ -13,7 +13,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestDrive_SyncDryRun locks in the request shape the +sync shortcut emits @@ -52,16 +51,16 @@ func TestDrive_SyncDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/files" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/drive/v1/files" { t.Fatalf("url = %q, want /open-apis/drive/v1/files\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } - desc := gjson.Get(out, "description").String() + desc := clie2e.DryRunGet(out, "description").String() if !strings.Contains(desc, "diff") { t.Fatalf("description missing diff phrase, got %q\nstdout:\n%s", desc, out) } @@ -168,10 +167,10 @@ func TestDrive_SyncDryRunAcceptsConflictStrategies(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } }) @@ -211,7 +210,7 @@ func TestDrive_SyncDryRunAcceptsDuplicateRemoteStrategies(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } }) @@ -249,10 +248,10 @@ func TestDrive_SyncDryRunAcceptsQuickFlag(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method = %q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "folder_token").String(); got != "fldcnE2E001" { + if got := clie2e.DryRunGet(out, "folder_token").String(); got != "fldcnE2E001" { t.Fatalf("folder_token = %q, want fldcnE2E001\nstdout:\n%s", got, out) } } diff --git a/tests/cli_e2e/drive/drive_upload_dryrun_test.go b/tests/cli_e2e/drive/drive_upload_dryrun_test.go index b33d88f46..df1bf5b3e 100644 --- a/tests/cli_e2e/drive/drive_upload_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_upload_dryrun_test.go @@ -66,7 +66,7 @@ func TestDriveUploadDryRun_WithFileToken(t *testing.T) { assert.Contains(t, output, "/open-apis/drive/v1/metas/batch_query") assert.Contains(t, output, `"with_url": true`) assert.Contains(t, output, `"parent_node": "fldDryRunUploadTarget"`) - assert.Contains(t, output, `"file_token": "boxcnDryRunOverwriteTarget"`) + assert.Equal(t, "boxcnDryRunOverwriteTarget", clie2e.DryRunGet(output, "api.0.body.file_token").String()) } func TestDriveUploadDryRunRejectsEmptyWikiToken(t *testing.T) { diff --git a/tests/cli_e2e/event/event_subscribe_dryrun_test.go b/tests/cli_e2e/event/event_subscribe_dryrun_test.go index d1bd94a86..2907a048d 100644 --- a/tests/cli_e2e/event/event_subscribe_dryrun_test.go +++ b/tests/cli_e2e/event/event_subscribe_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestEventSubscribeDryRun(t *testing.T) { @@ -37,10 +36,10 @@ func TestEventSubscribeDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "event +subscribe", gjson.Get(out, "command").String(), "stdout:\n%s", out) - require.Equal(t, "app", gjson.Get(out, "app_id").String(), "stdout:\n%s", out) - require.Equal(t, "im.message.receive_v1,contact.user.created_v3", gjson.Get(out, "event_types").String(), "stdout:\n%s", out) - require.Equal(t, "^im\\.", gjson.Get(out, "filter").String(), "stdout:\n%s", out) - require.Equal(t, "events_out", gjson.Get(out, "output_dir").String(), "stdout:\n%s", out) - require.Equal(t, "^im\\.message=dir:./messages", gjson.Get(out, "route").String(), "stdout:\n%s", out) + require.Equal(t, "event +subscribe", clie2e.DryRunGet(out, "command").String(), "stdout:\n%s", out) + require.Equal(t, "app", clie2e.DryRunGet(out, "app_id").String(), "stdout:\n%s", out) + require.Equal(t, "im.message.receive_v1,contact.user.created_v3", clie2e.DryRunGet(out, "event_types").String(), "stdout:\n%s", out) + require.Equal(t, "^im\\.", clie2e.DryRunGet(out, "filter").String(), "stdout:\n%s", out) + require.Equal(t, "events_out", clie2e.DryRunGet(out, "output_dir").String(), "stdout:\n%s", out) + require.Equal(t, "^im\\.message=dir:./messages", clie2e.DryRunGet(out, "route").String(), "stdout:\n%s", out) } diff --git a/tests/cli_e2e/im/im_download_resources_dryrun_test.go b/tests/cli_e2e/im/im_download_resources_dryrun_test.go index 01f631d8d..34956e465 100644 --- a/tests/cli_e2e/im/im_download_resources_dryrun_test.go +++ b/tests/cli_e2e/im/im_download_resources_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestIM_DownloadResourcesDryRun verifies the --download-resources flag is wired @@ -43,17 +42,17 @@ func TestIM_DownloadResourcesDryRun(t *testing.T) { t.Run("default off: no resources declaration, request unchanged", func(t *testing.T) { out := run(t) - require.Equal(t, "GET", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - require.Equal(t, "/open-apis/im/v1/messages", gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) - require.Equal(t, "oc_dryrun", gjson.Get(out, "api.0.params.container_id").String(), "stdout:\n%s", out) + require.Equal(t, "GET", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + require.Equal(t, "/open-apis/im/v1/messages", clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) + require.Equal(t, "oc_dryrun", clie2e.DryRunGet(out, "api.0.params.container_id").String(), "stdout:\n%s", out) require.NotContains(t, strings.ToLower(out), "lark-im-resources", "default must not declare resource download:\n%s", out) }) t.Run("with --download-resources: request unchanged, declares download", func(t *testing.T) { out := run(t, "--download-resources") - require.Equal(t, "GET", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - require.Equal(t, "/open-apis/im/v1/messages", gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) - require.Equal(t, "oc_dryrun", gjson.Get(out, "api.0.params.container_id").String(), "stdout:\n%s", out) + require.Equal(t, "GET", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + require.Equal(t, "/open-apis/im/v1/messages", clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) + require.Equal(t, "oc_dryrun", clie2e.DryRunGet(out, "api.0.params.container_id").String(), "stdout:\n%s", out) require.Contains(t, strings.ToLower(out), "lark-im-resources", "flag must declare resource download:\n%s", out) }) } diff --git a/tests/cli_e2e/mail/mail_draft_send_dryrun_test.go b/tests/cli_e2e/mail/mail_draft_send_dryrun_test.go index 4816f0a25..ceaec8991 100644 --- a/tests/cli_e2e/mail/mail_draft_send_dryrun_test.go +++ b/tests/cli_e2e/mail/mail_draft_send_dryrun_test.go @@ -12,7 +12,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestMail_DraftSendDryRun(t *testing.T) { @@ -39,12 +38,12 @@ func TestMail_DraftSendDryRun(t *testing.T) { "/open-apis/mail/v1/user_mailboxes/alias@example.com/drafts/draft_002/send", "/open-apis/mail/v1/user_mailboxes/alias@example.com/drafts/draft_003/send", } - assert.Equal(t, int64(len(wantURLs)), gjson.Get(result.Stdout, "api.#").Int(), "stdout:\n%s", result.Stdout) + assert.Equal(t, int64(len(wantURLs)), clie2e.DryRunGet(result.Stdout, "api.#").Int(), "stdout:\n%s", result.Stdout) for i, wantURL := range wantURLs { idx := strconv.Itoa(i) - assert.Equal(t, "POST", gjson.Get(result.Stdout, "api."+idx+".method").String(), "stdout:\n%s", result.Stdout) - assert.Equal(t, wantURL, gjson.Get(result.Stdout, "api."+idx+".url").String(), "stdout:\n%s", result.Stdout) - assert.False(t, gjson.Get(result.Stdout, "api."+idx+".body").Exists(), "stdout:\n%s", result.Stdout) + assert.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api."+idx+".method").String(), "stdout:\n%s", result.Stdout) + assert.Equal(t, wantURL, clie2e.DryRunGet(result.Stdout, "api."+idx+".url").String(), "stdout:\n%s", result.Stdout) + assert.False(t, clie2e.DryRunGet(result.Stdout, "api."+idx+".body").Exists(), "stdout:\n%s", result.Stdout) } } diff --git a/tests/cli_e2e/mail/mail_share_to_chat_dryrun_test.go b/tests/cli_e2e/mail/mail_share_to_chat_dryrun_test.go index ff152ab04..2ee620175 100644 --- a/tests/cli_e2e/mail/mail_share_to_chat_dryrun_test.go +++ b/tests/cli_e2e/mail/mail_share_to_chat_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestMail_ShareToChatDryRun validates the request shape emitted by @@ -99,14 +98,14 @@ func TestMail_ShareToChatDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - gotCount := int(gjson.Get(out, "api.#").Int()) + gotCount := int(clie2e.DryRunGet(out, "api.#").Int()) if gotCount != len(tt.wantURLs) { t.Fatalf("expected %d API calls, got %d\nstdout:\n%s", len(tt.wantURLs), gotCount, out) } for i, wantURL := range tt.wantURLs { idx := strconv.Itoa(i) - gotMethod := gjson.Get(out, "api."+idx+".method").String() - gotURL := gjson.Get(out, "api."+idx+".url").String() + gotMethod := clie2e.DryRunGet(out, "api."+idx+".method").String() + gotURL := clie2e.DryRunGet(out, "api."+idx+".url").String() if gotMethod != "POST" { t.Fatalf("api[%d].method = %q, want POST\nstdout:\n%s", i, gotMethod, out) } @@ -116,19 +115,19 @@ func TestMail_ShareToChatDryRun(t *testing.T) { } for k, v := range tt.wantCreateBody { - got := gjson.Get(out, "api.0.body."+k).String() + got := clie2e.DryRunGet(out, "api.0.body."+k).String() if got != v { t.Fatalf("api[0].body.%s = %q, want %q\nstdout:\n%s", k, got, v, out) } } for k, v := range tt.wantSendBody { - got := gjson.Get(out, "api.1.body."+k).String() + got := clie2e.DryRunGet(out, "api.1.body."+k).String() if got != v { t.Fatalf("api[1].body.%s = %q, want %q\nstdout:\n%s", k, got, v, out) } } for k, v := range tt.wantSendParams { - got := gjson.Get(out, "api.1.params."+k).String() + got := clie2e.DryRunGet(out, "api.1.params."+k).String() if got != v { t.Fatalf("api[1].params.%s = %q, want %q\nstdout:\n%s", k, got, v, out) } diff --git a/tests/cli_e2e/mail/mail_triage_dryrun_test.go b/tests/cli_e2e/mail/mail_triage_dryrun_test.go index 68b0b3c19..dea60344f 100644 --- a/tests/cli_e2e/mail/mail_triage_dryrun_test.go +++ b/tests/cli_e2e/mail/mail_triage_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestMail_TriageDryRunPreservesMailboxInRequestChain(t *testing.T) { @@ -32,15 +31,15 @@ func TestMail_TriageDryRunPreservesMailboxInRequestChain(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - require.Equal(t, int64(2), gjson.Get(result.Stdout, "api.#").Int(), "stdout:\n%s", result.Stdout) - require.Equal(t, "GET", gjson.Get(result.Stdout, "api.0.method").String(), "stdout:\n%s", result.Stdout) - require.Equal(t, "/open-apis/mail/v1/user_mailboxes/alias@example.com/messages", gjson.Get(result.Stdout, "api.0.url").String(), "stdout:\n%s", result.Stdout) - require.Equal(t, int64(3), gjson.Get(result.Stdout, "api.0.params.page_size").Int(), "stdout:\n%s", result.Stdout) - require.Equal(t, "INBOX", gjson.Get(result.Stdout, "api.0.params.folder_id").String(), "stdout:\n%s", result.Stdout) + require.Equal(t, int64(2), clie2e.DryRunGet(result.Stdout, "api.#").Int(), "stdout:\n%s", result.Stdout) + require.Equal(t, "GET", clie2e.DryRunGet(result.Stdout, "api.0.method").String(), "stdout:\n%s", result.Stdout) + require.Equal(t, "/open-apis/mail/v1/user_mailboxes/alias@example.com/messages", clie2e.DryRunGet(result.Stdout, "api.0.url").String(), "stdout:\n%s", result.Stdout) + require.Equal(t, int64(3), clie2e.DryRunGet(result.Stdout, "api.0.params.page_size").Int(), "stdout:\n%s", result.Stdout) + require.Equal(t, "INBOX", clie2e.DryRunGet(result.Stdout, "api.0.params.folder_id").String(), "stdout:\n%s", result.Stdout) - require.Equal(t, "POST", gjson.Get(result.Stdout, "api.1.method").String(), "stdout:\n%s", result.Stdout) - require.Equal(t, "/open-apis/mail/v1/user_mailboxes/alias@example.com/messages/batch_get", gjson.Get(result.Stdout, "api.1.url").String(), "stdout:\n%s", result.Stdout) - require.Equal(t, "metadata", gjson.Get(result.Stdout, "api.1.body.format").String(), "stdout:\n%s", result.Stdout) + require.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api.1.method").String(), "stdout:\n%s", result.Stdout) + require.Equal(t, "/open-apis/mail/v1/user_mailboxes/alias@example.com/messages/batch_get", clie2e.DryRunGet(result.Stdout, "api.1.url").String(), "stdout:\n%s", result.Stdout) + require.Equal(t, "metadata", clie2e.DryRunGet(result.Stdout, "api.1.body.format").String(), "stdout:\n%s", result.Stdout) } func setMailTriageDryRunEnv(t *testing.T) { diff --git a/tests/cli_e2e/markdown/markdown_dryrun_test.go b/tests/cli_e2e/markdown/markdown_dryrun_test.go index fa8afa336..0dff21cf0 100644 --- a/tests/cli_e2e/markdown/markdown_dryrun_test.go +++ b/tests/cli_e2e/markdown/markdown_dryrun_test.go @@ -13,7 +13,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestMarkdownCreateDryRun_Content(t *testing.T) { @@ -122,10 +121,10 @@ func TestMarkdownCreateDryRun_RejectsEmptyContent(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - if api := gjson.Get(result.Stdout, "api"); api.IsArray() && len(api.Array()) > 0 { + if api := clie2e.DryRunGet(result.Stdout, "api"); api.IsArray() && len(api.Array()) > 0 { t.Fatalf("dry-run api list must be empty when validation fails\nstdout:\n%s", result.Stdout) } - errMsg := gjson.Get(result.Stdout, "error").String() + errMsg := clie2e.DryRunGet(result.Stdout, "error").String() assert.Contains(t, errMsg, "empty markdown content is not supported") } @@ -279,10 +278,10 @@ func TestMarkdownOverwriteDryRun_RejectsEmptyFile(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - if api := gjson.Get(result.Stdout, "api"); api.IsArray() && len(api.Array()) > 0 { + if api := clie2e.DryRunGet(result.Stdout, "api"); api.IsArray() && len(api.Array()) > 0 { t.Fatalf("dry-run api list must be empty when validation fails\nstdout:\n%s", result.Stdout) } - errMsg := gjson.Get(result.Stdout, "error").String() + errMsg := clie2e.DryRunGet(result.Stdout, "error").String() assert.Contains(t, errMsg, "empty markdown content is not supported") } diff --git a/tests/cli_e2e/note/note_dryrun_test.go b/tests/cli_e2e/note/note_dryrun_test.go index 8d705556b..691983ce1 100644 --- a/tests/cli_e2e/note/note_dryrun_test.go +++ b/tests/cli_e2e/note/note_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestNoteDetailDryRun(t *testing.T) { @@ -31,10 +30,10 @@ func TestNoteDetailDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("method=%q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/vc/v1/notes/note_dryrun" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/vc/v1/notes/note_dryrun" { t.Fatalf("url=%q, want note detail endpoint\nstdout:\n%s", got, out) } } @@ -59,31 +58,31 @@ func TestNoteTranscriptDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if got := gjson.Get(out, "api.#").Int(); got != 2 { + if got := clie2e.DryRunGet(out, "api.#").Int(); got != 2 { t.Fatalf("api count=%d, want 2\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.0.method").String(); got != "GET" { t.Fatalf("detail method=%q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/vc/v1/notes/note_dryrun" { + if got := clie2e.DryRunGet(out, "api.0.url").String(); got != "/open-apis/vc/v1/notes/note_dryrun" { t.Fatalf("detail url=%q, want note detail endpoint\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.method").String(); got != "GET" { + if got := clie2e.DryRunGet(out, "api.1.method").String(); got != "GET" { t.Fatalf("transcript method=%q, want GET\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.url").String(); got != "/open-apis/vc/v1/notes/note_dryrun/unified_note_transcript" { + if got := clie2e.DryRunGet(out, "api.1.url").String(); got != "/open-apis/vc/v1/notes/note_dryrun/unified_note_transcript" { t.Fatalf("transcript url=%q, want unified transcript endpoint\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.params.format").String(); got != "plain_text" { + if got := clie2e.DryRunGet(out, "api.1.params.format").String(); got != "plain_text" { t.Fatalf("transcript API format=%q, want plain_text\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.params.page_size").Int(); got != 200 { + if got := clie2e.DryRunGet(out, "api.1.params.page_size").Int(); got != 200 { t.Fatalf("page_size=%d, want 200\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.1.params.locale").String(); got != "zh_cn" { + if got := clie2e.DryRunGet(out, "api.1.params.locale").String(); got != "zh_cn" { t.Fatalf("locale=%q, want zh_cn\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "transcript_format").String(); got != "plain_text" { + if got := clie2e.DryRunGet(out, "transcript_format").String(); got != "plain_text" { t.Fatalf("transcript_format=%q, want plain_text\nstdout:\n%s", got, out) } } diff --git a/tests/cli_e2e/sheets/sheets_gridline_dryrun_test.go b/tests/cli_e2e/sheets/sheets_gridline_dryrun_test.go index 4541c15c4..0d7564166 100644 --- a/tests/cli_e2e/sheets/sheets_gridline_dryrun_test.go +++ b/tests/cli_e2e/sheets/sheets_gridline_dryrun_test.go @@ -44,6 +44,9 @@ func TestSheets_GridlineDryRun(t *testing.T) { }, DefaultAs: "user", }) + if result != nil { + result.Stdout = clie2e.DryRunData(result.Stdout) + } require.NoError(t, err) result.AssertExitCode(t, 0) diff --git a/tests/cli_e2e/sheets/sheets_image_upload_dryrun_test.go b/tests/cli_e2e/sheets/sheets_image_upload_dryrun_test.go index 6bd3deb9e..98ec5ec89 100644 --- a/tests/cli_e2e/sheets/sheets_image_upload_dryrun_test.go +++ b/tests/cli_e2e/sheets/sheets_image_upload_dryrun_test.go @@ -12,7 +12,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestSheets_ImageUploadDryRunParentType pins the parent_type the sheets @@ -100,12 +99,12 @@ func TestSheets_ImageUploadDryRunParentType(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), "api.0 must be the drive upload; stdout:\n%s", out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), "data.api.0 must be the drive upload; stdout:\n%s", out) require.Equal(t, "/open-apis/drive/v1/medias/upload_all", - gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) - require.Equal(t, tt.wantParentType, gjson.Get(out, "api.0.body.parent_type").String(), + clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) + require.Equal(t, tt.wantParentType, clie2e.DryRunGet(out, "api.0.body.parent_type").String(), "parent_type for token %q must be %q; stdout:\n%s", tt.token, tt.wantParentType, out) - require.Equal(t, tt.token, gjson.Get(out, "api.0.body.parent_node").String(), + require.Equal(t, tt.token, clie2e.DryRunGet(out, "api.0.body.parent_node").String(), "parent_node must equal the spreadsheet token; stdout:\n%s", out) }) } diff --git a/tests/cli_e2e/sheets/sheets_sheet_shortcuts_dryrun_test.go b/tests/cli_e2e/sheets/sheets_sheet_shortcuts_dryrun_test.go index e51833e25..2314127e1 100644 --- a/tests/cli_e2e/sheets/sheets_sheet_shortcuts_dryrun_test.go +++ b/tests/cli_e2e/sheets/sheets_sheet_shortcuts_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func setSheetsDryRunEnv(t *testing.T) { @@ -169,9 +168,9 @@ func TestSheets_SheetShortcutsDryRun(t *testing.T) { }, wantURL: "/open-apis/sheets/v2/spreadsheets/shtDryRun/sheets_batch_update", wantFn: func(t *testing.T, out string) { - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - require.Equal(t, "Data", gjson.Get(out, "api.0.body.requests.0.addSheet.properties.title").String(), "stdout:\n%s", out) - require.Equal(t, int64(0), gjson.Get(out, "api.0.body.requests.0.addSheet.properties.index").Int(), "stdout:\n%s", out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + require.Equal(t, "Data", clie2e.DryRunGet(out, "api.0.body.requests.0.addSheet.properties.title").String(), "stdout:\n%s", out) + require.Equal(t, int64(0), clie2e.DryRunGet(out, "api.0.body.requests.0.addSheet.properties.index").Int(), "stdout:\n%s", out) }, }, { @@ -186,12 +185,12 @@ func TestSheets_SheetShortcutsDryRun(t *testing.T) { }, wantURL: "/open-apis/sheets/v2/spreadsheets/shtDryRun/sheets_batch_update", wantFn: func(t *testing.T, out string) { - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - require.Equal(t, "sheet1", gjson.Get(out, "api.0.body.requests.0.copySheet.source.sheetId").String(), "stdout:\n%s", out) - require.Equal(t, "Copy", gjson.Get(out, "api.0.body.requests.0.copySheet.destination.title").String(), "stdout:\n%s", out) - require.Equal(t, "POST", gjson.Get(out, "api.1.method").String(), "stdout:\n%s", out) - require.Equal(t, "", gjson.Get(out, "api.1.body.requests.0.updateSheet.properties.sheetId").String(), "stdout:\n%s", out) - require.Equal(t, int64(2), gjson.Get(out, "api.1.body.requests.0.updateSheet.properties.index").Int(), "stdout:\n%s", out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + require.Equal(t, "sheet1", clie2e.DryRunGet(out, "api.0.body.requests.0.copySheet.source.sheetId").String(), "stdout:\n%s", out) + require.Equal(t, "Copy", clie2e.DryRunGet(out, "api.0.body.requests.0.copySheet.destination.title").String(), "stdout:\n%s", out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.1.method").String(), "stdout:\n%s", out) + require.Equal(t, "", clie2e.DryRunGet(out, "api.1.body.requests.0.updateSheet.properties.sheetId").String(), "stdout:\n%s", out) + require.Equal(t, int64(2), clie2e.DryRunGet(out, "api.1.body.requests.0.updateSheet.properties.index").Int(), "stdout:\n%s", out) }, }, { @@ -204,8 +203,8 @@ func TestSheets_SheetShortcutsDryRun(t *testing.T) { }, wantURL: "/open-apis/sheets/v2/spreadsheets/shtDryRun/sheets_batch_update", wantFn: func(t *testing.T, out string) { - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - require.Equal(t, "sheet1", gjson.Get(out, "api.0.body.requests.0.deleteSheet.sheetId").String(), "stdout:\n%s", out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + require.Equal(t, "sheet1", clie2e.DryRunGet(out, "api.0.body.requests.0.deleteSheet.sheetId").String(), "stdout:\n%s", out) }, }, { @@ -226,14 +225,14 @@ func TestSheets_SheetShortcutsDryRun(t *testing.T) { }, wantURL: "/open-apis/sheets/v2/spreadsheets/shtDryRun/sheets_batch_update", wantFn: func(t *testing.T, out string) { - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - require.Equal(t, "open_id", gjson.Get(out, "api.0.params.user_id_type").String(), "stdout:\n%s", out) - require.Equal(t, "sheet1", gjson.Get(out, "api.0.body.requests.0.updateSheet.properties.sheetId").String(), "stdout:\n%s", out) - require.Equal(t, "Renamed", gjson.Get(out, "api.0.body.requests.0.updateSheet.properties.title").String(), "stdout:\n%s", out) - require.Equal(t, false, gjson.Get(out, "api.0.body.requests.0.updateSheet.properties.hidden").Bool(), "stdout:\n%s", out) - require.Equal(t, int64(2), gjson.Get(out, "api.0.body.requests.0.updateSheet.properties.frozenRowCount").Int(), "stdout:\n%s", out) - require.Equal(t, int64(1), gjson.Get(out, "api.0.body.requests.0.updateSheet.properties.frozenColCount").Int(), "stdout:\n%s", out) - require.Equal(t, "LOCK", gjson.Get(out, "api.0.body.requests.0.updateSheet.properties.protect.lock").String(), "stdout:\n%s", out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + require.Equal(t, "open_id", clie2e.DryRunGet(out, "api.0.params.user_id_type").String(), "stdout:\n%s", out) + require.Equal(t, "sheet1", clie2e.DryRunGet(out, "api.0.body.requests.0.updateSheet.properties.sheetId").String(), "stdout:\n%s", out) + require.Equal(t, "Renamed", clie2e.DryRunGet(out, "api.0.body.requests.0.updateSheet.properties.title").String(), "stdout:\n%s", out) + require.Equal(t, false, clie2e.DryRunGet(out, "api.0.body.requests.0.updateSheet.properties.hidden").Bool(), "stdout:\n%s", out) + require.Equal(t, int64(2), clie2e.DryRunGet(out, "api.0.body.requests.0.updateSheet.properties.frozenRowCount").Int(), "stdout:\n%s", out) + require.Equal(t, int64(1), clie2e.DryRunGet(out, "api.0.body.requests.0.updateSheet.properties.frozenColCount").Int(), "stdout:\n%s", out) + require.Equal(t, "LOCK", clie2e.DryRunGet(out, "api.0.body.requests.0.updateSheet.properties.protect.lock").String(), "stdout:\n%s", out) }, }, } @@ -251,7 +250,7 @@ func TestSheets_SheetShortcutsDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, tt.wantURL, gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) + require.Equal(t, tt.wantURL, clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) tt.wantFn(t, out) }) } diff --git a/tests/cli_e2e/sheets/sheets_table_get_dryrun_test.go b/tests/cli_e2e/sheets/sheets_table_get_dryrun_test.go index fdb032ca5..de764c7d5 100644 --- a/tests/cli_e2e/sheets/sheets_table_get_dryrun_test.go +++ b/tests/cli_e2e/sheets/sheets_table_get_dryrun_test.go @@ -36,7 +36,7 @@ func TestSheets_TableGetDefaultDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - out := result.Stdout + out := clie2e.DryRunData(result.Stdout) // api.0 — the structure read that supplies the grid dimensions. require.Equal(t, "get_workbook_structure", gjson.Get(out, "api.0.body.tool_name").String(), "stdout:\n%s", out) @@ -69,7 +69,7 @@ func TestSheets_TableGetSingleSheetDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - out := result.Stdout + out := clie2e.DryRunData(result.Stdout) require.Equal(t, "get_workbook_structure", gjson.Get(out, "api.0.body.tool_name").String(), "single-sheet path must still read the structure for grid dimensions; stdout:\n%s", out) require.Equal(t, "get_cell_ranges", gjson.Get(out, "api.1.body.tool_name").String(), "stdout:\n%s", out) diff --git a/tests/cli_e2e/sheets/sheets_table_put_dryrun_test.go b/tests/cli_e2e/sheets/sheets_table_put_dryrun_test.go index 23545399a..350d1274f 100644 --- a/tests/cli_e2e/sheets/sheets_table_put_dryrun_test.go +++ b/tests/cli_e2e/sheets/sheets_table_put_dryrun_test.go @@ -37,7 +37,7 @@ func TestSheets_TablePutStylesDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - out := result.Stdout + out := clie2e.DryRunData(result.Stdout) // api.0 — the typed write, with cell_styles merged into the cells matrix. require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) diff --git a/tests/cli_e2e/sheets/sheets_workbook_export_dryrun_test.go b/tests/cli_e2e/sheets/sheets_workbook_export_dryrun_test.go index 6eb0927e8..ee34aa3a7 100644 --- a/tests/cli_e2e/sheets/sheets_workbook_export_dryrun_test.go +++ b/tests/cli_e2e/sheets/sheets_workbook_export_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestSheets_WorkbookExportDryRun pins the +workbook-export dry-run shape. It @@ -44,16 +43,16 @@ func TestSheets_WorkbookExportDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) require.Equal(t, "/open-apis/drive/v1/export_tasks", - gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) - require.Equal(t, "shtDryRunExport", gjson.Get(out, "api.0.body.token").String(), "stdout:\n%s", out) - require.Equal(t, "sheet", gjson.Get(out, "api.0.body.type").String(), + clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) + require.Equal(t, "shtDryRunExport", clie2e.DryRunGet(out, "api.0.body.token").String(), "stdout:\n%s", out) + require.Equal(t, "sheet", clie2e.DryRunGet(out, "api.0.body.type").String(), "workbook-export must hard-code type=sheet; stdout:\n%s", out) - require.Equal(t, "xlsx", gjson.Get(out, "api.0.body.file_extension").String(), "stdout:\n%s", out) - require.False(t, gjson.Get(out, "api.0.body.sub_id").Exists(), + require.Equal(t, "xlsx", clie2e.DryRunGet(out, "api.0.body.file_extension").String(), "stdout:\n%s", out) + require.False(t, clie2e.DryRunGet(out, "api.0.body.sub_id").Exists(), "sub_id should be absent in xlsx mode; stdout:\n%s", out) - require.Equal(t, "./out.xlsx", gjson.Get(out, "output_dir").String(), + require.Equal(t, "./out.xlsx", clie2e.DryRunGet(out, "output_dir").String(), "--output-path carries through to the dry-run plan's top-level output_dir; stdout:\n%s", out) }) @@ -77,8 +76,8 @@ func TestSheets_WorkbookExportDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, "csv", gjson.Get(out, "api.0.body.file_extension").String(), "stdout:\n%s", out) - require.Equal(t, "sheet1", gjson.Get(out, "api.0.body.sub_id").String(), + require.Equal(t, "csv", clie2e.DryRunGet(out, "api.0.body.file_extension").String(), "stdout:\n%s", out) + require.Equal(t, "sheet1", clie2e.DryRunGet(out, "api.0.body.sub_id").String(), "--sheet-id must reach sub_id in csv mode; stdout:\n%s", out) }) diff --git a/tests/cli_e2e/sheets/sheets_workbook_import_dryrun_test.go b/tests/cli_e2e/sheets/sheets_workbook_import_dryrun_test.go index 92bc32d66..653e5d667 100644 --- a/tests/cli_e2e/sheets/sheets_workbook_import_dryrun_test.go +++ b/tests/cli_e2e/sheets/sheets_workbook_import_dryrun_test.go @@ -12,7 +12,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestSheets_WorkbookImportDryRun pins the +workbook-import dry-run shape: a @@ -49,25 +48,25 @@ func TestSheets_WorkbookImportDryRun(t *testing.T) { // api.0 — upload file to obtain the file_token; the wrapper sets // obj_type=sheet in extra so the upload is scoped for sheet import. - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) require.Equal(t, "/open-apis/drive/v1/medias/upload_all", - gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) - require.Contains(t, gjson.Get(out, "api.0.body.extra").String(), `"obj_type":"sheet"`, + clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) + require.Contains(t, clie2e.DryRunGet(out, "api.0.body.extra").String(), `"obj_type":"sheet"`, "upload extra should pin obj_type=sheet; stdout:\n%s", out) - require.Equal(t, "ccm_import_open", gjson.Get(out, "api.0.body.parent_type").String(), + require.Equal(t, "ccm_import_open", clie2e.DryRunGet(out, "api.0.body.parent_type").String(), "stdout:\n%s", out) // api.1 — create import task. type=sheet is the wrapper's whole reason for // existing (drive +import would require --doc-type sheet explicitly); // --name reaches the wire as file_name; file_extension is sniffed from // the local file (.csv). - require.Equal(t, "POST", gjson.Get(out, "api.1.method").String(), "stdout:\n%s", out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.1.method").String(), "stdout:\n%s", out) require.Equal(t, "/open-apis/drive/v1/import_tasks", - gjson.Get(out, "api.1.url").String(), "stdout:\n%s", out) - require.Equal(t, "sheet", gjson.Get(out, "api.1.body.type").String(), + clie2e.DryRunGet(out, "api.1.url").String(), "stdout:\n%s", out) + require.Equal(t, "sheet", clie2e.DryRunGet(out, "api.1.body.type").String(), "workbook-import must hard-code type=sheet; stdout:\n%s", out) - require.Equal(t, "imported", gjson.Get(out, "api.1.body.file_name").String(), + require.Equal(t, "imported", clie2e.DryRunGet(out, "api.1.body.file_name").String(), "--name should reach file_name; stdout:\n%s", out) - require.Equal(t, "csv", gjson.Get(out, "api.1.body.file_extension").String(), + require.Equal(t, "csv", clie2e.DryRunGet(out, "api.1.body.file_extension").String(), "file_extension sniffed from .csv; stdout:\n%s", out) } diff --git a/tests/cli_e2e/stdin_regression_test.go b/tests/cli_e2e/stdin_regression_test.go index 29e5a78aa..5975949ec 100644 --- a/tests/cli_e2e/stdin_regression_test.go +++ b/tests/cli_e2e/stdin_regression_test.go @@ -6,7 +6,6 @@ package clie2e import ( "context" "encoding/json" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -205,18 +204,18 @@ func setDryRunConfigEnv(t *testing.T) { func firstDryRunRequest(t *testing.T, stdout string) map[string]any { t.Helper() - const prefix = "=== Dry Run ===\n" - if !strings.HasPrefix(stdout, prefix) { - t.Fatalf("expected dry-run prefix, got:\n%s", stdout) - } - var payload map[string]any - if err := json.Unmarshal([]byte(strings.TrimPrefix(stdout, prefix)), &payload); err != nil { + if err := json.Unmarshal([]byte(stdout), &payload); err != nil { t.Fatalf("parse dry-run payload: %v\nstdout:\n%s", err, stdout) } + require.Equal(t, true, payload["ok"], "payload missing ok envelope: %#v", payload) + require.Equal(t, true, payload["dry_run"], "payload missing dry_run marker: %#v", payload) - apiEntries, ok := payload["api"].([]any) - require.True(t, ok, "payload missing api array: %#v", payload) + data, ok := payload["data"].(map[string]any) + require.True(t, ok, "payload missing data object: %#v", payload) + + apiEntries, ok := data["api"].([]any) + require.True(t, ok, "payload data missing api array: %#v", payload) require.Len(t, apiEntries, 1) entry, ok := apiEntries[0].(map[string]any) diff --git a/tests/cli_e2e/task/task_get_my_tasks_dryrun_test.go b/tests/cli_e2e/task/task_get_my_tasks_dryrun_test.go index 1d4660d68..e9b9c0da6 100644 --- a/tests/cli_e2e/task/task_get_my_tasks_dryrun_test.go +++ b/tests/cli_e2e/task/task_get_my_tasks_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestTask_GetMyTasksDryRun validates the request shape emitted by @@ -38,28 +37,28 @@ func TestTask_GetMyTasksDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if count := gjson.Get(out, "api.#").Int(); count != 1 { + if count := clie2e.DryRunGet(out, "api.#").Int(); count != 1 { t.Fatalf("expected 1 API call, got %d\nstdout:\n%s", count, out) } - if method := gjson.Get(out, "api.0.method").String(); method != "GET" { + if method := clie2e.DryRunGet(out, "api.0.method").String(); method != "GET" { t.Fatalf("api[0].method = %q, want GET\nstdout:\n%s", method, out) } - if url := gjson.Get(out, "api.0.url").String(); url != "/open-apis/task/v2/tasks" { + if url := clie2e.DryRunGet(out, "api.0.url").String(); url != "/open-apis/task/v2/tasks" { t.Fatalf("api[0].url = %q, want /open-apis/task/v2/tasks\nstdout:\n%s", url, out) } - if got := gjson.Get(out, "api.0.params.type").String(); got != "my_tasks" { + if got := clie2e.DryRunGet(out, "api.0.params.type").String(); got != "my_tasks" { t.Fatalf("api[0].params.type = %q, want my_tasks\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.user_id_type").String(); got != "open_id" { + if got := clie2e.DryRunGet(out, "api.0.params.user_id_type").String(); got != "open_id" { t.Fatalf("api[0].params.user_id_type = %q, want open_id\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.completed").Bool(); !got { + if got := clie2e.DryRunGet(out, "api.0.params.completed").Bool(); !got { t.Fatalf("api[0].params.completed = %v, want true\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.page_token").String(); got != "pt_001" { + if got := clie2e.DryRunGet(out, "api.0.params.page_token").String(); got != "pt_001" { t.Fatalf("api[0].params.page_token = %q, want pt_001\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.params.page_size").Int(); got != 50 { + if got := clie2e.DryRunGet(out, "api.0.params.page_size").Int(); got != 50 { t.Fatalf("api[0].params.page_size = %d, want 50\nstdout:\n%s", got, out) } } diff --git a/tests/cli_e2e/task/task_upload_attachment_dryrun_test.go b/tests/cli_e2e/task/task_upload_attachment_dryrun_test.go index f0ac9e33c..ef0399311 100644 --- a/tests/cli_e2e/task/task_upload_attachment_dryrun_test.go +++ b/tests/cli_e2e/task/task_upload_attachment_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) // TestTask_UploadAttachmentDryRun validates the request shape emitted by @@ -93,31 +92,31 @@ func TestTask_UploadAttachmentDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - if count := gjson.Get(out, "api.#").Int(); count != 1 { + if count := clie2e.DryRunGet(out, "api.#").Int(); count != 1 { t.Fatalf("expected 1 API call, got %d\nstdout:\n%s", count, out) } - if method := gjson.Get(out, "api.0.method").String(); method != "POST" { + if method := clie2e.DryRunGet(out, "api.0.method").String(); method != "POST" { t.Fatalf("api[0].method = %q, want POST\nstdout:\n%s", method, out) } - if url := gjson.Get(out, "api.0.url").String(); url != "/open-apis/task/v2/attachments/upload" { + if url := clie2e.DryRunGet(out, "api.0.url").String(); url != "/open-apis/task/v2/attachments/upload" { t.Fatalf("api[0].url = %q, want /open-apis/task/v2/attachments/upload\nstdout:\n%s", url, out) } - if got := gjson.Get(out, "api.0.params.user_id_type").String(); got != tt.wantUserIDType { + if got := clie2e.DryRunGet(out, "api.0.params.user_id_type").String(); got != tt.wantUserIDType { t.Fatalf("api[0].params.user_id_type = %q, want %q\nstdout:\n%s", got, tt.wantUserIDType, out) } - if got := gjson.Get(out, "api.0.body.resource_type").String(); got != tt.wantResourceType { + if got := clie2e.DryRunGet(out, "api.0.body.resource_type").String(); got != tt.wantResourceType { t.Fatalf("api[0].body.resource_type = %q, want %q\nstdout:\n%s", got, tt.wantResourceType, out) } - if got := gjson.Get(out, "api.0.body.resource_id").String(); got != tt.wantResourceID { + if got := clie2e.DryRunGet(out, "api.0.body.resource_id").String(); got != tt.wantResourceID { t.Fatalf("api[0].body.resource_id = %q, want %q\nstdout:\n%s", got, tt.wantResourceID, out) } - if got := gjson.Get(out, "api.0.body.file.field").String(); got != "file" { + if got := clie2e.DryRunGet(out, "api.0.body.file.field").String(); got != "file" { t.Fatalf("api[0].body.file.field = %q, want file\nstdout:\n%s", got, out) } - if got := gjson.Get(out, "api.0.body.file.path").String(); got != tt.wantFilePath { + if got := clie2e.DryRunGet(out, "api.0.body.file.path").String(); got != tt.wantFilePath { t.Fatalf("api[0].body.file.path = %q, want %q\nstdout:\n%s", got, tt.wantFilePath, out) } - if got := gjson.Get(out, "api.0.body.file.name").String(); got != tt.wantFileName { + if got := clie2e.DryRunGet(out, "api.0.body.file.name").String(); got != tt.wantFileName { t.Fatalf("api[0].body.file.name = %q, want %q\nstdout:\n%s", got, tt.wantFileName, out) } }) diff --git a/tests/cli_e2e/vc/vc_meeting_events_dryrun_test.go b/tests/cli_e2e/vc/vc_meeting_events_dryrun_test.go index 8f01e31ed..1e272cbfe 100644 --- a/tests/cli_e2e/vc/vc_meeting_events_dryrun_test.go +++ b/tests/cli_e2e/vc/vc_meeting_events_dryrun_test.go @@ -10,7 +10,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestVCMeetingEventsDryRun(t *testing.T) { @@ -35,12 +34,12 @@ func TestVCMeetingEventsDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, int64(1), gjson.Get(out, "api.#").Int(), "stdout:\n%s", out) - require.Equal(t, "GET", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - require.Equal(t, "/open-apis/vc/v1/bots/events", gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) - require.Equal(t, "7628568141510692381", gjson.Get(out, "api.0.params.meeting_id").String(), "stdout:\n%s", out) - require.Equal(t, "1710000000000000000", gjson.Get(out, "api.0.params.page_token").String(), "stdout:\n%s", out) - require.Equal(t, "40", gjson.Get(out, "api.0.params.page_size").String(), "stdout:\n%s", out) - require.Equal(t, "1710000000", gjson.Get(out, "api.0.params.start_time").String(), "stdout:\n%s", out) - require.Equal(t, "1710003600", gjson.Get(out, "api.0.params.end_time").String(), "stdout:\n%s", out) + require.Equal(t, int64(1), clie2e.DryRunGet(out, "api.#").Int(), "stdout:\n%s", out) + require.Equal(t, "GET", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + require.Equal(t, "/open-apis/vc/v1/bots/events", clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) + require.Equal(t, "7628568141510692381", clie2e.DryRunGet(out, "api.0.params.meeting_id").String(), "stdout:\n%s", out) + require.Equal(t, "1710000000000000000", clie2e.DryRunGet(out, "api.0.params.page_token").String(), "stdout:\n%s", out) + require.Equal(t, "40", clie2e.DryRunGet(out, "api.0.params.page_size").String(), "stdout:\n%s", out) + require.Equal(t, "1710000000", clie2e.DryRunGet(out, "api.0.params.start_time").String(), "stdout:\n%s", out) + require.Equal(t, "1710003600", clie2e.DryRunGet(out, "api.0.params.end_time").String(), "stdout:\n%s", out) } diff --git a/tests/cli_e2e/vc/vc_meeting_message_send_dryrun_test.go b/tests/cli_e2e/vc/vc_meeting_message_send_dryrun_test.go index 87bd79c74..efb9a3b73 100644 --- a/tests/cli_e2e/vc/vc_meeting_message_send_dryrun_test.go +++ b/tests/cli_e2e/vc/vc_meeting_message_send_dryrun_test.go @@ -65,16 +65,16 @@ func TestVCMeetingMessageSendDryRun(t *testing.T) { result.AssertExitCode(t, 0) out := result.Stdout - require.Equal(t, int64(1), gjson.Get(out, "api.#").Int(), "stdout:\n%s", out) - require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out) - require.Equal(t, "/open-apis/vc/v1/bots/message", gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out) - require.Equal(t, "7651377260537433044", gjson.Get(out, "api.0.body.meeting_id").String(), "stdout:\n%s", out) - require.Equal(t, tt.wantMsgType, gjson.Get(out, "api.0.body.msg_type").String(), "stdout:\n%s", out) - require.Equal(t, tt.wantContent, gjson.Get(out, "api.0.body.content").String(), "stdout:\n%s", out) + require.Equal(t, int64(1), clie2e.DryRunGet(out, "api.#").Int(), "stdout:\n%s", out) + require.Equal(t, "POST", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out) + require.Equal(t, "/open-apis/vc/v1/bots/message", clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out) + require.Equal(t, "7651377260537433044", clie2e.DryRunGet(out, "api.0.body.meeting_id").String(), "stdout:\n%s", out) + require.Equal(t, tt.wantMsgType, clie2e.DryRunGet(out, "api.0.body.msg_type").String(), "stdout:\n%s", out) + require.Equal(t, tt.wantContent, clie2e.DryRunGet(out, "api.0.body.content").String(), "stdout:\n%s", out) if tt.wantUUID == "" { - require.False(t, gjson.Get(out, "api.0.body.uuid").Exists(), "stdout:\n%s", out) + require.False(t, clie2e.DryRunGet(out, "api.0.body.uuid").Exists(), "stdout:\n%s", out) } else { - require.Equal(t, tt.wantUUID, gjson.Get(out, "api.0.body.uuid").String(), "stdout:\n%s", out) + require.Equal(t, tt.wantUUID, clie2e.DryRunGet(out, "api.0.body.uuid").String(), "stdout:\n%s", out) } }) } diff --git a/tests/cli_e2e/wiki/wiki_member_add_dryrun_test.go b/tests/cli_e2e/wiki/wiki_member_add_dryrun_test.go index 6abd8f097..a9fceb8f2 100644 --- a/tests/cli_e2e/wiki/wiki_member_add_dryrun_test.go +++ b/tests/cli_e2e/wiki/wiki_member_add_dryrun_test.go @@ -11,7 +11,6 @@ import ( clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" ) func TestWikiMemberAddDryRun(t *testing.T) { @@ -35,10 +34,10 @@ func TestWikiMemberAddDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "POST", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/wiki/v2/spaces/space_42/members", gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, "cli_app_123", gjson.Get(result.Stdout, "api.0.body.member_id").String()) - assert.Equal(t, "appid", gjson.Get(result.Stdout, "api.0.body.member_type").String()) - assert.Equal(t, "member", gjson.Get(result.Stdout, "api.0.body.member_role").String()) + assert.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/wiki/v2/spaces/space_42/members", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.Equal(t, "cli_app_123", clie2e.DryRunGet(result.Stdout, "api.0.body.member_id").String()) + assert.Equal(t, "appid", clie2e.DryRunGet(result.Stdout, "api.0.body.member_type").String()) + assert.Equal(t, "member", clie2e.DryRunGet(result.Stdout, "api.0.body.member_role").String()) }) } diff --git a/tests/cli_e2e/wiki/wiki_node_create_dryrun_test.go b/tests/cli_e2e/wiki/wiki_node_create_dryrun_test.go index 58b67a392..ea53d890e 100644 --- a/tests/cli_e2e/wiki/wiki_node_create_dryrun_test.go +++ b/tests/cli_e2e/wiki/wiki_node_create_dryrun_test.go @@ -44,11 +44,11 @@ func TestWikiNodeCreateDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "POST", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/wiki/v2/spaces/123456/nodes", gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, "origin", gjson.Get(result.Stdout, "api.0.body.node_type").String()) - assert.Equal(t, "docx", gjson.Get(result.Stdout, "api.0.body.obj_type").String()) - assert.Equal(t, "TestDoc", gjson.Get(result.Stdout, "api.0.body.title").String()) + assert.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/wiki/v2/spaces/123456/nodes", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.Equal(t, "origin", clie2e.DryRunGet(result.Stdout, "api.0.body.node_type").String()) + assert.Equal(t, "docx", clie2e.DryRunGet(result.Stdout, "api.0.body.obj_type").String()) + assert.Equal(t, "TestDoc", clie2e.DryRunGet(result.Stdout, "api.0.body.title").String()) }) t.Run("HappyPath_WithParentNodeToken", func(t *testing.T) { @@ -70,13 +70,13 @@ func TestWikiNodeCreateDryRun(t *testing.T) { result.AssertExitCode(t, 0) // 2-step: resolve parent node -> create node - assert.Equal(t, "GET", gjson.Get(result.Stdout, "api.0.method").String()) - assert.Equal(t, "/open-apis/wiki/v2/spaces/get_node", gjson.Get(result.Stdout, "api.0.url").String()) - assert.Equal(t, "wikcnABC123", gjson.Get(result.Stdout, "api.0.params.token").String()) + assert.Equal(t, "GET", clie2e.DryRunGet(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/wiki/v2/spaces/get_node", clie2e.DryRunGet(result.Stdout, "api.0.url").String()) + assert.Equal(t, "wikcnABC123", clie2e.DryRunGet(result.Stdout, "api.0.params.token").String()) - assert.Equal(t, "POST", gjson.Get(result.Stdout, "api.1.method").String()) - assert.Equal(t, "/open-apis/wiki/v2/spaces/123456/nodes", gjson.Get(result.Stdout, "api.1.url").String()) - assert.Equal(t, "wikcnABC123", gjson.Get(result.Stdout, "api.1.body.parent_node_token").String()) + assert.Equal(t, "POST", clie2e.DryRunGet(result.Stdout, "api.1.method").String()) + assert.Equal(t, "/open-apis/wiki/v2/spaces/123456/nodes", clie2e.DryRunGet(result.Stdout, "api.1.url").String()) + assert.Equal(t, "wikcnABC123", clie2e.DryRunGet(result.Stdout, "api.1.body.parent_node_token").String()) }) t.Run("HappyPath_ShortcutNodeType", func(t *testing.T) { @@ -98,8 +98,8 @@ func TestWikiNodeCreateDryRun(t *testing.T) { require.NoError(t, err) result.AssertExitCode(t, 0) - assert.Equal(t, "shortcut", gjson.Get(result.Stdout, "api.0.body.node_type").String()) - assert.Equal(t, "wikcnORIG", gjson.Get(result.Stdout, "api.0.body.origin_node_token").String()) + assert.Equal(t, "shortcut", clie2e.DryRunGet(result.Stdout, "api.0.body.node_type").String()) + assert.Equal(t, "wikcnORIG", clie2e.DryRunGet(result.Stdout, "api.0.body.origin_node_token").String()) }) t.Run("RejectsShortcutWithoutOriginNodeToken", func(t *testing.T) {