From 3a661f80002b315e0433193d36aad21bb7eeb3a1 Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:15:11 +0900 Subject: [PATCH 01/15] =?UTF-8?q?:boom:=20cobra=E3=82=92=E4=BD=BF=E3=81=86?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/root.go | 178 ++++++++++++++++++++++++++++++ cmd/root_test.go | 25 +++++ cmd/util.go | 11 ++ go.mod | 25 +++++ go.sum | 63 +++++++++++ internal/client/webhook/client.go | 16 +-- internal/cmd/cmd.go | 13 --- internal/cmd/error.go | 7 -- internal/cmd/message.go | 98 ---------------- internal/conf/client.go | 6 - internal/conf/conf/client.go | 85 -------------- internal/conf/error.go | 8 -- main.go | 94 +--------------- 13 files changed, 309 insertions(+), 320 deletions(-) create mode 100644 cmd/root.go create mode 100644 cmd/root_test.go create mode 100644 cmd/util.go delete mode 100644 internal/cmd/cmd.go delete mode 100644 internal/cmd/error.go delete mode 100644 internal/cmd/message.go delete mode 100644 internal/conf/client.go delete mode 100644 internal/conf/conf/client.go delete mode 100644 internal/conf/error.go diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..b53e9de --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,178 @@ +/* +Copyright © 2024 ikura-hamu 104292023+ikura-hamu@users.noreply.github.com +*/ +package cmd + +import ( + "bufio" + "fmt" + "os" + "runtime/debug" + "strings" + + "github.com/ikura-hamu/q-cli/internal/client" + "github.com/ikura-hamu/q-cli/internal/client/webhook" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var cfgFile string + +var cl client.Client + +func SetClient(c client.Client) { + cl = c +} + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "q-cli", + Short: "traQ Webhook CLI", + Long: `"q-cli" is a CLI tool for sending messages to traQ via webhook. +It reads the configuration file and sends the message to the specified webhook.`, + + // Uncomment the following line if your bare application + // has an action associated with it: + Run: func(cmd *cobra.Command, args []string) { + if printVersion { + printVersionInfo() + return + } + + conf := webhookConfig{ + host: viper.GetString(configKeyWebhookHost), + id: viper.GetString(configKeyWebhookID), + secret: viper.GetString(configKeyWebhookSecret), + } + + if conf.host == "" || conf.id == "" || conf.secret == "" { + returnWithError("some webhook configuration field(s) is empty\n") + return + } + var message string + + if len(args) > 0 { + message = strings.Join(args, " ") + } else { + sc := bufio.NewScanner(os.Stdin) + sb := &strings.Builder{} + for sc.Scan() { + text := sc.Text() + sb.WriteString(text + "\n") + } + message = sb.String() + } + + if withCodeBlock { + message = fmt.Sprintf("```%s\n%s\n```", codeBlockLang, message) + } + + if cl != nil { + + err := cl.SendMessage(message) + // err := makeWebhookRequest(conf, message) + if err != nil { + returnWithError("failed to send message: %v\n", err) + } + } else { + panic("client is nil") + } + + return + }, +} + +var ( + printVersion bool + withCodeBlock bool + codeBlockLang string + version string +) + +const ( + configKeyWebhookHost = "webhook_host" + configKeyWebhookID = "webhook_id" + configKeyWebhookSecret = "webhook_secret" +) + +type webhookConfig struct { + host string + id string + secret string +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.q-cli.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolVarP(&printVersion, "version", "v", false, "Print version information and quit") + + rootCmd.Flags().BoolVarP(&withCodeBlock, "code", "c", false, "Send message with code block") + rootCmd.Flags().StringVarP(&codeBlockLang, "lang", "l", "text", "Code block language") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // Search config in home directory with name ".q-cli" (without extension). + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".q-cli") + } + + viper.AutomaticEnv() // read in environment variables that match + + viper.SetEnvPrefix("q") + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } + + cl, err := webhook.NewWebhookClient(viper.GetString(configKeyWebhookID), viper.GetString(configKeyWebhookHost), viper.GetString(configKeyWebhookSecret)) + if err != nil { + returnWithError("failed to create webhook client: %v\n", err) + } + SetClient(cl) +} + +func printVersionInfo() { + v := "" + if version != "" { + v = version + } else { + i, ok := debug.ReadBuildInfo() + if !ok { + v = "unknown" + } else { + v = i.Main.Version + if v == "" { + v = "unknown" + } + } + } + fmt.Printf("q version %s\n", v) +} diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..97e6128 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "testing" + + "github.com/spf13/viper" +) + +func TestRoot(t *testing.T) { + t.Run("rootCmd", func(t *testing.T) { + t.Log("host", viper.GetString("webhook_host")) + t.Log("version", printVersion) + + rootCmd.DebugFlags() + // rootCmd.Flag("version").Value.Set("true") + rootCmd.DebugFlags() + viper.Set("webhook_host", "http://localhost:8080") + + t.Log("version", printVersion) + t.Log(cl) + + rootCmd.Run(rootCmd, []string{"test"}) + + }) +} diff --git a/cmd/util.go b/cmd/util.go new file mode 100644 index 0000000..0608d8c --- /dev/null +++ b/cmd/util.go @@ -0,0 +1,11 @@ +package cmd + +import ( + "fmt" + "os" +) + +func returnWithError(message string, args ...any) { + fmt.Fprintf(os.Stderr, message, args...) + os.Exit(1) +} diff --git a/go.mod b/go.mod index 7ee3096..e2da4ee 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,28 @@ module github.com/ikura-hamu/q-cli go 1.22.0 + +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.19.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index e69de29..967063c 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,63 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/client/webhook/client.go b/internal/client/webhook/client.go index 0f6ee7b..aea2ae9 100644 --- a/internal/client/webhook/client.go +++ b/internal/client/webhook/client.go @@ -9,8 +9,6 @@ import ( "net/http" "net/url" "strings" - - "github.com/ikura-hamu/q-cli/internal/conf" ) type WebhookClient struct { @@ -19,22 +17,12 @@ type WebhookClient struct { webhookURL string } -func NewWebhookClient(conf conf.ClientConfig) (*WebhookClient, error) { - secret, err := conf.GetWebhookSecret() - if err != nil { - return nil, fmt.Errorf("failed to get webhook secret: %w", err) - } - +func NewWebhookClient(webhookID string, hostName string, secret string) (*WebhookClient, error) { mac := hmac.New(sha1.New, []byte(secret)) client := http.DefaultClient - webhookID, err := conf.GetWebhookID() - if err != nil { - return nil, fmt.Errorf("failed to get webhook ID: %w", err) - } - - webhookURL, err := url.JoinPath("https://q.trap.jp/api/v3/webhooks", webhookID) + webhookURL, err := url.JoinPath(hostName, "/api/v3/webhooks", webhookID) if err != nil { panic(err) } diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go deleted file mode 100644 index e3ac455..0000000 --- a/internal/cmd/cmd.go +++ /dev/null @@ -1,13 +0,0 @@ -package cmd - -import "github.com/ikura-hamu/q-cli/internal/client" - -type Command struct { - client client.Client -} - -func NewCommand(client client.Client) *Command { - return &Command{ - client: client, - } -} diff --git a/internal/cmd/error.go b/internal/cmd/error.go deleted file mode 100644 index 0809934..0000000 --- a/internal/cmd/error.go +++ /dev/null @@ -1,7 +0,0 @@ -package cmd - -import "errors" - -var ( - ErrEmptyMessage = errors.New("empty message") -) diff --git a/internal/cmd/message.go b/internal/cmd/message.go deleted file mode 100644 index 5cc12ab..0000000 --- a/internal/cmd/message.go +++ /dev/null @@ -1,98 +0,0 @@ -package cmd - -import ( - "bufio" - "errors" - "fmt" - "os" - "strings" -) - -type MessageOptionFunc func(string) string - -func WithCodeBlock(lang string) MessageOptionFunc { - return func(message string) string { - return fmt.Sprintf("```%s\n%s\n```", lang, message) - } -} - -func (cm *Command) SendMessage(message string, options ...MessageOptionFunc) error { - if message == "" { - return ErrEmptyMessage - } - - for _, option := range options { - message = option(message) - } - - err := cm.client.SendMessage(message) - if errors.Is(err, ErrEmptyMessage) { - return ErrEmptyMessage - } - if err != nil { - return err - } - - return nil -} - -func (cm *Command) SendWithStdin(options ...MessageOptionFunc) error { - message, err := readFromStdin("") - if err != nil { - return fmt.Errorf("failed to read from stdin: %w", err) - } - - err = cm.SendMessage(message) - if err != nil { - return fmt.Errorf("failed to send message: %w", err) - } - - return nil -} - -func (cm *Command) SendWithInteractiveMode(options ...MessageOptionFunc) error { - count := 1 - for { - message, err := readFromStdin(fmt.Sprintf("q [%d]: ", count)) - if err != nil { - return fmt.Errorf("failed to read from stdin: %w", err) - } - - if message == "exit" || message == "" { - fmt.Println() - break - } - - for _, option := range options { - message = option(message) - } - - err = cm.SendMessage(message) - if err != nil { - return fmt.Errorf("failed to send message: %w", err) - } - - fmt.Println() - - count++ - } - - return nil -} - -func readFromStdin(prompt string) (string, error) { - var err error - sb := &strings.Builder{} - sc := bufio.NewScanner(os.Stdin) - fmt.Print(prompt) - for sc.Scan() { - fmt.Print(prompt) - line := sc.Text() - _, err = sb.WriteString(line + "\n") - if err != nil { - return "", fmt.Errorf("failed to write string input: %w", err) - } - } - - return sb.String(), nil -} diff --git a/internal/conf/client.go b/internal/conf/client.go deleted file mode 100644 index a99f6bc..0000000 --- a/internal/conf/client.go +++ /dev/null @@ -1,6 +0,0 @@ -package conf - -type ClientConfig interface { - GetWebhookID() (string, error) - GetWebhookSecret() (string, error) -} diff --git a/internal/conf/conf/client.go b/internal/conf/conf/client.go deleted file mode 100644 index 007c640..0000000 --- a/internal/conf/conf/client.go +++ /dev/null @@ -1,85 +0,0 @@ -package conf - -import ( - "encoding/json" - "os" - "path" - - "github.com/ikura-hamu/q-cli/internal/conf" -) - -type ClientConfig struct { - configData *configData -} - -func NewClientConfig() *ClientConfig { - return &ClientConfig{ - configData: readFromFile(), - } -} - -type configData struct { - WebhookID string `json:"webhook_id"` - WebhookSecret string `json:"webhook_secret"` -} - -func readFromFile() *configData { - userConfDir, err := os.UserConfigDir() - if err != nil { - panic(err) - } - - qConfDir := path.Join(userConfDir, "q-cli") - if _, err := os.Stat(qConfDir); os.IsNotExist(err) { - return &configData{} - } else if err != nil { - panic(err) - } - - qConfFileName := path.Join(qConfDir, "config.json") - if _, err := os.Stat(qConfFileName); os.IsNotExist(err) { - return &configData{} - } else if err != nil { - panic(err) - } - - file, err := os.Open(qConfFileName) - if err != nil { - panic(err) - } - defer file.Close() - - var configData configData - err = json.NewDecoder(file).Decode(&configData) - if err != nil { - panic(err) - } - - return &configData -} - -func (c *ClientConfig) GetWebhookID() (string, error) { - if c.configData.WebhookID != "" { - return c.configData.WebhookID, nil - } - - webhookID, ok := os.LookupEnv("Q_WEBHOOK_ID") - if !ok { - return "", conf.ErrWebhookIDNotSet - } - - return webhookID, nil -} - -func (c *ClientConfig) GetWebhookSecret() (string, error) { - if c.configData.WebhookSecret != "" { - return c.configData.WebhookSecret, nil - } - - webhookSecret, ok := os.LookupEnv("Q_WEBHOOK_SECRET") - if !ok { - return "", conf.ErrWebhookSecretNotSet - } - - return webhookSecret, nil -} diff --git a/internal/conf/error.go b/internal/conf/error.go deleted file mode 100644 index 3f4e8ca..0000000 --- a/internal/conf/error.go +++ /dev/null @@ -1,8 +0,0 @@ -package conf - -import "errors" - -var ( - ErrWebhookIDNotSet = errors.New("webhook ID not set") - ErrWebhookSecretNotSet = errors.New("webhook secret not set") -) diff --git a/main.go b/main.go index af4ec25..68852be 100644 --- a/main.go +++ b/main.go @@ -1,96 +1,12 @@ +/* +Copyright © 2024 NAME HERE +*/ package main import ( - "flag" - "fmt" - "log" - "os" - "runtime/debug" - "strings" - - "github.com/ikura-hamu/q-cli/internal/client/webhook" - "github.com/ikura-hamu/q-cli/internal/cmd" - "github.com/ikura-hamu/q-cli/internal/conf/conf" + "github.com/ikura-hamu/q-cli/cmd" ) -var version = "" - -var option struct { - Interactive bool - Version bool - Help bool - Stdin bool - CodeBlock string -} - func main() { - flag.Usage = func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0]) - fmt.Fprintln(flag.CommandLine.Output(), "q [option] [message]") - flag.PrintDefaults() - } - - flag.BoolVar(&option.Interactive, "i", false, "Interactive mode") - flag.BoolVar(&option.Version, "v", false, "Print version") - flag.BoolVar(&option.Help, "h", false, "Print this message") - flag.BoolVar(&option.Stdin, "s", false, "Accept input from stdin") - flag.StringVar(&option.CodeBlock, "c", "", "Send message as code block. Specify the language name (e.g. go, python, shell)") - - flag.Parse() - - if option.Version { - printVersion() - return - } - if option.Help { - flag.Usage() - return - } - - conf := conf.NewClientConfig() - client, err := webhook.NewWebhookClient(conf) - if err != nil { - log.Fatalf("Failed to create webhook client: %v\n", err) - } - command := cmd.NewCommand(client) - - messageOpts := make([]cmd.MessageOptionFunc, 0, 1) - if option.CodeBlock != "" { - messageOpts = append(messageOpts, cmd.WithCodeBlock(option.CodeBlock)) - } - - if option.Interactive { - err = command.SendWithInteractiveMode(messageOpts...) - if err != nil { - log.Fatalf("Failed to send message: %v\n", err) - } - return - } - if option.Stdin { - err = command.SendWithStdin(messageOpts...) - if err != nil { - log.Fatalf("Failed to send message: %v\n", err) - } - return - } - - err = command.SendMessage(strings.Join(flag.Args(), " "), messageOpts...) - if err != nil { - log.Fatalf("Failed to send message: %v\n", err) - } -} - -func printVersion() { - v := "" - if version != "" { - v = version - } else { - i, ok := debug.ReadBuildInfo() - if !ok { - v = "unknown" - } else { - v = i.Main.Version - } - } - fmt.Printf("q version %s\n", v) + cmd.Execute() } From b5e02a74131b529e96fc85ce83ed2af2ef3cba42 Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Sun, 28 Jul 2024 00:50:14 +0900 Subject: [PATCH 02/15] =?UTF-8?q?:adhesive=5Fbandage:=20=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E7=94=A8=E3=81=AB=E3=81=84=E3=82=8D=E3=81=84=E3=82=8D?= =?UTF-8?q?=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/root.go | 42 ++++++++++++++++++++++++++---------------- cmd/util.go | 5 ++--- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index b53e9de..f4eb97d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,7 @@ package cmd import ( "bufio" + "errors" "fmt" "os" "runtime/debug" @@ -16,9 +17,15 @@ import ( "github.com/spf13/viper" ) -var cfgFile string +var ( + cfgFile string + + cl client.Client +) -var cl client.Client +var ( + ErrEmptyConfiguration = errors.New("some webhook configuration field(s) is empty") +) func SetClient(c client.Client) { cl = c @@ -30,13 +37,21 @@ var rootCmd = &cobra.Command{ Short: "traQ Webhook CLI", Long: `"q-cli" is a CLI tool for sending messages to traQ via webhook. It reads the configuration file and sends the message to the specified webhook.`, + PreRunE: func(cmd *cobra.Command, args []string) error { + cl, err := webhook.NewWebhookClient(viper.GetString(configKeyWebhookID), viper.GetString(configKeyWebhookHost), viper.GetString(configKeyWebhookSecret)) + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + SetClient(cl) + return nil + }, // Uncomment the following line if your bare application - // has an action associated with it: - Run: func(cmd *cobra.Command, args []string) { + // has an action associated with it + RunE: func(cmd *cobra.Command, args []string) error { if printVersion { printVersionInfo() - return + return nil } conf := webhookConfig{ @@ -46,8 +61,7 @@ It reads the configuration file and sends the message to the specified webhook.` } if conf.host == "" || conf.id == "" || conf.secret == "" { - returnWithError("some webhook configuration field(s) is empty\n") - return + return ErrEmptyConfiguration } var message string @@ -60,7 +74,7 @@ It reads the configuration file and sends the message to the specified webhook.` text := sc.Text() sb.WriteString(text + "\n") } - message = sb.String() + message = strings.TrimSpace(sb.String()) } if withCodeBlock { @@ -72,13 +86,13 @@ It reads the configuration file and sends the message to the specified webhook.` err := cl.SendMessage(message) // err := makeWebhookRequest(conf, message) if err != nil { - returnWithError("failed to send message: %v\n", err) + return fmt.Errorf("failed to send message: %w", err) } } else { panic("client is nil") } - return + return nil }, } @@ -106,6 +120,7 @@ type webhookConfig struct { func Execute() { err := rootCmd.Execute() if err != nil { + printError("%v", err) os.Exit(1) } } @@ -124,7 +139,7 @@ func init() { rootCmd.Flags().BoolVarP(&printVersion, "version", "v", false, "Print version information and quit") rootCmd.Flags().BoolVarP(&withCodeBlock, "code", "c", false, "Send message with code block") - rootCmd.Flags().StringVarP(&codeBlockLang, "lang", "l", "text", "Code block language") + rootCmd.Flags().StringVarP(&codeBlockLang, "lang", "l", "", "Code block language") } // initConfig reads in config file and ENV variables if set. @@ -152,11 +167,6 @@ func initConfig() { fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) } - cl, err := webhook.NewWebhookClient(viper.GetString(configKeyWebhookID), viper.GetString(configKeyWebhookHost), viper.GetString(configKeyWebhookSecret)) - if err != nil { - returnWithError("failed to create webhook client: %v\n", err) - } - SetClient(cl) } func printVersionInfo() { diff --git a/cmd/util.go b/cmd/util.go index 0608d8c..18bb377 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -5,7 +5,6 @@ import ( "os" ) -func returnWithError(message string, args ...any) { - fmt.Fprintf(os.Stderr, message, args...) - os.Exit(1) +func printError(message string, args ...any) { + fmt.Fprintf(os.Stderr, "q-cli error: "+message, args...) } From 8daac230a73d70d630189cdd02e7116aa419b0fc Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Sun, 28 Jul 2024 00:50:30 +0900 Subject: [PATCH 03/15] =?UTF-8?q?root=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + cmd/root_test.go | 124 ++++++++++++++++++++++++++++++++++---- go.mod | 13 +++- go.sum | 28 ++++++++- internal/client/client.go | 2 + tools.go | 7 +++ 6 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 tools.go diff --git a/.gitignore b/.gitignore index e6b6cec..1354f05 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ q .env dist/ +**/mock diff --git a/cmd/root_test.go b/cmd/root_test.go index 97e6128..223893a 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1,25 +1,127 @@ package cmd import ( + "bytes" + "fmt" + "os" "testing" + "github.com/ikura-hamu/q-cli/internal/client/mock" "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRoot(t *testing.T) { - t.Run("rootCmd", func(t *testing.T) { - t.Log("host", viper.GetString("webhook_host")) - t.Log("version", printVersion) + test := map[string]struct { + webhookHost string + webhookID string + webhookSecret string + codeBlock bool + codeBlockLang string + stdin string + args []string + expectedMessage string + }{ + "ok": {"http://localhost:8080", "test", "test", false, "", "", []string{"test"}, "test"}, + "コードブロックがあっても問題なし": {"http://localhost:8080", "test", "test", true, "", "", []string{"print('Hello, World!')"}, "```\nprint('Hello, World!')\n```"}, + "コードブロックと言語指定があっても問題なし": {"http://localhost:8080", "test", "test", true, "python", "", []string{"print('Hello, World!')"}, "```python\nprint('Hello, World!')\n```"}, + "メッセージがない場合は標準入力から": {"http://localhost:8080", "test", "test", false, "", "stdin test", []string{}, "stdin test"}, + "メッセージがあったら標準入力は無視": {"http://localhost:8080", "test", "test", false, "", "stdin test", []string{"test"}, "test"}, + } - rootCmd.DebugFlags() - // rootCmd.Flag("version").Value.Set("true") - rootCmd.DebugFlags() - viper.Set("webhook_host", "http://localhost:8080") + for description, tt := range test { + t.Run(description, func(t *testing.T) { + viper.Set("webhook_host", tt.webhookHost) + viper.Set("webhook_id", tt.webhookID) + viper.Set("webhook_secret", tt.webhookSecret) - t.Log("version", printVersion) - t.Log(cl) + withCodeBlock = tt.codeBlock + codeBlockLang = tt.codeBlockLang - rootCmd.Run(rootCmd, []string{"test"}) + r, w, err := os.Pipe() + require.NoError(t, err, "failed to create pipe") - }) + origStdin := os.Stdin + os.Stdin = r + defer func() { + os.Stdin = origStdin + r.Close() + }() + + _, err = fmt.Fprint(w, tt.stdin) + require.NoError(t, err, "failed to write to pipe") + w.Close() + + mockClient := &mock.ClientMock{ + SendMessageFunc: func(message string) error { + return nil + }, + } + + SetClient(mockClient) + + rootCmd.RunE(rootCmd, tt.args) + + assert.Len(t, mockClient.SendMessageCalls(), 1) + assert.Equal(t, tt.expectedMessage, mockClient.SendMessageCalls()[0].Message) + }) + } +} + +func TestRoot_NoSendMessage(t *testing.T) { + test := map[string]struct { + webhookHost string + webhookID string + webhookSecret string + args []string + printVersion bool + wantStdout string + wantErr error + }{ + "print version": {"http://localhost:8080", "test", "test", []string{}, true, "q version unknown\n", nil}, + "設定が不十分でもversionをprint": {"", "test", "test", []string{}, true, "q version unknown\n", nil}, + "設定が不十分なのでエラーメッセージ": {"", "", "", []string{"aaa"}, false, "", ErrEmptyConfiguration}, + } + + for description, tt := range test { + t.Run(description, func(t *testing.T) { + viper.Set("webhook_host", tt.webhookHost) + viper.Set("webhook_id", tt.webhookID) + viper.Set("webhook_secret", tt.webhookSecret) + + mockClient := &mock.ClientMock{ + SendMessageFunc: func(message string) error { + return nil + }, + } + + r, w, err := os.Pipe() + require.NoError(t, err, "failed to create pipe") + origStdout := os.Stdout + os.Stdout = w + defer func() { + os.Stdout = origStdout + }() + + printVersion = tt.printVersion + + SetClient(mockClient) + + cmdErr := rootCmd.RunE(rootCmd, []string{}) + w.Close() + + assert.Len(t, mockClient.SendMessageCalls(), 0) + var buffer bytes.Buffer + _, err = buffer.ReadFrom(r) + require.NoError(t, err, "failed to read from pipe") + + assert.Equal(t, buffer.String(), tt.wantStdout) + if tt.wantErr != nil { + assert.ErrorIs(t, tt.wantErr, cmdErr) + } else { + assert.NoError(t, cmdErr) + } + }) + } } diff --git a/go.mod b/go.mod index e2da4ee..0742bc3 100644 --- a/go.mod +++ b/go.mod @@ -3,26 +3,35 @@ module github.com/ikura-hamu/q-cli go 1.22.0 require ( + github.com/matryer/moq v0.3.4 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/mod v0.14.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.17.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 967063c..c0f7049 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,35 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/matryer/moq v0.3.4 h1:czCFIos9rI2tyOehN9ktc/6bQ76N9J4xQ2n3dk063ac= +github.com/matryer/moq v0.3.4/go.mod h1:wqm9QObyoMuUtH81zFfs3EK6mXEcByy+TjvSROOXJ2U= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= @@ -26,8 +41,6 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -42,6 +55,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= @@ -51,11 +65,19 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/client/client.go b/internal/client/client.go index f155bbf..9fbb10b 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -1,5 +1,7 @@ package client +//go:generate go run github.com/matryer/moq -pkg mock -out mock/${GOFILE}.go . Client + type Client interface { // SendMessage sends a message to the webhook URL // if message is empty, it should return ErrEmptyMessage diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..c547c4b --- /dev/null +++ b/tools.go @@ -0,0 +1,7 @@ +//go:build tools + +package main + +import ( + _ "github.com/matryer/moq" +) From e1a4df5293e01c85346326d6b264c8b6e391146d Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Sun, 28 Jul 2024 01:06:08 +0900 Subject: [PATCH 04/15] =?UTF-8?q?:wrench:=20Taskfile=E5=B0=8E=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ Makefile | 18 ------------------ Taskfile.yml | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+), 18 deletions(-) delete mode 100644 Makefile create mode 100644 Taskfile.yml diff --git a/.gitignore b/.gitignore index 1354f05..974f74a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ q dist/ **/mock + +.task diff --git a/Makefile b/Makefile deleted file mode 100644 index c92816b..0000000 --- a/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -# Go compiler -GO := go - -# Output binary name -BINARY := q - -DIR := ~/bin - -# Build target -build: - $(GO) build -o $(BINARY) -ldflags "-s -w" . - -install: - cp $(BINARY) $(DIR) - -# Clean target -clean: - rm -f $(BINARY) diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..9c29ed0 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,24 @@ +# https://taskfile.dev + +version: "3" + +silent: true + +tasks: + test: + cmds: + - go test ./... -v -race + + generate: + cmds: + - go generate ./... + aliases: + - gen + generates: + - ./**/mock/*.go + sources: + - ./internal/*/*.go + + build: + cmds: + - go build -o ./bin/ -ldflags "-s -w" . From 543bbefe13b99e1912019b8b75471048215d0eb7 Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:21:34 +0900 Subject: [PATCH 05/15] =?UTF-8?q?:adhesive=5Fbandage:=20=E3=83=A1=E3=83=83?= =?UTF-8?q?=E3=82=BB=E3=83=BC=E3=82=B8=E3=81=8C=E7=A9=BA=E3=81=AE=E3=81=A8?= =?UTF-8?q?=E3=81=8D=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92=E8=BF=94=E3=81=99?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/client/webhook/client.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/client/webhook/client.go b/internal/client/webhook/client.go index aea2ae9..b75e88b 100644 --- a/internal/client/webhook/client.go +++ b/internal/client/webhook/client.go @@ -9,6 +9,8 @@ import ( "net/http" "net/url" "strings" + + "github.com/ikura-hamu/q-cli/internal/client" ) type WebhookClient struct { @@ -36,6 +38,10 @@ func NewWebhookClient(webhookID string, hostName string, secret string) (*Webhoo } func (c *WebhookClient) SendMessage(message string) error { + if message == "" { + return client.ErrEmptyMessage + } + req, err := http.NewRequest(http.MethodPost, c.webhookURL, strings.NewReader(message)) if err != nil { return fmt.Errorf("failed to create request: %w", err) From 7ff527ca538a07aa836f74e85e90c62bb312d728 Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:28:51 +0900 Subject: [PATCH 06/15] =?UTF-8?q?:adhesive=5Fbandage:=20ErrEmptyMessage?= =?UTF-8?q?=E3=81=AB=E3=81=A4=E3=81=84=E3=81=A6=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/root.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index f4eb97d..9b8798b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -33,7 +33,7 @@ func SetClient(c client.Client) { // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "q-cli", + Use: "q [message]", Short: "traQ Webhook CLI", Long: `"q-cli" is a CLI tool for sending messages to traQ via webhook. It reads the configuration file and sends the message to the specified webhook.`, @@ -82,9 +82,10 @@ It reads the configuration file and sends the message to the specified webhook.` } if cl != nil { - err := cl.SendMessage(message) - // err := makeWebhookRequest(conf, message) + if errors.Is(err, client.ErrEmptyMessage) { + return errors.New("empty message is not allowed") + } if err != nil { return fmt.Errorf("failed to send message: %w", err) } @@ -120,7 +121,6 @@ type webhookConfig struct { func Execute() { err := rootCmd.Execute() if err != nil { - printError("%v", err) os.Exit(1) } } From de31b33361354ad66860021c3531d206d7e02e40 Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:44:26 +0900 Subject: [PATCH 07/15] =?UTF-8?q?:white=5Fcheck=5Fmark:=20ErrEmptyMessage?= =?UTF-8?q?=E3=81=AB=E9=96=A2=E3=81=99=E3=82=8B=E3=83=86=E3=82=B9=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/root_test.go | 53 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/cmd/root_test.go b/cmd/root_test.go index 223893a..e2de575 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -6,6 +6,7 @@ import ( "os" "testing" + "github.com/ikura-hamu/q-cli/internal/client" "github.com/ikura-hamu/q-cli/internal/client/mock" "github.com/spf13/viper" "github.com/stretchr/testify/assert" @@ -13,28 +14,40 @@ import ( ) func TestRoot(t *testing.T) { + type webhookConfig struct { + webhookHost string + webhookID string + webhookSecret string + } + defaultWebhookConfig := webhookConfig{"http://example.com", "test", "test"} + + type input struct { + codeBlock bool + codeBlockLang string + stdin string + args []string + } + test := map[string]struct { - webhookHost string - webhookID string - webhookSecret string - codeBlock bool - codeBlockLang string - stdin string - args []string + webhookConfig + input + SendMessageErr error expectedMessage string + isError bool }{ - "ok": {"http://localhost:8080", "test", "test", false, "", "", []string{"test"}, "test"}, - "コードブロックがあっても問題なし": {"http://localhost:8080", "test", "test", true, "", "", []string{"print('Hello, World!')"}, "```\nprint('Hello, World!')\n```"}, - "コードブロックと言語指定があっても問題なし": {"http://localhost:8080", "test", "test", true, "python", "", []string{"print('Hello, World!')"}, "```python\nprint('Hello, World!')\n```"}, - "メッセージがない場合は標準入力から": {"http://localhost:8080", "test", "test", false, "", "stdin test", []string{}, "stdin test"}, - "メッセージがあったら標準入力は無視": {"http://localhost:8080", "test", "test", false, "", "stdin test", []string{"test"}, "test"}, + "ok": {defaultWebhookConfig, input{false, "", "", []string{"test"}}, nil, "test", false}, + "コードブロックがあっても問題なし": {defaultWebhookConfig, input{true, "", "", []string{"print('Hello, World!')"}}, nil, "```\nprint('Hello, World!')\n```", false}, + "コードブロックと言語指定があっても問題なし": {defaultWebhookConfig, input{true, "python", "", []string{"print('Hello, World!')"}}, nil, "```python\nprint('Hello, World!')\n```", false}, + "メッセージがない場合は標準入力から": {defaultWebhookConfig, input{false, "", "stdin test", nil}, nil, "stdin test", false}, + "メッセージがあったら標準入力は無視": {defaultWebhookConfig, input{false, "", "stdin test", []string{"test"}}, nil, "test", false}, + "SendMessageがErrEmptyMessageを返す": {defaultWebhookConfig, input{false, "", "", nil}, client.ErrEmptyMessage, "", true}, } for description, tt := range test { t.Run(description, func(t *testing.T) { - viper.Set("webhook_host", tt.webhookHost) - viper.Set("webhook_id", tt.webhookID) - viper.Set("webhook_secret", tt.webhookSecret) + viper.Set("webhook_host", tt.webhookConfig.webhookHost) + viper.Set("webhook_id", tt.webhookConfig.webhookID) + viper.Set("webhook_secret", tt.webhookConfig.webhookSecret) withCodeBlock = tt.codeBlock codeBlockLang = tt.codeBlockLang @@ -55,16 +68,22 @@ func TestRoot(t *testing.T) { mockClient := &mock.ClientMock{ SendMessageFunc: func(message string) error { - return nil + return tt.SendMessageErr }, } SetClient(mockClient) - rootCmd.RunE(rootCmd, tt.args) + cmdErr := rootCmd.RunE(rootCmd, tt.args) assert.Len(t, mockClient.SendMessageCalls(), 1) assert.Equal(t, tt.expectedMessage, mockClient.SendMessageCalls()[0].Message) + + if tt.isError { + assert.Error(t, cmdErr) + } else { + assert.NoError(t, cmdErr) + } }) } } From d75d27540be56ae344f51e0ac8a3ac8ec46010af Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:44:51 +0900 Subject: [PATCH 08/15] =?UTF-8?q?:white=5Fcheck=5Fmark:=20client=E3=81=AES?= =?UTF-8?q?endMessage=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/client/webhook/client_test.go | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 internal/client/webhook/client_test.go diff --git a/internal/client/webhook/client_test.go b/internal/client/webhook/client_test.go new file mode 100644 index 0000000..ab0cf24 --- /dev/null +++ b/internal/client/webhook/client_test.go @@ -0,0 +1,57 @@ +package webhook + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/ikura-hamu/q-cli/internal/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSendMessage(t *testing.T) { + var ( + mes string + path string + ) + ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + b, err := io.ReadAll(req.Body) + require.NoError(t, err) + mes = string(b) + path = req.URL.Path + + res.WriteHeader(http.StatusNoContent) + return + })) + defer ts.Close() + + webhookID := "test" + cl, err := NewWebhookClient(webhookID, ts.URL, "test") + require.NoError(t, err) + + testCases := map[string]struct { + message string + isError bool + wantErr error + }{ + "ok": {"test", false, nil}, + "メッセージが空なのでエラー": {"", true, client.ErrEmptyMessage}, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + // 共通のmesにリクエストボディを書き出しているので、t.Parallel()にはできない。 + err := cl.SendMessage(tc.message) + + if tc.isError { + assert.ErrorIs(t, err, tc.wantErr) + return + } + + assert.Equal(t, tc.message, mes) + assert.Equal(t, "/api/v3/webhooks/"+webhookID, path) + }) + } +} From ac0f5a5cf819c7d568f890e9383a6b11a1a56dcc Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Mon, 29 Jul 2024 23:39:43 +0900 Subject: [PATCH 09/15] =?UTF-8?q?:white=5Fcheck=5Fmark:=20=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=83=86=E3=82=B0=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=83=86=E3=82=B9=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/root.go | 7 ++-- integration/root_test.go | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 integration/root_test.go diff --git a/cmd/root.go b/cmd/root.go index 9b8798b..6ccd8b9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -163,9 +163,10 @@ func initConfig() { viper.SetEnvPrefix("q") // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) - } + // if err := viper.ReadInConfig(); err == nil { + // fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + // } + viper.ReadInConfig() } diff --git a/integration/root_test.go b/integration/root_test.go new file mode 100644 index 0000000..eacd2c6 --- /dev/null +++ b/integration/root_test.go @@ -0,0 +1,71 @@ +package integration + +import ( + "io" + "net/http" + "net/http/httptest" + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRoot(t *testing.T) { + ts := httptest.NewServer(nil) + defer ts.Close() + + t.Setenv("Q_WEBHOOK_HOST", ts.URL) + t.Setenv("Q_WEBHOOK_ID", "test") + t.Setenv("Q_WEBHOOK_SECRET", "test") + + baseCommand := []string{"run", ".."} + + testCases := map[string]struct { + args []string + message string + stdout string + }{ + "ok": {args: []string{"test"}, message: "test"}, + "version表示": {args: []string{"-v"}, stdout: "q version (devel)\n"}, + "コードブロック": {args: []string{"-c", "print('Hello, World!')"}, message: "```\nprint('Hello, World!')\n```"}, + "言語指定コードブロック": {args: []string{"-c", "-l", "python", "print('Hello, World!')"}, message: "```python\nprint('Hello, World!')\n```"}, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + checks := []func(*http.Request){} + if tc.message != "" { + checks = append(checks, func(req *http.Request) { + b, err := io.ReadAll(req.Body) + require.NoError(t, err) + assert.Equal(t, tc.message, string(b)) + }) + } + ts.Config.Handler = HandlerWithChecks(checks...) + + cmd := append(baseCommand, tc.args...) + out, err := exec.Command("go", cmd...).Output() + + if err != nil { + t.Log(err) + } + + assert.NoError(t, err) + assert.Equal(t, tc.stdout, string(out)) + }) + } + +} + +func HandlerWithChecks(checks ...func(*http.Request)) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + + for _, check := range checks { + check(req) + } + + res.WriteHeader(http.StatusNoContent) + } +} From aaf3dfb182173e4c88d92f2651cb1648314a7d3f Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Tue, 30 Jul 2024 00:35:01 +0900 Subject: [PATCH 10/15] =?UTF-8?q?:wrench:=20golangci-lint=E5=B0=8E?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .golangci.yaml | 0 .vscode/settings.json | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 .golangci.yaml create mode 100644 .vscode/settings.json diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..e69de29 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c40ede1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "go.lintTool": "golangci-lint", + "go.lintFlags": [ + "--fast" + ] +} \ No newline at end of file From 7b52fa367f0ef1eb4e05c2578d2c685444159c20 Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Tue, 30 Jul 2024 00:35:20 +0900 Subject: [PATCH 11/15] =?UTF-8?q?:recycle:=20Lint=E7=B5=90=E6=9E=9C?= =?UTF-8?q?=E9=81=A9=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/root.go | 3 +-- cmd/util.go | 10 ---------- internal/client/webhook/client_test.go | 1 - 3 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 cmd/util.go diff --git a/cmd/root.go b/cmd/root.go index 6ccd8b9..f1737f8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -166,8 +166,7 @@ func initConfig() { // if err := viper.ReadInConfig(); err == nil { // fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) // } - viper.ReadInConfig() - + _ = viper.ReadInConfig() } func printVersionInfo() { diff --git a/cmd/util.go b/cmd/util.go deleted file mode 100644 index 18bb377..0000000 --- a/cmd/util.go +++ /dev/null @@ -1,10 +0,0 @@ -package cmd - -import ( - "fmt" - "os" -) - -func printError(message string, args ...any) { - fmt.Fprintf(os.Stderr, "q-cli error: "+message, args...) -} diff --git a/internal/client/webhook/client_test.go b/internal/client/webhook/client_test.go index ab0cf24..4b981e1 100644 --- a/internal/client/webhook/client_test.go +++ b/internal/client/webhook/client_test.go @@ -23,7 +23,6 @@ func TestSendMessage(t *testing.T) { path = req.URL.Path res.WriteHeader(http.StatusNoContent) - return })) defer ts.Close() From d4601b7c2f8c4a4c0e6ee0a25b8bd782e620459f Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Tue, 30 Jul 2024 00:35:36 +0900 Subject: [PATCH 12/15] :wrench: CI --- .github/workflows/ci.yml | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a4ba810 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: "CI" + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Install dependencies + run: go mod download + + - name: Generate + run: go generate ./... + + - name: Build + run: go build -v ./... + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Install dependencies + run: go mod download + + - name: Test + run: go test -v -race ./... + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.59.1 From 5a2baef13a2a3eecdd303a83270433873c17c651 Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Tue, 30 Jul 2024 00:41:15 +0900 Subject: [PATCH 13/15] =?UTF-8?q?:bug:=20CI=E3=81=A7generate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4ba810..dde0cb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,9 @@ jobs: - name: Install dependencies run: go mod download + - name: Generate + run: go generate ./... + - name: Test run: go test -v -race ./... @@ -48,6 +51,12 @@ jobs: with: go-version-file: ./go.mod + - name: Install dependencies + run: go mod download + + - name: Generate + run: go generate ./... + - name: Lint uses: golangci/golangci-lint-action@v6 with: From c03f9547cf677198d456fe97e12ddd9b19b216ce Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Tue, 30 Jul 2024 00:44:28 +0900 Subject: [PATCH 14/15] =?UTF-8?q?:fire:=20=E3=82=A4=E3=83=B3=E3=83=86?= =?UTF-8?q?=E3=82=B0=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=82=84=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Taskfile.yml | 2 +- integration/root_test.go | 71 ---------------------------------------- 2 files changed, 1 insertion(+), 72 deletions(-) delete mode 100644 integration/root_test.go diff --git a/Taskfile.yml b/Taskfile.yml index 9c29ed0..85c9a37 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -7,7 +7,7 @@ silent: true tasks: test: cmds: - - go test ./... -v -race + - go test -v -race ./... generate: cmds: diff --git a/integration/root_test.go b/integration/root_test.go deleted file mode 100644 index eacd2c6..0000000 --- a/integration/root_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package integration - -import ( - "io" - "net/http" - "net/http/httptest" - "os/exec" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestRoot(t *testing.T) { - ts := httptest.NewServer(nil) - defer ts.Close() - - t.Setenv("Q_WEBHOOK_HOST", ts.URL) - t.Setenv("Q_WEBHOOK_ID", "test") - t.Setenv("Q_WEBHOOK_SECRET", "test") - - baseCommand := []string{"run", ".."} - - testCases := map[string]struct { - args []string - message string - stdout string - }{ - "ok": {args: []string{"test"}, message: "test"}, - "version表示": {args: []string{"-v"}, stdout: "q version (devel)\n"}, - "コードブロック": {args: []string{"-c", "print('Hello, World!')"}, message: "```\nprint('Hello, World!')\n```"}, - "言語指定コードブロック": {args: []string{"-c", "-l", "python", "print('Hello, World!')"}, message: "```python\nprint('Hello, World!')\n```"}, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - checks := []func(*http.Request){} - if tc.message != "" { - checks = append(checks, func(req *http.Request) { - b, err := io.ReadAll(req.Body) - require.NoError(t, err) - assert.Equal(t, tc.message, string(b)) - }) - } - ts.Config.Handler = HandlerWithChecks(checks...) - - cmd := append(baseCommand, tc.args...) - out, err := exec.Command("go", cmd...).Output() - - if err != nil { - t.Log(err) - } - - assert.NoError(t, err) - assert.Equal(t, tc.stdout, string(out)) - }) - } - -} - -func HandlerWithChecks(checks ...func(*http.Request)) http.HandlerFunc { - return func(res http.ResponseWriter, req *http.Request) { - defer req.Body.Close() - - for _, check := range checks { - check(req) - } - - res.WriteHeader(http.StatusNoContent) - } -} From 18fa7b30c05db071adc7788c43cb67ca3c101f64 Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Tue, 30 Jul 2024 00:52:36 +0900 Subject: [PATCH 15/15] =?UTF-8?q?:memo:=20README=E3=82=92=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 85 +++++++++++++++++++++++++------------------------------ 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 17d1011..439e2f3 100644 --- a/README.md +++ b/README.md @@ -54,39 +54,34 @@ unzip q-cli_{{OS}}_{{architecture}}.zip #### 環境変数の場合 +`Q_WEBHOOK_HOST`: traQのドメイン `Q_WEBHOOK_ID`: traQのWebhook ID `Q_WEBHOOK_SECRET`: traQのWebhook シークレット #### 設定ファイルを使う場合 -以下のような`config.json`を用意します。 +以下のような`.q-cli.yml`を用意します。 -```json:config.json -{ - "webhook_id": "{{traQのWebhook ID}}", - "webhook_secret": "{{traQのWebhookシークレット}}" -} +```json:.q-cli.yml +webhook_host: "{{traQのドメイン}}" +webhook_id: "{{traQのWebhook ID}}" +webhook_secret: "{{traQのWebhookシークレット}}" ``` -それを適切なディレクトリに配置します。以下を参考にして配置してください。(Goの`os.UserConfigDir()`を使用しています。) - -> UserConfigDir returns the default root directory to use for user-specific configuration data. Users should create their own application-specific subdirectory within this one and use that. -> -> On Unix systems, it returns \$XDG_CONFIG_HOME as specified by https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if non-empty, else \$HOME/.config. On Darwin, it returns \$HOME/Library/Application Support. On Windows, it returns %AppData%. On Plan 9, it returns $home/lib. -> -> If the location cannot be determined (for example, $HOME is not defined), then it will return an error. +このファイルをホームディレクトリに配置します。 ## Usage ```txt -Usage of q: -q [option] [message] - -c string - Send message as code block. Specify the language name (e.g. go, python, shell) - -h Print this message - -i Interactive mode - -s Accept input from stdin - -v Print version +Usage: + q [message] [flags] + +Flags: + -c, --code Send message with code block + --config string config file (default is $HOME/.q-cli.yaml) + -h, --help help for q + -l, --lang string Code block language + -v, --version Print version information and quit ``` ```sh @@ -106,55 +101,51 @@ traQに「Hello World」と投稿されます。 --- ```sh -q -i -``` - -```txt -q [1]: 1行目 -q [1]: 2行目 -q [1]: -q [2]: +q ``` -`-i` オプションを使用すると、ターミナルのように連続して、また、複数行のメッセージを送信できます(interactive mode)。改行では送信されず、`Ctrl-D`を入力すると送信されます。この例では、 - ```txt 1行目 2行目 + ``` -のようになります。 +メッセージが何もない場合は、標準入力からテキストを受け取り、投稿します。`EOF`(`Ctrl-D`)を受け取るまで投稿を待ちます。 + +以下のようにしてファイルの中身を投稿できます。 -また、何も入力されていない状態で`Ctrl-D`を入力するとinteractive modeを抜けられます。 +```sh +q < a.txt +``` --- ```sh -echo foo | q -s +q -c text ``` -```txt -foo +````md +``` +text ``` +```` -`-s`オプションを使用すると、標準入力から値を受け取り、それをそのままメッセージとして投稿できます。機密情報を間違って投稿しないよう気を付けてください。 +`--code`(`-c`)オプションを使用すると、コードブロックとして投稿できます。 ---- +`--lang`(`-l`)オプションで指定した値が最初の` ``` `の後に追加され、traQ上で適切なシンタックスハイライトが付きます。 ```sh -q -c go package main +q -c -l go < main.go ``` -````md +````txt ```go package main -``` -```` - -`-c`オプションを使用すると、コードブロックとして投稿できます。オプションで指定した値が最初の` ``` `の後に追加され、traQ上で適切なシンタックスハイライトが付きます。値は必ず指定しなくてはいけません。 -上の`-s`オプションと組み合わせるとファイルの中身をきれいにtraQに投稿できます。 +import "fmt" -```sh -cat main.go | q -s -c go +func main() { + fmt.Println("Hello, world") +} ``` +````