From d13531d26d4a6764599d72086fad795ea36c1ca3 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Wed, 30 Sep 2026 17:12:27 +0530 Subject: [PATCH 1/5] Add CHANGELOG for v1.37.0 --- CHANGELOG.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac147a6fc..c9a045c15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +# [v1.37.0](https://github.com/auth0/auth0-cli/tree/v1.37.0) (September 30, 2026) + +[Full Changelog](https://github.com/auth0/auth0-cli/compare/v1.36.0...v1.37.0) + +### Added +- Add non-persistent env-based authentication mode (`AUTH0_CLI_AUTH_MODE=env`) for agents, CI, and sandboxes that cannot use the OS keychain; domain validation and a new `stored_token_unavailable` error for unreadable keychains [#1692] +- Add `auth0 docs search` command to discover Auth0 documentation from the terminal, with `--json`, `--csv`, and `--open` support [#1686] +- Add `--schema`, `--data`, and `--query` flags to `auth0 logs list`, `auth0 domains`, `auth0 email templates update`, `auth0 universal-login update`, `auth0 universal-login prompts update`, and `auth0 acul config` commands [#1691] +- Add `--organization-usage`, `--organization-require-behavior`, and `--organization-discovery-methods` flags to `auth0 apps create` and `auth0 apps update` [#1690] + +### Changed +- Add `mfa` alias to `auth0 guardian` for easier discovery [#1687] + +### Fixed +- Fix `-q` flag in `auth0 api` to treat each value as a single query parameter, preserving comma-separated values like `fields=a,b,c` instead of splitting them [#1689] + # [v1.36.0](https://github.com/auth0/auth0-cli/tree/v1.36.0) (September 18, 2026) [Full Changelog](https://github.com/auth0/auth0-cli/compare/v1.35.0...v1.36.0) @@ -839,7 +855,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `auth0 tenants add` command in favor of `auth0 login` [#546] - Updating of action triggers which inevitably results in error [#597] -[unreleased]: https://github.com/auth0/auth0-cli/compare/v1.36.0...HEAD +[unreleased]: https://github.com/auth0/auth0-cli/compare/v1.37.0...HEAD +[#1692]: https://github.com/auth0/auth0-cli/pull/1692 +[#1691]: https://github.com/auth0/auth0-cli/pull/1691 +[#1690]: https://github.com/auth0/auth0-cli/pull/1690 +[#1689]: https://github.com/auth0/auth0-cli/pull/1689 +[#1687]: https://github.com/auth0/auth0-cli/pull/1687 +[#1686]: https://github.com/auth0/auth0-cli/pull/1686 [#1681]: https://github.com/auth0/auth0-cli/pull/1681 [#1680]: https://github.com/auth0/auth0-cli/pull/1680 [#1677]: https://github.com/auth0/auth0-cli/pull/1677 From 81745216dd2bd5a188104eb653702dc56d443440 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Sat, 10 Oct 2026 11:08:08 +0530 Subject: [PATCH 2/5] feat: add --schema and --data options for email and phone provider commands --- docs/auth0_email_provider_create.md | 12 +++ docs/auth0_email_provider_update.md | 12 +++ docs/auth0_phone_provider_create.md | 14 ++- docs/auth0_phone_provider_list.md | 11 ++- docs/auth0_phone_provider_update.md | 12 +++ internal/cli/email_provider.go | 89 +++++++++++++++++- internal/cli/email_provider_json_test.go | 84 +++++++++++++++++ internal/cli/phone_provider.go | 112 +++++++++++++++++++++-- internal/cli/phone_provider_json_test.go | 96 +++++++++++++++++++ 9 files changed, 430 insertions(+), 12 deletions(-) create mode 100644 internal/cli/email_provider_json_test.go create mode 100644 internal/cli/phone_provider_json_test.go diff --git a/docs/auth0_email_provider_create.md b/docs/auth0_email_provider_create.md index 3e9a522db..845b8aa33 100644 --- a/docs/auth0_email_provider_create.md +++ b/docs/auth0_email_provider_create.md @@ -11,6 +11,8 @@ To create interactively, use `auth0 email provider create` with no arguments. To create non-interactively, supply the provider name and other information through the flags. +Use `--schema` to print the request payload schema and exit. + ## Usage ``` auth0 email provider create [flags] @@ -34,6 +36,14 @@ auth0 email provider create [flags] auth0 email provider create --provider azure_cs --credentials='{ "connection_string":"TheConnectionString" }' auth0 email provider create --provider ms365 --credentials='{ "tenantId":"TheTenantId", "clientId":"TheClientID", "clientSecret":"TheClientSecret" }' auth0 email provider create --provider custom --enabled=true --default-from-address="admin@example.com" + + # Discover the payload schema + auth0 email provider create --schema + auth0 email provider create --schema --json + + # JSON input mode (for agents and automation) + auth0 email provider create --data '{"name":"sendgrid","credentials":{"api_key":"TheAPIKey"},"enabled":true}' + auth0 email provider create --data @provider.json ``` @@ -41,11 +51,13 @@ auth0 email provider create [flags] ``` -c, --credentials string Credentials for the email provider, formatted as JSON. + --data string JSON payload for the operation, as a JSON string or file path (@file.json). Can also be piped via stdin. -f, --default-from-address string Provider default FROM address if none is specified. -e, --enabled Whether the provided is enabled (true) or disabled (false). (default true) --json Output in json format. --json-compact Output in compact json format. -p, --provider string Provider name. Can be 'mandrill', 'ses', 'sendgrid', 'sparkpost', 'mailgun', 'smtp', 'azure_cs', 'ms365', or 'custom' + --schema Print the request payload schema for this command and exit. Use with --json or --json-compact for machine-readable output. -s, --settings string Settings for the email provider. formatted as JSON. ``` diff --git a/docs/auth0_email_provider_update.md b/docs/auth0_email_provider_update.md index 92369f882..fca0d4531 100644 --- a/docs/auth0_email_provider_update.md +++ b/docs/auth0_email_provider_update.md @@ -11,6 +11,8 @@ To update interactively, use `auth0 email provider update` with no arguments. To update non-interactively, supply the provider name and other information through the flags. +Use `--schema` to print the request payload schema and exit. + ## Usage ``` auth0 email provider update [flags] @@ -38,6 +40,14 @@ auth0 email provider update [flags] auth0 email provider update --provider azure_cs --credentials='{ "connection_string":"TheConnectionString" }' auth0 email provider update --provider ms365 --credentials='{ "tenantId":"TheTenantId", "clientId":"TheClientID", "clientSecret":"TheClientSecret" }' auth0 email provider update --provider custom --enabled=true --default-from-address="admin@example.com" + + # Discover the payload schema + auth0 email provider update --schema + auth0 email provider update --schema --json + + # JSON input mode (for agents and automation) + auth0 email provider update --data '{"credentials":{"api_key":"NewAPIKey"}}' + auth0 email provider update --data @provider.json ``` @@ -45,11 +55,13 @@ auth0 email provider update [flags] ``` -c, --credentials string Credentials for the email provider, formatted as JSON. + --data string JSON payload for the operation, as a JSON string or file path (@file.json). Can also be piped via stdin. -f, --default-from-address string Provider default FROM address if none is specified. -e, --enabled Whether the provided is enabled (true) or disabled (false). (default true) --json Output in json format. --json-compact Output in compact json format. -p, --provider string Provider name. Can be 'mandrill', 'ses', 'sendgrid', 'sparkpost', 'mailgun', 'smtp', 'azure_cs', 'ms365', or 'custom' + --schema Print the request payload schema for this command and exit. Use with --json or --json-compact for machine-readable output. -s, --settings string Settings for the email provider. formatted as JSON. ``` diff --git a/docs/auth0_phone_provider_create.md b/docs/auth0_phone_provider_create.md index ba749ac06..50144942e 100644 --- a/docs/auth0_phone_provider_create.md +++ b/docs/auth0_phone_provider_create.md @@ -11,6 +11,8 @@ To create interactively, use `auth0 phone provider create` with no arguments. To create non-interactively, supply the provider name and other information through the flags. +Use `--schema` to print the request payload schema and exit. + ## Usage ``` auth0 phone provider create [flags] @@ -24,7 +26,15 @@ auth0 phone provider create [flags] auth0 phone provider create --json-compact auth0 phone provider create --provider twilio --disabled=false --credentials='{ "auth_token":"TheAuthToken" }' --configuration='{ "default_from": "admin@example.com", "sid": "+1234567890", "delivery_methods": ["text", "voice"] }' auth0 phone provider create --provider custom --disabled=true --configuration='{ "delivery_methods": ["text", "voice"] }' - auth0 phone provider create -p twilio -d "false" -c '{ "auth_token":"TheAuthToken" }' -s '{ "default_from": "admin@example.com", "sid": "+1234567890", "delivery_methods": ["text"] }' + auth0 phone provider create -p twilio -d "false" -c '{ "auth_token":"TheAuthToken" }' -s '{ "default_from": "admin@example.com", "sid": "+1234567890", "delivery_methods": ["text"] }' + + # Discover the payload schema + auth0 phone provider create --schema + auth0 phone provider create --schema --json + + # JSON input mode (for agents and automation) + auth0 phone provider create --data '{"name":"twilio","credentials":{"auth_token":"TheAuthToken"},"configuration":{"default_from":"+1234567890","delivery_methods":["text"]}}' + auth0 phone provider create --data @provider.json ``` @@ -33,10 +43,12 @@ auth0 phone provider create [flags] ``` -s, --configuration string Configuration for the phone provider. formatted as JSON. -c, --credentials string Credentials for the phone provider, formatted as JSON. + --data string JSON payload for the operation, as a JSON string or file path (@file.json). Can also be piped via stdin. -d, --disabled Whether the provided is disabled (true) or enabled (false). --json Output in json format. --json-compact Output in compact json format. -p, --provider string Provider name. Can be 'twilio', or 'custom' + --schema Print the request payload schema for this command and exit. Use with --json or --json-compact for machine-readable output. ``` diff --git a/docs/auth0_phone_provider_list.md b/docs/auth0_phone_provider_list.md index cfcfea7d6..bc224d16c 100644 --- a/docs/auth0_phone_provider_list.md +++ b/docs/auth0_phone_provider_list.md @@ -16,10 +16,17 @@ auth0 phone provider list [flags] ``` auth0 phone provider list - auth0 phone provider ls + auth0 phone provider ls auth0 phone provider ls --json auth0 phone provider ls --json-compact auth0 phone provider ls --csv + + # Discover the query schema + auth0 phone provider list --schema + auth0 phone provider list --schema --json + + # JSON query (for agents and automation) + auth0 phone provider list --query '{"channel":"sms"}' ``` @@ -29,6 +36,8 @@ auth0 phone provider list [flags] --csv Output in csv format. --json Output in json format. --json-compact Output in compact json format. + -q, --query string Filter results with a JSON object of query parameters. Any API-supported parameter works immediately. Run '--schema' to see documented parameters. On offset-paginated endpoints, add "include_totals":true to receive total counts and a pagination hint (without it the API returns a bare array and no hint can be given). + --schema Print the request payload schema for this command and exit. Use with --json or --json-compact for machine-readable output. ``` diff --git a/docs/auth0_phone_provider_update.md b/docs/auth0_phone_provider_update.md index f8557ff91..c6a574a62 100644 --- a/docs/auth0_phone_provider_update.md +++ b/docs/auth0_phone_provider_update.md @@ -11,6 +11,8 @@ To update interactively, use `auth0 phone provider update` with no arguments. To update non-interactively, supply the provider name and other information through the flags. +Use `--schema` to print the request payload schema and exit. + ## Usage ``` auth0 phone provider update [flags] @@ -28,6 +30,14 @@ auth0 phone provider update [flags] auth0 phone provider update --configuration='{ "default_from": admin@example.com }' auth0 phone provider update --provider twilio --disabled=false --credentials='{ "auth_token":"NewAuthToken" }' --configuration='{ "sid": "+1234567890", "default_from": "admin@example.com", "delivery_methods": ["voice", "text"] }' auth0 phone provider update --provider custom --disabled=false --configuration='{ "delivery_methods": ["voice", "text"] }' + + # Discover the payload schema + auth0 phone provider update --schema + auth0 phone provider update --schema --json + + # JSON input mode (for agents and automation) + auth0 phone provider update --data '{"credentials":{"auth_token":"NewAuthToken"}}' + auth0 phone provider update --data @provider.json ``` @@ -36,10 +46,12 @@ auth0 phone provider update [flags] ``` -s, --configuration string Configuration for the phone provider. formatted as JSON. -c, --credentials string Credentials for the phone provider, formatted as JSON. + --data string JSON payload for the operation, as a JSON string or file path (@file.json). Can also be piped via stdin. -d, --disabled Whether the provided is disabled (true) or enabled (false). --json Output in json format. --json-compact Output in compact json format. -p, --provider string Provider name. Can be 'twilio', or 'custom' + --schema Print the request payload schema for this command and exit. Use with --json or --json-compact for machine-readable output. ``` diff --git a/internal/cli/email_provider.go b/internal/cli/email_provider.go index 1b4c34960..830e0897e 100644 --- a/internal/cli/email_provider.go +++ b/internal/cli/email_provider.go @@ -3,6 +3,7 @@ package cli import ( "encoding/json" "fmt" + "net/http" "github.com/auth0/go-auth0/management" "github.com/spf13/cobra" @@ -136,6 +137,8 @@ func createEmailProviderCmd(cli *cli) *cobra.Command { credentials string settings string enabled bool + Data string + Schema bool } cmd := &cobra.Command{ @@ -145,7 +148,8 @@ func createEmailProviderCmd(cli *cli) *cobra.Command { Long: "Create the email provider.\n\n" + "To create interactively, use `auth0 email provider create` with no arguments.\n\n" + "To create non-interactively, supply the provider name and other information " + - "through the flags.", + "through the flags.\n\n" + + "Use `--schema` to print the request payload schema and exit.", Example: ` auth0 email provider create auth0 email provider create --json auth0 email provider create --json-compact @@ -160,8 +164,28 @@ func createEmailProviderCmd(cli *cli) *cobra.Command { auth0 email provider create --provider smtp --credentials='{ "smtp_host":"smtp.example.com", "smtp_port":25, "smtp_user":"smtp", "smtp_pass":"TheSMTPPassword" }' auth0 email provider create --provider azure_cs --credentials='{ "connection_string":"TheConnectionString" }' auth0 email provider create --provider ms365 --credentials='{ "tenantId":"TheTenantId", "clientId":"TheClientID", "clientSecret":"TheClientSecret" }' - auth0 email provider create --provider custom --enabled=true --default-from-address="admin@example.com"`, + auth0 email provider create --provider custom --enabled=true --default-from-address="admin@example.com" + + # Discover the payload schema + auth0 email provider create --schema + auth0 email provider create --schema --json + + # JSON input mode (for agents and automation) + auth0 email provider create --data '{"name":"sendgrid","credentials":{"api_key":"TheAPIKey"},"enabled":true}' + auth0 email provider create --data @provider.json`, RunE: func(cmd *cobra.Command, args []string) error { + if inputs.Schema { + return printOperationSchema(cli, "POST", "/emails/provider") + } + + payload, provided, err := ResolveData(cmd) + if err != nil { + return err + } + if provided { + return createEmailProviderFromJSON(cli, cmd, payload) + } + if err := emailProviderName.Select(cmd, &inputs.name, providerNameOptions, nil); err != nil { return err } @@ -246,6 +270,9 @@ func createEmailProviderCmd(cli *cli) *cobra.Command { emailProviderCredentials.RegisterString(cmd, &inputs.credentials, "") emailProviderSettings.RegisterString(cmd, &inputs.settings, "") emailProviderEnabled.RegisterBool(cmd, &inputs.enabled, true) + dataFlag.RegisterString(cmd, &inputs.Data, "") + schemaFlag.RegisterBool(cmd, &inputs.Schema, false) + markDataExclusive(cmd) return cmd } @@ -257,6 +284,8 @@ func updateEmailProviderCmd(cli *cli) *cobra.Command { credentials string settings string enabled bool + Data string + Schema bool } cmd := &cobra.Command{ @@ -266,7 +295,8 @@ func updateEmailProviderCmd(cli *cli) *cobra.Command { Long: "Update the email provider.\n\n" + "To update interactively, use `auth0 email provider update` with no arguments.\n\n" + "To update non-interactively, supply the provider name and other information " + - "through the flags.", + "through the flags.\n\n" + + "Use `--schema` to print the request payload schema and exit.", Example: ` auth0 email provider update auth0 email provider update --json auth0 email provider update --json-compact @@ -285,8 +315,28 @@ func updateEmailProviderCmd(cli *cli) *cobra.Command { auth0 email provider update --provider smtp --credentials='{ "smtp_host":"smtp.example.com", "smtp_port":25, "smtp_user":"smtp", "smtp_pass":"TheSMTPPassword" }' auth0 email provider update --provider azure_cs --credentials='{ "connection_string":"TheConnectionString" }' auth0 email provider update --provider ms365 --credentials='{ "tenantId":"TheTenantId", "clientId":"TheClientID", "clientSecret":"TheClientSecret" }' - auth0 email provider update --provider custom --enabled=true --default-from-address="admin@example.com"`, + auth0 email provider update --provider custom --enabled=true --default-from-address="admin@example.com" + + # Discover the payload schema + auth0 email provider update --schema + auth0 email provider update --schema --json + + # JSON input mode (for agents and automation) + auth0 email provider update --data '{"credentials":{"api_key":"NewAPIKey"}}' + auth0 email provider update --data @provider.json`, RunE: func(cmd *cobra.Command, args []string) error { + if inputs.Schema { + return printOperationSchema(cli, "PATCH", "/emails/provider") + } + + payload, provided, err := ResolveData(cmd) + if err != nil { + return err + } + if provided { + return updateEmailProviderFromJSON(cli, cmd, payload) + } + var currentProvider *management.EmailProvider if err := ansi.Waiting(func() (err error) { @@ -390,6 +440,9 @@ func updateEmailProviderCmd(cli *cli) *cobra.Command { emailProviderCredentials.RegisterString(cmd, &inputs.credentials, "") emailProviderSettings.RegisterString(cmd, &inputs.settings, "") emailProviderEnabled.RegisterBool(cmd, &inputs.enabled, true) + dataFlag.RegisterString(cmd, &inputs.Data, "") + schemaFlag.RegisterBool(cmd, &inputs.Schema, false) + markDataExclusive(cmd) return cmd } @@ -433,3 +486,31 @@ func deleteEmailProviderCmd(cli *cli) *cobra.Command { return cmd } + +func createEmailProviderFromJSON(cli *cli, cmd *cobra.Command, dataStr string) error { + ep, err := runJSONWrite[management.EmailProvider](cli, cmd, jsonWriteSpec{ + Method: http.MethodPost, + SchemaPath: "/emails/provider", + URI: cli.api.HTTPClient.URI("emails", "provider"), + Data: dataStr, + SchemaCmd: "auth0 email provider create", + }) + if err != nil { + return fmt.Errorf("failed to create email provider: %w", err) + } + return cli.renderer.EmailProviderCreate(ep) +} + +func updateEmailProviderFromJSON(cli *cli, cmd *cobra.Command, dataStr string) error { + ep, err := runJSONWrite[management.EmailProvider](cli, cmd, jsonWriteSpec{ + Method: http.MethodPatch, + SchemaPath: "/emails/provider", + URI: cli.api.HTTPClient.URI("emails", "provider"), + Data: dataStr, + SchemaCmd: "auth0 email provider update", + }) + if err != nil { + return fmt.Errorf("failed to update email provider: %w", err) + } + return cli.renderer.EmailProviderUpdate(ep) +} diff --git a/internal/cli/email_provider_json_test.go b/internal/cli/email_provider_json_test.go new file mode 100644 index 000000000..f929572a3 --- /dev/null +++ b/internal/cli/email_provider_json_test.go @@ -0,0 +1,84 @@ +package cli + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/auth0/go-auth0/management" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/auth0/auth0-cli/internal/auth0" + "github.com/auth0/auth0-cli/internal/display" +) + +// emailProviderWriteClient captures the method and URI runJSONWrite sends, so a +// --data test can assert the command built the right resource path. +type emailProviderWriteClient struct { + rawHTTPClientStub + gotMethod string + gotURI string +} + +// Request records the method and URI, writing a minimal email-provider response +// into the payload pointer so runJSONWrite can decode it for rendering. +func (c *emailProviderWriteClient) Request(_ context.Context, method, uri string, payload interface{}, _ ...management.RequestOption) error { + c.gotMethod = method + c.gotURI = uri + if raw, ok := payload.(*json.RawMessage); ok { + *raw = json.RawMessage(`{"name":"sendgrid","enabled":true}`) + } + return nil +} + +func newEmailProviderWriteCLI(client *emailProviderWriteClient, stdout *bytes.Buffer) *cli { + return &cli{ + api: &auth0.API{HTTPClient: client}, + renderer: &display.Renderer{ + MessageWriter: io.Discard, + ResultWriter: stdout, + }, + } +} + +func TestEmailProviderCreateCmdData(t *testing.T) { + t.Run("sends the validated payload to POST /emails/provider", func(t *testing.T) { + client := &emailProviderWriteClient{} + cmd := createEmailProviderCmd(newEmailProviderWriteCLI(client, &bytes.Buffer{})) + // The leaf command runs without the root PreRun, so clear the required-flag + // annotations here (stdin is non-TTY under test) to reach RunE with only --data. + prepareInteractivity(cmd) + cmd.SetArgs([]string{"--data", `{"name":"sendgrid","credentials":{"api_key":"key"},"enabled":true}`}) + + require.NoError(t, cmd.Execute()) + assert.Equal(t, http.MethodPost, client.gotMethod) + assert.Contains(t, client.gotURI, "/emails/provider") + }) + + t.Run("surfaces a schema validation failure before calling the API", func(t *testing.T) { + client := &emailProviderWriteClient{} + cmd := createEmailProviderCmd(newEmailProviderWriteCLI(client, &bytes.Buffer{})) + prepareInteractivity(cmd) + cmd.SetArgs([]string{"--data", `{"name":123}`}) + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "schema validation") + assert.Empty(t, client.gotMethod, "must not reach the API when validation fails") + }) +} + +func TestEmailProviderUpdateCmdData(t *testing.T) { + client := &emailProviderWriteClient{} + cmd := updateEmailProviderCmd(newEmailProviderWriteCLI(client, &bytes.Buffer{})) + prepareInteractivity(cmd) + cmd.SetArgs([]string{"--data", `{"credentials":{"api_key":"NewKey"}}`}) + + require.NoError(t, cmd.Execute()) + assert.Equal(t, http.MethodPatch, client.gotMethod) + assert.Contains(t, client.gotURI, "/emails/provider") +} diff --git a/internal/cli/phone_provider.go b/internal/cli/phone_provider.go index 868095757..80e3f6b33 100644 --- a/internal/cli/phone_provider.go +++ b/internal/cli/phone_provider.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "github.com/auth0/auth0-cli/internal/auth0" @@ -123,17 +124,34 @@ func showBrandingPhoneProviderCmd(cli *cli) *cobra.Command { } func listBrandingPhoneProviderCmd(cli *cli) *cobra.Command { + var schema bool + var query string + cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List your Phone providers", Long: "List your existing Phone providers. Currently we can create a max of 1 phone Provider, If none are created, you can create one by running `auth0 phone provider create`.", Example: ` auth0 phone provider list - auth0 phone provider ls + auth0 phone provider ls auth0 phone provider ls --json auth0 phone provider ls --json-compact - auth0 phone provider ls --csv`, + auth0 phone provider ls --csv + + # Discover the query schema + auth0 phone provider list --schema + auth0 phone provider list --schema --json + + # JSON query (for agents and automation) + auth0 phone provider list --query '{"channel":"sms"}'`, RunE: func(cmd *cobra.Command, args []string) error { + if schema { + return printOperationSchema(cli, "GET", "/branding/phone/providers") + } + if query != "" { + return runJSONQuery(cli, cmd, jsonQuerySpec{Path: "branding/phone/providers", SchemaCmd: "auth0 phone provider list"}, query) + } + var list *management.BrandingPhoneProviderList if err := ansi.Waiting(func() (err error) { list, err = cli.api.Branding.ListPhoneProviders(cmd.Context()) @@ -150,6 +168,8 @@ func listBrandingPhoneProviderCmd(cli *cli) *cobra.Command { cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") cmd.Flags().BoolVar(&cli.csv, "csv", false, "Output in csv format.") cmd.MarkFlagsMutuallyExclusive("json", "json-compact", "csv") + schemaFlag.RegisterBool(cmd, &schema, false) + listQueryFlag.RegisterString(cmd, &query, "") return cmd } @@ -160,6 +180,8 @@ func createBrandingPhoneProviderCmd(cli *cli) *cobra.Command { credentials string configuration string disabled bool + Data string + Schema bool } cmd := &cobra.Command{ @@ -168,14 +190,35 @@ func createBrandingPhoneProviderCmd(cli *cli) *cobra.Command { Long: "Create the phone provider.\n\n" + "To create interactively, use `auth0 phone provider create` with no arguments.\n\n" + "To create non-interactively, supply the provider name and other information " + - "through the flags.", + "through the flags.\n\n" + + "Use `--schema` to print the request payload schema and exit.", Example: ` auth0 phone provider create auth0 phone provider create --json auth0 phone provider create --json-compact auth0 phone provider create --provider twilio --disabled=false --credentials='{ "auth_token":"TheAuthToken" }' --configuration='{ "default_from": "admin@example.com", "sid": "+1234567890", "delivery_methods": ["text", "voice"] }' auth0 phone provider create --provider custom --disabled=true --configuration='{ "delivery_methods": ["text", "voice"] }' - auth0 phone provider create -p twilio -d "false" -c '{ "auth_token":"TheAuthToken" }' -s '{ "default_from": "admin@example.com", "sid": "+1234567890", "delivery_methods": ["text"] }' `, + auth0 phone provider create -p twilio -d "false" -c '{ "auth_token":"TheAuthToken" }' -s '{ "default_from": "admin@example.com", "sid": "+1234567890", "delivery_methods": ["text"] }' + + # Discover the payload schema + auth0 phone provider create --schema + auth0 phone provider create --schema --json + + # JSON input mode (for agents and automation) + auth0 phone provider create --data '{"name":"twilio","credentials":{"auth_token":"TheAuthToken"},"configuration":{"default_from":"+1234567890","delivery_methods":["text"]}}' + auth0 phone provider create --data @provider.json`, RunE: func(cmd *cobra.Command, args []string) error { + if inputs.Schema { + return printOperationSchema(cli, "POST", "/branding/phone/providers") + } + + payload, provided, err := ResolveData(cmd) + if err != nil { + return err + } + if provided { + return createPhoneProviderFromJSON(cli, cmd, payload) + } + if err := phoneProviderName.Select(cmd, &inputs.name, PhoneProviderNameOptions, nil); err != nil { return err } @@ -236,6 +279,9 @@ func createBrandingPhoneProviderCmd(cli *cli) *cobra.Command { phoneProviderCredentials.RegisterString(cmd, &inputs.credentials, "") phoneProviderConfiguration.RegisterString(cmd, &inputs.configuration, "") phoneProviderDisabled.RegisterBool(cmd, &inputs.disabled, false) + dataFlag.RegisterString(cmd, &inputs.Data, "") + schemaFlag.RegisterBool(cmd, &inputs.Schema, false) + markDataExclusive(cmd) return cmd } @@ -267,6 +313,8 @@ func updateBrandingPhoneProviderCmd(cli *cli) *cobra.Command { credentials string configuration string disabled bool + Data string + Schema bool } cmd := &cobra.Command{ @@ -275,7 +323,8 @@ func updateBrandingPhoneProviderCmd(cli *cli) *cobra.Command { Long: "Update the phone provider.\n\n" + "To update interactively, use `auth0 phone provider update` with no arguments.\n\n" + "To update non-interactively, supply the provider name and other information " + - "through the flags.", + "through the flags.\n\n" + + "Use `--schema` to print the request payload schema and exit.", Example: ` auth0 phone provider update auth0 phone provider update --json auth0 phone provider update --json-compact @@ -284,8 +333,20 @@ func updateBrandingPhoneProviderCmd(cli *cli) *cobra.Command { auth0 phone provider update --configuration='{ "delivery_methods": ["voice"] }' auth0 phone provider update --configuration='{ "default_from": admin@example.com }' auth0 phone provider update --provider twilio --disabled=false --credentials='{ "auth_token":"NewAuthToken" }' --configuration='{ "sid": "+1234567890", "default_from": "admin@example.com", "delivery_methods": ["voice", "text"] }' - auth0 phone provider update --provider custom --disabled=false --configuration='{ "delivery_methods": ["voice", "text"] }'`, + auth0 phone provider update --provider custom --disabled=false --configuration='{ "delivery_methods": ["voice", "text"] }' + + # Discover the payload schema + auth0 phone provider update --schema + auth0 phone provider update --schema --json + + # JSON input mode (for agents and automation) + auth0 phone provider update --data '{"credentials":{"auth_token":"NewAuthToken"}}' + auth0 phone provider update --data @provider.json`, RunE: func(cmd *cobra.Command, args []string) error { + if inputs.Schema { + return printOperationSchema(cli, "PATCH", "/branding/phone/providers/{id}") + } + var ( existingProvider *management.BrandingPhoneProvider credentials *management.BrandingPhoneProviderCredential @@ -301,6 +362,14 @@ func updateBrandingPhoneProviderCmd(cli *cli) *cobra.Command { inputs.id = args[0] } + payload, provided, err := ResolveData(cmd) + if err != nil { + return err + } + if provided { + return updatePhoneProviderFromJSON(cli, cmd, inputs.id, payload) + } + if err := ansi.Waiting(func() (err error) { existingProvider, err = cli.api.Branding.ReadPhoneProvider(cmd.Context(), inputs.id) return @@ -372,6 +441,9 @@ func updateBrandingPhoneProviderCmd(cli *cli) *cobra.Command { phoneProviderCredentials.RegisterStringU(cmd, &inputs.credentials, "") phoneProviderConfiguration.RegisterStringU(cmd, &inputs.configuration, "") phoneProviderDisabled.RegisterBool(cmd, &inputs.disabled, false) + dataFlag.RegisterString(cmd, &inputs.Data, "") + schemaFlag.RegisterBool(cmd, &inputs.Schema, false) + markDataExclusive(cmd) return cmd } @@ -425,3 +497,31 @@ auth0 phone provider rm --force`, return cmd } + +func createPhoneProviderFromJSON(cli *cli, cmd *cobra.Command, dataStr string) error { + pp, err := runJSONWrite[management.BrandingPhoneProvider](cli, cmd, jsonWriteSpec{ + Method: http.MethodPost, + SchemaPath: "/branding/phone/providers", + URI: cli.api.HTTPClient.URI("branding", "phone", "providers"), + Data: dataStr, + SchemaCmd: "auth0 phone provider create", + }) + if err != nil { + return fmt.Errorf("failed to create phone provider: %w", err) + } + return cli.renderer.PhoneProviderCreate(pp) +} + +func updatePhoneProviderFromJSON(cli *cli, cmd *cobra.Command, id, dataStr string) error { + pp, err := runJSONWrite[management.BrandingPhoneProvider](cli, cmd, jsonWriteSpec{ + Method: http.MethodPatch, + SchemaPath: "/branding/phone/providers/{id}", + URI: cli.api.HTTPClient.URI("branding", "phone", "providers", id), + Data: dataStr, + SchemaCmd: "auth0 phone provider update", + }) + if err != nil { + return fmt.Errorf("failed to update phone provider with ID %q: %w", id, err) + } + return cli.renderer.PhoneProviderUpdate(pp) +} diff --git a/internal/cli/phone_provider_json_test.go b/internal/cli/phone_provider_json_test.go new file mode 100644 index 000000000..a1ae645a2 --- /dev/null +++ b/internal/cli/phone_provider_json_test.go @@ -0,0 +1,96 @@ +package cli + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/auth0/go-auth0/management" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/auth0/auth0-cli/internal/auth0" + "github.com/auth0/auth0-cli/internal/display" +) + +// phoneProviderWriteClient captures the method and URI runJSONWrite sends, so a +// --data test can assert the command built the right resource path. +type phoneProviderWriteClient struct { + rawHTTPClientStub + gotMethod string + gotURI string +} + +// Request records the method and URI, writing a minimal phone-provider response +// into the payload pointer so runJSONWrite can decode it for rendering. +func (c *phoneProviderWriteClient) Request(_ context.Context, method, uri string, payload interface{}, _ ...management.RequestOption) error { + c.gotMethod = method + c.gotURI = uri + if raw, ok := payload.(*json.RawMessage); ok { + *raw = json.RawMessage(`{"id":"pp_123","name":"twilio","disabled":false}`) + } + return nil +} + +func newPhoneProviderWriteCLI(client *phoneProviderWriteClient, stdout *bytes.Buffer) *cli { + return &cli{ + api: &auth0.API{HTTPClient: client}, + renderer: &display.Renderer{ + MessageWriter: io.Discard, + ResultWriter: stdout, + }, + } +} + +func TestPhoneProviderListCmdInvalidQuery(t *testing.T) { + c := &cli{ + renderer: &display.Renderer{MessageWriter: io.Discard, ResultWriter: io.Discard}, + api: &auth0.API{}, + } + + cmd := listBrandingPhoneProviderCmd(c) + cmd.SetArgs([]string{"--query", "not-valid-json"}) + + assert.ErrorContains(t, cmd.Execute(), "invalid --query value") +} + +func TestPhoneProviderCreateCmdData(t *testing.T) { + t.Run("sends the validated payload to POST /branding/phone/providers", func(t *testing.T) { + client := &phoneProviderWriteClient{} + cmd := createBrandingPhoneProviderCmd(newPhoneProviderWriteCLI(client, &bytes.Buffer{})) + // The leaf command runs without the root PreRun, so clear the required-flag + // annotations here (stdin is non-TTY under test) to reach RunE with only --data. + prepareInteractivity(cmd) + cmd.SetArgs([]string{"--data", `{"name":"twilio","credentials":{"auth_token":"tok"},"configuration":{"delivery_methods":["text"]}}`}) + + require.NoError(t, cmd.Execute()) + assert.Equal(t, http.MethodPost, client.gotMethod) + assert.Contains(t, client.gotURI, "/branding/phone/providers") + }) + + t.Run("surfaces a schema validation failure before calling the API", func(t *testing.T) { + client := &phoneProviderWriteClient{} + cmd := createBrandingPhoneProviderCmd(newPhoneProviderWriteCLI(client, &bytes.Buffer{})) + prepareInteractivity(cmd) + cmd.SetArgs([]string{"--data", `{"name":123}`}) + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "schema validation") + assert.Empty(t, client.gotMethod, "must not reach the API when validation fails") + }) +} + +func TestPhoneProviderUpdateCmdData(t *testing.T) { + client := &phoneProviderWriteClient{} + cmd := updateBrandingPhoneProviderCmd(newPhoneProviderWriteCLI(client, &bytes.Buffer{})) + prepareInteractivity(cmd) + cmd.SetArgs([]string{"pp_123", "--data", `{"credentials":{"auth_token":"newTok"}}`}) + + require.NoError(t, cmd.Execute()) + assert.Equal(t, http.MethodPatch, client.gotMethod) + assert.Contains(t, client.gotURI, "/branding/phone/providers/pp_123") +} From a8252e4ae4ac5ad2cf5d2773e10a20a3c21991cb Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Wed, 14 Oct 2026 23:01:16 +0530 Subject: [PATCH 3/5] feat: enhance --data option handling for email and phone provider commands --- CHANGELOG.md | 8 +++++- docs/auth0_email_provider_create.md | 3 ++- docs/auth0_email_provider_update.md | 2 +- docs/auth0_phone_provider_list.md | 2 +- internal/cli/data_json.go | 31 ++++++++++++++++++++++-- internal/cli/email_provider.go | 27 ++++++++++++--------- internal/cli/email_provider_json_test.go | 20 ++++++++++----- internal/cli/phone_provider.go | 2 +- 8 files changed, 70 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 137799404..14d505268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -856,7 +856,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `auth0 tenants add` command in favor of `auth0 login` [#546] - Updating of action triggers which inevitably results in error [#597] -[unreleased]: https://github.com/auth0/auth0-cli/compare/v1.36.0...HEAD +[unreleased]: https://github.com/auth0/auth0-cli/compare/v1.37.0...HEAD +[#1692]: https://github.com/auth0/auth0-cli/pull/1692 +[#1691]: https://github.com/auth0/auth0-cli/pull/1691 +[#1690]: https://github.com/auth0/auth0-cli/pull/1690 +[#1689]: https://github.com/auth0/auth0-cli/pull/1689 +[#1687]: https://github.com/auth0/auth0-cli/pull/1687 +[#1686]: https://github.com/auth0/auth0-cli/pull/1686 [#1681]: https://github.com/auth0/auth0-cli/pull/1681 [#1680]: https://github.com/auth0/auth0-cli/pull/1680 [#1677]: https://github.com/auth0/auth0-cli/pull/1677 diff --git a/docs/auth0_email_provider_create.md b/docs/auth0_email_provider_create.md index 845b8aa33..c63036df3 100644 --- a/docs/auth0_email_provider_create.md +++ b/docs/auth0_email_provider_create.md @@ -11,7 +11,7 @@ To create interactively, use `auth0 email provider create` with no arguments. To create non-interactively, supply the provider name and other information through the flags. -Use `--schema` to print the request payload schema and exit. +Use `--schema` to print the request payload schema and exit. A `--data` payload is sent as-is and validated by the API. ## Usage ``` @@ -43,6 +43,7 @@ auth0 email provider create [flags] # JSON input mode (for agents and automation) auth0 email provider create --data '{"name":"sendgrid","credentials":{"api_key":"TheAPIKey"},"enabled":true}' + auth0 email provider create --data '{"name":"custom","credentials":{},"enabled":true}' auth0 email provider create --data @provider.json ``` diff --git a/docs/auth0_email_provider_update.md b/docs/auth0_email_provider_update.md index fca0d4531..0190c5afd 100644 --- a/docs/auth0_email_provider_update.md +++ b/docs/auth0_email_provider_update.md @@ -11,7 +11,7 @@ To update interactively, use `auth0 email provider update` with no arguments. To update non-interactively, supply the provider name and other information through the flags. -Use `--schema` to print the request payload schema and exit. +Use `--schema` to print the request payload schema and exit. A `--data` payload is sent as-is and validated by the API. ## Usage ``` diff --git a/docs/auth0_phone_provider_list.md b/docs/auth0_phone_provider_list.md index bc224d16c..d6524aa79 100644 --- a/docs/auth0_phone_provider_list.md +++ b/docs/auth0_phone_provider_list.md @@ -26,7 +26,7 @@ auth0 phone provider list [flags] auth0 phone provider list --schema --json # JSON query (for agents and automation) - auth0 phone provider list --query '{"channel":"sms"}' + auth0 phone provider list --query '{"disabled":false}' ``` diff --git a/internal/cli/data_json.go b/internal/cli/data_json.go index 571447461..a9119d7d0 100644 --- a/internal/cli/data_json.go +++ b/internal/cli/data_json.go @@ -116,6 +116,20 @@ func readAndValidateJSON(cli *cli, inputStr, method, path string) (json.RawMessa return handler.ReadAndValidate(inputStr, method, path) } +// readJSONObject reads a --data payload and checks only that it is a JSON object. +func readJSONObject(input string) (json.RawMessage, error) { + data, err := readJSONInput(input) + if err != nil { + return nil, validationError{err: fmt.Errorf("failed to read JSON input: %w", err)} + } + + var obj map[string]json.RawMessage + if err := json.Unmarshal(data, &obj); err != nil || obj == nil { + return nil, validationError{err: fmt.Errorf("the payload must be a JSON object"), reason: "malformed_json"} + } + return data, nil +} + // readJSONInput reads JSON from various input sources. func readJSONInput(input string) ([]byte, error) { if input == "" { @@ -220,6 +234,10 @@ type jsonWriteSpec struct { URI string // Fully-qualified request URL. Data string // Raw --data value (inline JSON, @file, or piped payload). SchemaCmd string // Command to suggest in the "--schema" hint, e.g. "auth0 actions create". + + // SkipValidation sends the payload after checking only that it is a JSON object, + // leaving validation to the API, for operations whose published schema is unreliable. + SkipValidation bool } // runJSONWrite validates a --data payload against the OpenAPI schema, sends it to @@ -228,7 +246,16 @@ type jsonWriteSpec struct { // supplying their spec and the management type; the API's own semantics (e.g. PATCH // preserving unspecified fields) apply to the exact bytes sent. func runJSONWrite[T any](cli *cli, cmd *cobra.Command, spec jsonWriteSpec) (*T, error) { - payload, validated, err := readAndValidateJSON(cli, spec.Data, spec.Method, spec.SchemaPath) + var ( + payload json.RawMessage + validated = true + err error + ) + if spec.SkipValidation { + payload, err = readJSONObject(spec.Data) + } else { + payload, validated, err = readAndValidateJSON(cli, spec.Data, spec.Method, spec.SchemaPath) + } if err != nil { cli.renderer.Infof("Run '%s --schema' to see the expected schema.", spec.SchemaCmd) return nil, err @@ -251,7 +278,7 @@ func runJSONWrite[T any](cli *cli, cmd *cobra.Command, spec jsonWriteSpec) (*T, // validation via the 400 status and the schema stays discoverable through // --schema. Keep the API's own error clean for machines and enrich it with // the schema hint only for humans. - if cli.wantsJSONError() { + if cli.wantsJSONError() || spec.SkipValidation { return nil, err } return nil, enhanceAPIError(err, spec.Method, spec.SchemaPath) diff --git a/internal/cli/email_provider.go b/internal/cli/email_provider.go index 830e0897e..c579384d8 100644 --- a/internal/cli/email_provider.go +++ b/internal/cli/email_provider.go @@ -149,7 +149,7 @@ func createEmailProviderCmd(cli *cli) *cobra.Command { "To create interactively, use `auth0 email provider create` with no arguments.\n\n" + "To create non-interactively, supply the provider name and other information " + "through the flags.\n\n" + - "Use `--schema` to print the request payload schema and exit.", + "Use `--schema` to print the request payload schema and exit. A `--data` payload is sent as-is and validated by the API.", Example: ` auth0 email provider create auth0 email provider create --json auth0 email provider create --json-compact @@ -172,6 +172,7 @@ func createEmailProviderCmd(cli *cli) *cobra.Command { # JSON input mode (for agents and automation) auth0 email provider create --data '{"name":"sendgrid","credentials":{"api_key":"TheAPIKey"},"enabled":true}' + auth0 email provider create --data '{"name":"custom","credentials":{},"enabled":true}' auth0 email provider create --data @provider.json`, RunE: func(cmd *cobra.Command, args []string) error { if inputs.Schema { @@ -296,7 +297,7 @@ func updateEmailProviderCmd(cli *cli) *cobra.Command { "To update interactively, use `auth0 email provider update` with no arguments.\n\n" + "To update non-interactively, supply the provider name and other information " + "through the flags.\n\n" + - "Use `--schema` to print the request payload schema and exit.", + "Use `--schema` to print the request payload schema and exit. A `--data` payload is sent as-is and validated by the API.", Example: ` auth0 email provider update auth0 email provider update --json auth0 email provider update --json-compact @@ -489,11 +490,12 @@ func deleteEmailProviderCmd(cli *cli) *cobra.Command { func createEmailProviderFromJSON(cli *cli, cmd *cobra.Command, dataStr string) error { ep, err := runJSONWrite[management.EmailProvider](cli, cmd, jsonWriteSpec{ - Method: http.MethodPost, - SchemaPath: "/emails/provider", - URI: cli.api.HTTPClient.URI("emails", "provider"), - Data: dataStr, - SchemaCmd: "auth0 email provider create", + Method: http.MethodPost, + SchemaPath: "/emails/provider", + URI: cli.api.HTTPClient.URI("emails", "provider"), + Data: dataStr, + SchemaCmd: "auth0 email provider create", + SkipValidation: true, }) if err != nil { return fmt.Errorf("failed to create email provider: %w", err) @@ -503,11 +505,12 @@ func createEmailProviderFromJSON(cli *cli, cmd *cobra.Command, dataStr string) e func updateEmailProviderFromJSON(cli *cli, cmd *cobra.Command, dataStr string) error { ep, err := runJSONWrite[management.EmailProvider](cli, cmd, jsonWriteSpec{ - Method: http.MethodPatch, - SchemaPath: "/emails/provider", - URI: cli.api.HTTPClient.URI("emails", "provider"), - Data: dataStr, - SchemaCmd: "auth0 email provider update", + Method: http.MethodPatch, + SchemaPath: "/emails/provider", + URI: cli.api.HTTPClient.URI("emails", "provider"), + Data: dataStr, + SchemaCmd: "auth0 email provider update", + SkipValidation: true, }) if err != nil { return fmt.Errorf("failed to update email provider: %w", err) diff --git a/internal/cli/email_provider_json_test.go b/internal/cli/email_provider_json_test.go index f929572a3..65881b2ac 100644 --- a/internal/cli/email_provider_json_test.go +++ b/internal/cli/email_provider_json_test.go @@ -59,16 +59,24 @@ func TestEmailProviderCreateCmdData(t *testing.T) { assert.Contains(t, client.gotURI, "/emails/provider") }) - t.Run("surfaces a schema validation failure before calling the API", func(t *testing.T) { + t.Run("leaves schema validation to the API", func(t *testing.T) { client := &emailProviderWriteClient{} cmd := createEmailProviderCmd(newEmailProviderWriteCLI(client, &bytes.Buffer{})) prepareInteractivity(cmd) - cmd.SetArgs([]string{"--data", `{"name":123}`}) + cmd.SetArgs([]string{"--data", `{"name":"custom","enabled":true}`}) - err := cmd.Execute() - require.Error(t, err) - assert.Contains(t, err.Error(), "schema validation") - assert.Empty(t, client.gotMethod, "must not reach the API when validation fails") + require.NoError(t, cmd.Execute()) + assert.Equal(t, http.MethodPost, client.gotMethod) + }) + + t.Run("rejects a payload that is not a JSON object", func(t *testing.T) { + client := &emailProviderWriteClient{} + cmd := createEmailProviderCmd(newEmailProviderWriteCLI(client, &bytes.Buffer{})) + prepareInteractivity(cmd) + cmd.SetArgs([]string{"--data", `["custom"]`}) + + assert.ErrorContains(t, cmd.Execute(), "must be a JSON object") + assert.Empty(t, client.gotMethod, "must not reach the API when the payload is not an object") }) } diff --git a/internal/cli/phone_provider.go b/internal/cli/phone_provider.go index 80e3f6b33..f82e4accd 100644 --- a/internal/cli/phone_provider.go +++ b/internal/cli/phone_provider.go @@ -143,7 +143,7 @@ func listBrandingPhoneProviderCmd(cli *cli) *cobra.Command { auth0 phone provider list --schema --json # JSON query (for agents and automation) - auth0 phone provider list --query '{"channel":"sms"}'`, + auth0 phone provider list --query '{"disabled":false}'`, RunE: func(cmd *cobra.Command, args []string) error { if schema { return printOperationSchema(cli, "GET", "/branding/phone/providers") From 3a93d6ceafaa4d866124bb5ac71959b24b180ba8 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Thu, 15 Oct 2026 20:27:32 +0530 Subject: [PATCH 4/5] feat: update documentation for --data and --schema options in email and phone provider commands --- docs/auth0_email_provider_create.md | 4 +- docs/auth0_email_provider_update.md | 2 +- docs/auth0_phone_provider_create.md | 2 +- docs/auth0_phone_provider_list.md | 3 + docs/auth0_phone_provider_update.md | 2 +- internal/cli/data_json.go | 8 +- internal/cli/data_json_test.go | 22 ++++ internal/cli/email_provider.go | 6 +- internal/cli/email_provider_json_test.go | 139 +++++++++++++---------- internal/cli/phone_provider.go | 8 +- internal/cli/phone_provider_json_test.go | 88 +++++++------- 11 files changed, 171 insertions(+), 113 deletions(-) diff --git a/docs/auth0_email_provider_create.md b/docs/auth0_email_provider_create.md index c63036df3..b66af2d34 100644 --- a/docs/auth0_email_provider_create.md +++ b/docs/auth0_email_provider_create.md @@ -11,7 +11,9 @@ To create interactively, use `auth0 email provider create` with no arguments. To create non-interactively, supply the provider name and other information through the flags. -Use `--schema` to print the request payload schema and exit. A `--data` payload is sent as-is and validated by the API. +Use `--data` to supply the whole payload as JSON (inline, `@file.json`, or piped via stdin). It is sent as-is and validated by the API. Use `--schema` to print the request payload schema and exit. + +For a `custom` provider, `--data` must include `"credentials":{}`; the API rejects the request without it. ## Usage ``` diff --git a/docs/auth0_email_provider_update.md b/docs/auth0_email_provider_update.md index 0190c5afd..d1c6b978e 100644 --- a/docs/auth0_email_provider_update.md +++ b/docs/auth0_email_provider_update.md @@ -11,7 +11,7 @@ To update interactively, use `auth0 email provider update` with no arguments. To update non-interactively, supply the provider name and other information through the flags. -Use `--schema` to print the request payload schema and exit. A `--data` payload is sent as-is and validated by the API. +Use `--data` to supply the whole payload as JSON (inline, `@file.json`, or piped via stdin). It is sent as-is and validated by the API. Use `--schema` to print the request payload schema and exit. ## Usage ``` diff --git a/docs/auth0_phone_provider_create.md b/docs/auth0_phone_provider_create.md index 50144942e..6a64101a1 100644 --- a/docs/auth0_phone_provider_create.md +++ b/docs/auth0_phone_provider_create.md @@ -11,7 +11,7 @@ To create interactively, use `auth0 phone provider create` with no arguments. To create non-interactively, supply the provider name and other information through the flags. -Use `--schema` to print the request payload schema and exit. +Use `--data` to supply the whole payload as JSON (inline, `@file.json`, or piped via stdin). It is validated against the OpenAPI schema before it is sent. Use `--schema` to print the request payload schema and exit. ## Usage ``` diff --git a/docs/auth0_phone_provider_list.md b/docs/auth0_phone_provider_list.md index d6524aa79..ffa720cc9 100644 --- a/docs/auth0_phone_provider_list.md +++ b/docs/auth0_phone_provider_list.md @@ -7,6 +7,9 @@ has_toc: false List your existing Phone providers. Currently we can create a max of 1 phone Provider, If none are created, you can create one by running `auth0 phone provider create`. +Use '--schema' to see available query parameters. +Use '--query' to filter results via a JSON object (any API-supported parameter works immediately). + ## Usage ``` auth0 phone provider list [flags] diff --git a/docs/auth0_phone_provider_update.md b/docs/auth0_phone_provider_update.md index c6a574a62..bcf72d43d 100644 --- a/docs/auth0_phone_provider_update.md +++ b/docs/auth0_phone_provider_update.md @@ -11,7 +11,7 @@ To update interactively, use `auth0 phone provider update` with no arguments. To update non-interactively, supply the provider name and other information through the flags. -Use `--schema` to print the request payload schema and exit. +Use `--data` to supply the whole payload as JSON (inline, `@file.json`, or piped via stdin). It is validated against the OpenAPI schema before it is sent. Use `--schema` to print the request payload schema and exit. ## Usage ``` diff --git a/internal/cli/data_json.go b/internal/cli/data_json.go index a9119d7d0..ed40af8de 100644 --- a/internal/cli/data_json.go +++ b/internal/cli/data_json.go @@ -2,6 +2,7 @@ package cli import ( "encoding/json" + "errors" "fmt" "os" "strings" @@ -124,8 +125,11 @@ func readJSONObject(input string) (json.RawMessage, error) { } var obj map[string]json.RawMessage - if err := json.Unmarshal(data, &obj); err != nil || obj == nil { - return nil, validationError{err: fmt.Errorf("the payload must be a JSON object"), reason: "malformed_json"} + if err := json.Unmarshal(data, &obj); err != nil { + return nil, validationError{err: fmt.Errorf("invalid JSON: the payload must be a JSON object: %w", err), reason: "malformed_json"} + } + if obj == nil { + return nil, validationError{err: errors.New("the payload must be a JSON object"), reason: "malformed_json"} } return data, nil } diff --git a/internal/cli/data_json_test.go b/internal/cli/data_json_test.go index 57bb563ed..8f936204a 100644 --- a/internal/cli/data_json_test.go +++ b/internal/cli/data_json_test.go @@ -57,6 +57,28 @@ func withPipedStdin(t *testing.T, content string, fn func()) { require.NoError(t, r.Close()) } +func TestReadJSONObject(t *testing.T) { + for name, tc := range map[string]struct{ data, wantErr string }{ + "object": {`{"name":"x"}`, ""}, + "empty object": {`{}`, ""}, + "truncated keeps the parse detail": {`{"name":"x",`, "unexpected end of JSON input"}, + "empty input": {``, "no input provided"}, + "array": {`["x"]`, "must be a JSON object"}, + "string": {`"x"`, "must be a JSON object"}, + "null": {`null`, "must be a JSON object"}, + "number": {`5`, "must be a JSON object"}, + } { + t.Run(name, func(t *testing.T) { + _, err := readJSONObject(tc.data) + if tc.wantErr == "" { + require.NoError(t, err) + return + } + assert.ErrorContains(t, err, tc.wantErr) + }) + } +} + func TestReadJSONInput(t *testing.T) { t.Run("inline JSON is returned verbatim", func(t *testing.T) { data, err := readJSONInput(`{"name":"x"}`) diff --git a/internal/cli/email_provider.go b/internal/cli/email_provider.go index c579384d8..7569c1b9d 100644 --- a/internal/cli/email_provider.go +++ b/internal/cli/email_provider.go @@ -149,7 +149,8 @@ func createEmailProviderCmd(cli *cli) *cobra.Command { "To create interactively, use `auth0 email provider create` with no arguments.\n\n" + "To create non-interactively, supply the provider name and other information " + "through the flags.\n\n" + - "Use `--schema` to print the request payload schema and exit. A `--data` payload is sent as-is and validated by the API.", + "Use `--data` to supply the whole payload as JSON (inline, `@file.json`, or piped via stdin). It is sent as-is and validated by the API. Use `--schema` to print the request payload schema and exit.\n\n" + + "For a `custom` provider, `--data` must include `\"credentials\":{}`; the API rejects the request without it.", Example: ` auth0 email provider create auth0 email provider create --json auth0 email provider create --json-compact @@ -297,7 +298,8 @@ func updateEmailProviderCmd(cli *cli) *cobra.Command { "To update interactively, use `auth0 email provider update` with no arguments.\n\n" + "To update non-interactively, supply the provider name and other information " + "through the flags.\n\n" + - "Use `--schema` to print the request payload schema and exit. A `--data` payload is sent as-is and validated by the API.", + "Use `--data` to supply the whole payload as JSON (inline, `@file.json`, or piped via stdin)." + + " It is sent as-is and validated by the API. Use `--schema` to print the request payload schema and exit.", Example: ` auth0 email provider update auth0 email provider update --json auth0 email provider update --json-compact diff --git a/internal/cli/email_provider_json_test.go b/internal/cli/email_provider_json_test.go index 65881b2ac..5b1d6636b 100644 --- a/internal/cli/email_provider_json_test.go +++ b/internal/cli/email_provider_json_test.go @@ -1,92 +1,113 @@ package cli import ( - "bytes" - "context" - "encoding/json" - "io" + "errors" "net/http" "testing" - "github.com/auth0/go-auth0/management" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/auth0/auth0-cli/internal/auth0" - "github.com/auth0/auth0-cli/internal/display" ) -// emailProviderWriteClient captures the method and URI runJSONWrite sends, so a -// --data test can assert the command built the right resource path. -type emailProviderWriteClient struct { - rawHTTPClientStub - gotMethod string - gotURI string -} - -// Request records the method and URI, writing a minimal email-provider response -// into the payload pointer so runJSONWrite can decode it for rendering. -func (c *emailProviderWriteClient) Request(_ context.Context, method, uri string, payload interface{}, _ ...management.RequestOption) error { - c.gotMethod = method - c.gotURI = uri - if raw, ok := payload.(*json.RawMessage); ok { - *raw = json.RawMessage(`{"name":"sendgrid","enabled":true}`) - } - return nil -} - -func newEmailProviderWriteCLI(client *emailProviderWriteClient, stdout *bytes.Buffer) *cli { - return &cli{ - api: &auth0.API{HTTPClient: client}, - renderer: &display.Renderer{ - MessageWriter: io.Discard, - ResultWriter: stdout, - }, - } -} +const emailProviderResponse = `{"name":"sendgrid","enabled":true}` func TestEmailProviderCreateCmdData(t *testing.T) { - t.Run("sends the validated payload to POST /emails/provider", func(t *testing.T) { - client := &emailProviderWriteClient{} - cmd := createEmailProviderCmd(newEmailProviderWriteCLI(client, &bytes.Buffer{})) + t.Run("sends the payload to POST /emails/provider", func(t *testing.T) { + stub := &jsonWriteStub{response: emailProviderResponse} + cmd := createEmailProviderCmd(newJSONWriteCLI(stub)) // The leaf command runs without the root PreRun, so clear the required-flag // annotations here (stdin is non-TTY under test) to reach RunE with only --data. prepareInteractivity(cmd) cmd.SetArgs([]string{"--data", `{"name":"sendgrid","credentials":{"api_key":"key"},"enabled":true}`}) require.NoError(t, cmd.Execute()) - assert.Equal(t, http.MethodPost, client.gotMethod) - assert.Contains(t, client.gotURI, "/emails/provider") + assert.Equal(t, http.MethodPost, stub.gotMethod) + assert.Contains(t, stub.gotURI, "/emails/provider") }) - t.Run("leaves schema validation to the API", func(t *testing.T) { - client := &emailProviderWriteClient{} - cmd := createEmailProviderCmd(newEmailProviderWriteCLI(client, &bytes.Buffer{})) - prepareInteractivity(cmd) - cmd.SetArgs([]string{"--data", `{"name":"custom","enabled":true}`}) + for name, data := range map[string]string{ + "custom with an empty credentials object": `{"name":"custom","credentials":{},"enabled":true}`, + "custom without credentials (API decides)": `{"name":"custom","enabled":true}`, + } { + t.Run("sends "+name+" as-is", func(t *testing.T) { + stub := &jsonWriteStub{response: emailProviderResponse} + cmd := createEmailProviderCmd(newJSONWriteCLI(stub)) + prepareInteractivity(cmd) + cmd.SetArgs([]string{"--data", data}) - require.NoError(t, cmd.Execute()) - assert.Equal(t, http.MethodPost, client.gotMethod) - }) + require.NoError(t, cmd.Execute()) + assert.Equal(t, data, stub.gotBody) + }) + } - t.Run("rejects a payload that is not a JSON object", func(t *testing.T) { - client := &emailProviderWriteClient{} - cmd := createEmailProviderCmd(newEmailProviderWriteCLI(client, &bytes.Buffer{})) + t.Run("passes the API error through without a schema dump", func(t *testing.T) { + stub := &jsonWriteStub{err: errors.New("400 Bad Request: Payload validation error: 'Missing required property: credentials'")} + cmd := createEmailProviderCmd(newJSONWriteCLI(stub)) prepareInteractivity(cmd) - cmd.SetArgs([]string{"--data", `["custom"]`}) + cmd.SetArgs([]string{"--data", `{"name":"custom","enabled":true}`}) - assert.ErrorContains(t, cmd.Execute(), "must be a JSON object") - assert.Empty(t, client.gotMethod, "must not reach the API when the payload is not an object") + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "Missing required property: credentials") + assert.NotContains(t, err.Error(), "Expected Request Schema") }) } func TestEmailProviderUpdateCmdData(t *testing.T) { - client := &emailProviderWriteClient{} - cmd := updateEmailProviderCmd(newEmailProviderWriteCLI(client, &bytes.Buffer{})) + stub := &jsonWriteStub{response: emailProviderResponse} + cmd := updateEmailProviderCmd(newJSONWriteCLI(stub)) prepareInteractivity(cmd) cmd.SetArgs([]string{"--data", `{"credentials":{"api_key":"NewKey"}}`}) require.NoError(t, cmd.Execute()) - assert.Equal(t, http.MethodPatch, client.gotMethod) - assert.Contains(t, client.gotURI, "/emails/provider") + assert.Equal(t, http.MethodPatch, stub.gotMethod) + assert.Contains(t, stub.gotURI, "/emails/provider") + assert.Equal(t, `{"credentials":{"api_key":"NewKey"}}`, stub.gotBody) +} + +func TestEmailProviderDataRejectsBadPayload(t *testing.T) { + commands := map[string]func(*cli) *cobra.Command{ + "create": createEmailProviderCmd, + "update": updateEmailProviderCmd, + } + payloads := map[string]struct{ data, wantErr string }{ + "syntax error keeps the parse detail": {`{"name":"sendgrid",`, "unexpected end of JSON input"}, + "array is not an object": {`["custom"]`, "must be a JSON object"}, + "null is not an object": {`null`, "must be a JSON object"}, + "empty --data": {``, "no input provided"}, + } + + for cmdName, newCmd := range commands { + for name, tc := range payloads { + t.Run(cmdName+" rejects "+name, func(t *testing.T) { + stub := &jsonWriteStub{} + cmd := newCmd(newJSONWriteCLI(stub)) + prepareInteractivity(cmd) + cmd.SetArgs([]string{"--data", tc.data}) + + assert.ErrorContains(t, cmd.Execute(), tc.wantErr) + assert.Empty(t, stub.gotMethod, "must not reach the API on a bad payload") + }) + } + } +} + +func TestEmailProviderDataConflictsWithFlags(t *testing.T) { + commands := map[string]func(*cli) *cobra.Command{ + "create": createEmailProviderCmd, + "update": updateEmailProviderCmd, + } + + for name, newCmd := range commands { + t.Run(name, func(t *testing.T) { + stub := &jsonWriteStub{} + cmd := newCmd(newJSONWriteCLI(stub)) + prepareInteractivity(cmd) + cmd.SetArgs([]string{"--data", `{"name":"sendgrid","credentials":{}}`, "--provider", "sendgrid"}) + + assert.ErrorContains(t, cmd.Execute(), "none of the others can be") + assert.Empty(t, stub.gotMethod, "must not reach the API when flags conflict") + }) + } } diff --git a/internal/cli/phone_provider.go b/internal/cli/phone_provider.go index f82e4accd..e60c98982 100644 --- a/internal/cli/phone_provider.go +++ b/internal/cli/phone_provider.go @@ -131,7 +131,9 @@ func listBrandingPhoneProviderCmd(cli *cli) *cobra.Command { Use: "list", Aliases: []string{"ls"}, Short: "List your Phone providers", - Long: "List your existing Phone providers. Currently we can create a max of 1 phone Provider, If none are created, you can create one by running `auth0 phone provider create`.", + Long: "List your existing Phone providers. Currently we can create a max of 1 phone Provider, If none are created, you can create one by running `auth0 phone provider create`.\n\n" + + "Use '--schema' to see available query parameters.\n" + + "Use '--query' to filter results via a JSON object (any API-supported parameter works immediately).", Example: ` auth0 phone provider list auth0 phone provider ls auth0 phone provider ls --json @@ -191,7 +193,7 @@ func createBrandingPhoneProviderCmd(cli *cli) *cobra.Command { "To create interactively, use `auth0 phone provider create` with no arguments.\n\n" + "To create non-interactively, supply the provider name and other information " + "through the flags.\n\n" + - "Use `--schema` to print the request payload schema and exit.", + "Use `--data` to supply the whole payload as JSON (inline, `@file.json`, or piped via stdin). It is validated against the OpenAPI schema before it is sent. Use `--schema` to print the request payload schema and exit.", Example: ` auth0 phone provider create auth0 phone provider create --json auth0 phone provider create --json-compact @@ -324,7 +326,7 @@ func updateBrandingPhoneProviderCmd(cli *cli) *cobra.Command { "To update interactively, use `auth0 phone provider update` with no arguments.\n\n" + "To update non-interactively, supply the provider name and other information " + "through the flags.\n\n" + - "Use `--schema` to print the request payload schema and exit.", + "Use `--data` to supply the whole payload as JSON (inline, `@file.json`, or piped via stdin). It is validated against the OpenAPI schema before it is sent. Use `--schema` to print the request payload schema and exit.", Example: ` auth0 phone provider update auth0 phone provider update --json auth0 phone provider update --json-compact diff --git a/internal/cli/phone_provider_json_test.go b/internal/cli/phone_provider_json_test.go index a1ae645a2..904357ac6 100644 --- a/internal/cli/phone_provider_json_test.go +++ b/internal/cli/phone_provider_json_test.go @@ -1,14 +1,11 @@ package cli import ( - "bytes" - "context" - "encoding/json" "io" "net/http" "testing" - "github.com/auth0/go-auth0/management" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,34 +13,7 @@ import ( "github.com/auth0/auth0-cli/internal/display" ) -// phoneProviderWriteClient captures the method and URI runJSONWrite sends, so a -// --data test can assert the command built the right resource path. -type phoneProviderWriteClient struct { - rawHTTPClientStub - gotMethod string - gotURI string -} - -// Request records the method and URI, writing a minimal phone-provider response -// into the payload pointer so runJSONWrite can decode it for rendering. -func (c *phoneProviderWriteClient) Request(_ context.Context, method, uri string, payload interface{}, _ ...management.RequestOption) error { - c.gotMethod = method - c.gotURI = uri - if raw, ok := payload.(*json.RawMessage); ok { - *raw = json.RawMessage(`{"id":"pp_123","name":"twilio","disabled":false}`) - } - return nil -} - -func newPhoneProviderWriteCLI(client *phoneProviderWriteClient, stdout *bytes.Buffer) *cli { - return &cli{ - api: &auth0.API{HTTPClient: client}, - renderer: &display.Renderer{ - MessageWriter: io.Discard, - ResultWriter: stdout, - }, - } -} +const phoneProviderResponse = `{"id":"pp_123","name":"twilio","disabled":false}` func TestPhoneProviderListCmdInvalidQuery(t *testing.T) { c := &cli{ @@ -59,38 +29,70 @@ func TestPhoneProviderListCmdInvalidQuery(t *testing.T) { func TestPhoneProviderCreateCmdData(t *testing.T) { t.Run("sends the validated payload to POST /branding/phone/providers", func(t *testing.T) { - client := &phoneProviderWriteClient{} - cmd := createBrandingPhoneProviderCmd(newPhoneProviderWriteCLI(client, &bytes.Buffer{})) + stub := &jsonWriteStub{response: phoneProviderResponse} + cmd := createBrandingPhoneProviderCmd(newJSONWriteCLI(stub)) // The leaf command runs without the root PreRun, so clear the required-flag // annotations here (stdin is non-TTY under test) to reach RunE with only --data. prepareInteractivity(cmd) cmd.SetArgs([]string{"--data", `{"name":"twilio","credentials":{"auth_token":"tok"},"configuration":{"delivery_methods":["text"]}}`}) require.NoError(t, cmd.Execute()) - assert.Equal(t, http.MethodPost, client.gotMethod) - assert.Contains(t, client.gotURI, "/branding/phone/providers") + assert.Equal(t, http.MethodPost, stub.gotMethod) + assert.Contains(t, stub.gotURI, "/branding/phone/providers") }) t.Run("surfaces a schema validation failure before calling the API", func(t *testing.T) { - client := &phoneProviderWriteClient{} - cmd := createBrandingPhoneProviderCmd(newPhoneProviderWriteCLI(client, &bytes.Buffer{})) + stub := &jsonWriteStub{} + cmd := createBrandingPhoneProviderCmd(newJSONWriteCLI(stub)) prepareInteractivity(cmd) cmd.SetArgs([]string{"--data", `{"name":123}`}) err := cmd.Execute() require.Error(t, err) assert.Contains(t, err.Error(), "schema validation") - assert.Empty(t, client.gotMethod, "must not reach the API when validation fails") + assert.Empty(t, stub.gotMethod, "must not reach the API when validation fails") }) } func TestPhoneProviderUpdateCmdData(t *testing.T) { - client := &phoneProviderWriteClient{} - cmd := updateBrandingPhoneProviderCmd(newPhoneProviderWriteCLI(client, &bytes.Buffer{})) + stub := &jsonWriteStub{response: phoneProviderResponse} + cmd := updateBrandingPhoneProviderCmd(newJSONWriteCLI(stub)) prepareInteractivity(cmd) cmd.SetArgs([]string{"pp_123", "--data", `{"credentials":{"auth_token":"newTok"}}`}) require.NoError(t, cmd.Execute()) - assert.Equal(t, http.MethodPatch, client.gotMethod) - assert.Contains(t, client.gotURI, "/branding/phone/providers/pp_123") + assert.Equal(t, http.MethodPatch, stub.gotMethod) + assert.Contains(t, stub.gotURI, "/branding/phone/providers/pp_123") +} + +func TestPhoneProviderUpdateCmdDataRejectsInvalidSchema(t *testing.T) { + stub := &jsonWriteStub{} + cmd := updateBrandingPhoneProviderCmd(newJSONWriteCLI(stub)) + prepareInteractivity(cmd) + cmd.SetArgs([]string{"pp_123", "--data", `{"disabled":"yes"}`}) + + assert.ErrorContains(t, cmd.Execute(), "schema validation") + assert.Empty(t, stub.gotMethod, "must not reach the API when validation fails") +} + +func TestPhoneProviderDataConflictsWithFlags(t *testing.T) { + commands := map[string]struct { + newCmd func(*cli) *cobra.Command + args []string + }{ + "create": {createBrandingPhoneProviderCmd, []string{"--data", `{"name":"twilio"}`, "--provider", "twilio"}}, + "update": {updateBrandingPhoneProviderCmd, []string{"pp_123", "--data", `{"disabled":true}`, "--disabled"}}, + } + + for name, tc := range commands { + t.Run(name, func(t *testing.T) { + stub := &jsonWriteStub{} + cmd := tc.newCmd(newJSONWriteCLI(stub)) + prepareInteractivity(cmd) + cmd.SetArgs(tc.args) + + assert.ErrorContains(t, cmd.Execute(), "none of the others can be") + assert.Empty(t, stub.gotMethod, "must not reach the API when flags conflict") + }) + } } From 54a83b4aa7aaf5a297e1e2164121e1da5c2c6387 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Thu, 15 Oct 2026 21:23:34 +0530 Subject: [PATCH 5/5] feat: add jsonWriteStub for testing --data option in CLI commands --- internal/cli/json_write_stub_test.go | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 internal/cli/json_write_stub_test.go diff --git a/internal/cli/json_write_stub_test.go b/internal/cli/json_write_stub_test.go new file mode 100644 index 000000000..83da64265 --- /dev/null +++ b/internal/cli/json_write_stub_test.go @@ -0,0 +1,43 @@ +package cli + +import ( + "context" + "encoding/json" + "io" + + "github.com/auth0/go-auth0/management" + + "github.com/auth0/auth0-cli/internal/auth0" + "github.com/auth0/auth0-cli/internal/display" +) + +// jsonWriteStub records the request runJSONWrite sends and replies with response, +// or with err when set, so a --data test can assert the request and the outcome. +type jsonWriteStub struct { + rawHTTPClientStub + response string + err error + + gotMethod string + gotURI string + gotBody string +} + +func (s *jsonWriteStub) Request(_ context.Context, method, uri string, payload interface{}, _ ...management.RequestOption) error { + s.gotMethod = method + s.gotURI = uri + if raw, ok := payload.(*json.RawMessage); ok { + s.gotBody = string(*raw) + if s.response != "" { + *raw = json.RawMessage(s.response) + } + } + return s.err +} + +func newJSONWriteCLI(stub *jsonWriteStub) *cli { + return &cli{ + api: &auth0.API{HTTPClient: stub}, + renderer: &display.Renderer{MessageWriter: io.Discard, ResultWriter: io.Discard}, + } +}