diff --git a/cmd/events.go b/cmd/events.go index f6188dda..19924ab5 100644 --- a/cmd/events.go +++ b/cmd/events.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" "github.com/twitchdev/twitch-cli/internal/events" + configure_event "github.com/twitchdev/twitch-cli/internal/events/configure" "github.com/twitchdev/twitch-cli/internal/events/trigger" "github.com/twitchdev/twitch-cli/internal/events/types" "github.com/twitchdev/twitch-cli/internal/events/verify" @@ -25,6 +26,7 @@ var ( forwardAddress string event string transport string + noConfig bool fromUser string toUser string giftUser string @@ -127,16 +129,25 @@ var startWebsocketServerCmd = &cobra.Command{ Deprecated: `use "twitch event websocket start-server" instead.`, } +var configureEventCmd = &cobra.Command{ + Use: "configure", + Short: "Allows users to configure defaults for the twitch event subcommands.", + RunE: configureEventRun, + Example: `twitch event configure`, +} + func init() { rootCmd.AddCommand(eventCmd) - eventCmd.AddCommand(triggerCmd, retriggerCmd, verifyCmd, websocketCmd, startWebsocketServerCmd) + eventCmd.AddCommand(triggerCmd, retriggerCmd, verifyCmd, websocketCmd, startWebsocketServerCmd, configureEventCmd) + eventCmd.Flags().BoolVarP(&noConfig, "no-config", "D", false, "Disables the use of the configuration, if it exists.") // trigger flags //// flags for forwarding functionality/changing payloads triggerCmd.Flags().StringVarP(&forwardAddress, "forward-address", "F", "", "Forward address for mock event (webhook only).") triggerCmd.Flags().StringVarP(&transport, "transport", "T", "webhook", fmt.Sprintf("Preferred transport method for event. Defaults to /EventSub.\nSupported values: %s", events.ValidTransports())) triggerCmd.Flags().StringVarP(&secret, "secret", "s", "", "Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length.") + triggerCmd.Flags().BoolVarP(&noConfig, "no-config", "D", false, "Disables the use of the configuration, if it exists.") // trigger flags //// per-topic flags @@ -167,6 +178,7 @@ func init() { retriggerCmd.Flags().StringVarP(&forwardAddress, "forward-address", "F", "", "Forward address for mock event (webhook only).") retriggerCmd.Flags().StringVarP(&eventID, "id", "i", "", "ID of the event to be refired.") retriggerCmd.Flags().StringVarP(&secret, "secret", "s", "", "Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length.") + retriggerCmd.Flags().BoolVarP(&noConfig, "no-config", "D", false, "Disables the use of the configuration, if it exists.") retriggerCmd.MarkFlagRequired("id") // verify-subscription flags @@ -176,8 +188,8 @@ func init() { verifyCmd.Flags().StringVar(×tamp, "timestamp", "", "Sets the timestamp to be used in payloads and headers. Must be in RFC3339Nano format.") verifyCmd.Flags().StringVarP(&eventID, "subscription-id", "u", "", "Manually set the subscription/event ID of the event itself.") // TODO: This description will need to change with https://github.com/twitchdev/twitch-cli/issues/184 verifyCmd.Flags().StringVarP(&version, "version", "v", "", "Chooses the EventSub version used for a specific event. Not required for most events.") + verifyCmd.Flags().BoolVarP(&noConfig, "no-config", "D", false, "Disables the use of the configuration, if it exists.") verifyCmd.Flags().StringVarP(&toUser, "broadcaster", "b", "", "User ID of the broadcaster for the verification event.") - verifyCmd.MarkFlagRequired("forward-address") // websocket flags /// flags for start-server @@ -193,6 +205,10 @@ func init() { websocketCmd.Flags().StringVar(&wsSubscription, "subscription", "", `Subscription to target with your server command. Used with "websocket subscription".`) websocketCmd.Flags().StringVar(&wsStatus, "status", "", `Changes the status of an existing subscription. Used with "websocket subscription".`) websocketCmd.Flags().StringVar(&wsReason, "reason", "", `Sets the close reason when sending a Close message to the client. Used with "websocket close".`) + + // configure flags + configureEventCmd.Flags().StringVarP(&forwardAddress, "forward-address", "F", "", "Forward address for mock event (webhook only).") + configureEventCmd.Flags().StringVarP(&secret, "secret", "s", "", "Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length.") } func triggerCmdRun(cmd *cobra.Command, args []string) error { @@ -205,8 +221,14 @@ func triggerCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf(websubDeprecationNotice) } - if secret != "" && (len(secret) < 10 || len(secret) > 100) { - return fmt.Errorf("Invalid secret provided. Secrets must be between 10-100 characters") + defaults := configure_event.GetEventConfiguration(noConfig) + + if secret != "" { + if len(secret) < 10 || len(secret) > 100 { + return fmt.Errorf("Invalid secret provided. Secrets must be between 10-100 characters") + } + } else { + secret = defaults.Secret } // Validate that the forward address is actually a URL @@ -215,6 +237,8 @@ func triggerCmdRun(cmd *cobra.Command, args []string) error { if err != nil { return err } + } else { + forwardAddress = defaults.ForwardAddress } for i := 0; i < count; i++ { @@ -261,8 +285,21 @@ func retriggerCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf(websubDeprecationNotice) } - if secret != "" && (len(secret) < 10 || len(secret) > 100) { - return fmt.Errorf("Invalid secret provided. Secrets must be between 10-100 characters") + defaults := configure_event.GetEventConfiguration(noConfig) + + if secret != "" { + if len(secret) < 10 || len(secret) > 100 { + return fmt.Errorf("Invalid secret provided. Secrets must be between 10-100 characters") + } + } else { + secret = defaults.Secret + } + + if forwardAddress == "" { + if defaults.ForwardAddress == "" { + return fmt.Errorf("if a default configuration is not set, forward-address must be provided") + } + forwardAddress = defaults.ForwardAddress } res, err := trigger.RefireEvent(eventID, trigger.TriggerParameters{ @@ -288,8 +325,14 @@ func verifyCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf(websubDeprecationNotice) } - if secret != "" && (len(secret) < 10 || len(secret) > 100) { - return fmt.Errorf("Invalid secret provided. Secrets must be between 10-100 characters") + defaults := configure_event.GetEventConfiguration(noConfig) + + if secret != "" { + if len(secret) < 10 || len(secret) > 100 { + return fmt.Errorf("Invalid secret provided. Secrets must be between 10-100 characters") + } + } else { + secret = defaults.Secret } // Validate that the forward address is actually a URL @@ -298,6 +341,8 @@ func verifyCmdRun(cmd *cobra.Command, args []string) error { if err != nil { return err } + } else { + forwardAddress = defaults.ForwardAddress } if timestamp == "" { @@ -355,3 +400,10 @@ func websocketCmdRun(cmd *cobra.Command, args []string) error { return nil } + +func configureEventRun(cmd *cobra.Command, args []string) error { + return configure_event.ConfigureEvents(configure_event.EventConfigurationParams{ + ForwardAddress: forwardAddress, + Secret: secret, + }) +} diff --git a/docs/event.md b/docs/event.md index cd3eade7..c8be09a7 100644 --- a/docs/event.md +++ b/docs/event.md @@ -2,6 +2,7 @@ - [Events](#events) - [Description](#description) + - [Configure](#configure) - [Trigger](#trigger) - [Retrigger](#retrigger) - [Verify-Subscription](#verify-subscription) @@ -9,10 +10,23 @@ ## Description -The `event` product contains commands to trigger mock events for local webhook testing or migration. +The `event` command contains subcommands to trigger mock events for local webhook testing or migration. All commands exit the program with a non-zero exit code when the command fails, including when an event does not exist, or when the mock EventSub WebSocket server does not start correctly. + +## Configure + +Used to configure the forwarding address and/or the secret used with the `trigger`, `verify-subscription`, and `retrigger` subcommands. + +**Flags** + +| Flag | Shorthand | Description | Example | Required? (Y/N) | +|---------------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|-----------------| +| `--forward-address` | `-F` | Web server address for where to send mock events. | `-F https://localhost:8080` | N | +| `--secret` | `-s` | Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length. | `-s testsecret` | N | + + ## Trigger Used to either create or send mock events for use with local webhooks testing. @@ -92,6 +106,7 @@ This command can take either the Event or Alias listed as an argument. It is pre | `--gift-user` | `-g` | Used only for subcription-based events, denotes the gifting user ID. | `-g 44635596` | N | | `--item-id` | `-i` | Manually set the ID of the event payload item (for example the reward ID in redemption events or game in stream events). | `-i 032e4a6c-4aef-11eb-a9f5-1f703d1f0b92` | N | | `--item-name` | `-n` | Manually set the name of the event payload item (for example the reward ID in redemption events or game name in stream events). | `-n "Science & Technology"` | N | +| `--no-config` | `-D` | Disables the use of the configuration values should they exist. | `-D` | N | | `--secret` | `-s` | Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length. | `-s testsecret` | N | | `--session` | | WebSocket session to target. Only used when forwarding to WebSocket servers with --transport=websocket | `--session e411cc1e_a2613d4e` | N | | `--subscription-id` | `-u` | Manually set the subscription/event ID of the event itself. | `-u 5d3aed06-d019-11ed-afa1-0242ac120002` | N | @@ -134,8 +149,10 @@ None |---------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|-----------------| | `--forward-address` | `-F` | Web server address for where to send mock events. | `-F https://localhost:8080` | N | | `--id` | `-i` | The ID of the event to refire. | `-i ` | Y | +| `--no-config` | `-D` | Disables the use of the configuration values should they exist. | `-D` | N | | `--secret` | `-s` | Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length. | `-s testsecret` | N | + **Examples** ```sh @@ -144,7 +161,7 @@ twitch event retrigger -i "713f3254-0178-9757-7439-d779400c0999" -F https://loca ## Verify-Subscription -Allows you to test if your webserver responds to subscription requests properly. +Allows you to test if your webserver responds to subscription requests properly. The `forward-address` flag is required *unless* you have configured a default forwarding address via `twitch event configure -F
`. **Args** @@ -156,6 +173,7 @@ This command takes the same arguments as [Trigger](#trigger). |---------------------|-----------|----------------------------------------------------------------------------------------------------------------------|-----------------------------|-----------------| | `--broadcaster` | `-b` | The broadcaster's user ID to be used for verification | `-b 1234` | N | | `--forward-address` | `-F` | Web server address for where to send mock subscription. | `-F https://localhost:8080` | Y | +| `--no-config` | `-D` | Disables the use of the configuration values should they exist. | `-D` | N | | `--secret` | `-s` | Webhook secret. If defined, signs all forwarded events with the SHA256 HMAC and must be 10-100 characters in length. | `-s testsecret` | N | | `--transport` | `-T` | The method used to send events. Default is `eventsub`. | `-T eventsub` | N | diff --git a/internal/events/configure/configure.go b/internal/events/configure/configure.go new file mode 100644 index 00000000..dc5d8f8e --- /dev/null +++ b/internal/events/configure/configure.go @@ -0,0 +1,58 @@ +package configure_event + +import ( + "fmt" + "net/url" + + "github.com/spf13/viper" + "github.com/twitchdev/twitch-cli/internal/util" +) + +type EventConfigurationParams struct { + Secret string + ForwardAddress string +} + +func ConfigureEvents(p EventConfigurationParams) error { + var err error + if p.ForwardAddress == "" && p.Secret == "" { + return fmt.Errorf("you must provide at least one of --secret or --forward-address") + } + + // Validate that the forward address is actually a URL + if len(p.ForwardAddress) > 0 { + _, err := url.ParseRequestURI(p.ForwardAddress) + if err != nil { + return err + } + viper.Set("forwardAddress", p.ForwardAddress) + } + if p.Secret != "" { + if len(p.Secret) < 10 || len(p.Secret) > 100 { + return fmt.Errorf("invalid secret provided. Secrets must be between 10-100 characters") + } + viper.Set("eventSecret", p.Secret) + } + + configPath, err := util.GetConfigPath() + if err != nil { + return err + } + + if err := viper.WriteConfigAs(configPath); err != nil { + return fmt.Errorf("failed to write configuration: %v", err.Error()) + } + + fmt.Println("Updated configuration.") + return nil +} + +func GetEventConfiguration(noConfig bool) EventConfigurationParams { + if noConfig { + return EventConfigurationParams{} + } + return EventConfigurationParams{ + ForwardAddress: viper.GetString("forwardAddress"), + Secret: viper.GetString("eventSecret"), + } +} diff --git a/internal/events/configure/configure_test.go b/internal/events/configure/configure_test.go new file mode 100644 index 00000000..66759097 --- /dev/null +++ b/internal/events/configure/configure_test.go @@ -0,0 +1,36 @@ +package configure_event_test + +import ( + "testing" + + "github.com/spf13/viper" + configure_event "github.com/twitchdev/twitch-cli/internal/events/configure" + "github.com/twitchdev/twitch-cli/test_setup" +) + +func TestWriteEventConfig(t *testing.T) { + a := test_setup.SetupTestEnv(t) + defaultForwardAddress := "http://localhost:3000/" + defaultSecret := "12345678910" + test_config := configure_event.EventConfigurationParams{ + ForwardAddress: defaultForwardAddress, + Secret: defaultSecret, + } + + // test a good config writes correctly + a.NoError(configure_event.ConfigureEvents(test_config)) + + a.Equal(defaultForwardAddress, viper.Get("forwardAddress")) + a.Equal(defaultSecret, viper.Get("eventSecret")) + + // test for secret length validation + test_config.Secret = "1" + a.Error(configure_event.ConfigureEvents(test_config)) + a.NotEqual("1", viper.Get("eventSecret")) + test_config.Secret = defaultSecret + + // test for forward address validation + test_config.ForwardAddress = "not a url" + a.Error(configure_event.ConfigureEvents(test_config)) + a.NotEqual("not a url", viper.Get("forwardAddress")) +} diff --git a/internal/util/path.go b/internal/util/path.go index 0ed804e7..17b51cb9 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -58,5 +58,10 @@ func GetConfigPath() (string, error) { configPath := filepath.Join(home, ".twitch-cli.env") + // purely for testing purposes- this allows us to run tests without overwriting the user's config + if os.Getenv("GOLANG_TESTING") == "true" { + configPath = filepath.Join(home, ".twitch-cli-test.env") + } + return configPath, nil } diff --git a/test_setup/test_setup.go b/test_setup/test_setup.go index 74a06abf..65c98ba5 100644 --- a/test_setup/test_setup.go +++ b/test_setup/test_setup.go @@ -21,5 +21,6 @@ func SetupTestEnv(t *testing.T) *assert.Assertions { viper.SetConfigType("env") viper.Set("DB_FILENAME", "test-eventCache.db") + t.Setenv("GOLANG_TESTING", "true") return assert }