Repository: CatchZeng/dingtalk Branch: master Commit: f4f1df9b5f30 Files: 50 Total size: 118.2 KB Directory structure: gitextract_cg7u8c_q/ ├── .editorconfig ├── .github/ │ └── workflows/ │ └── go.yml ├── .gitignore ├── .vscode/ │ └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── READMEEN.md ├── build/ │ └── package/ │ ├── Dockerfile │ └── build.sh ├── cmd/ │ └── dingtalk/ │ ├── actionCard.go │ ├── actionCard_test.go │ ├── feedCard.go │ ├── feedCard_test.go │ ├── link.go │ ├── link_test.go │ ├── markdown.go │ ├── markdown_test.go │ ├── root.go │ ├── root_test.go │ ├── text.go │ ├── text_test.go │ ├── version.go │ └── version_test.go ├── configs/ │ ├── app.go │ ├── configs.go │ └── configs_test.go ├── go.mod ├── go.sum ├── internal/ │ └── security/ │ ├── security.go │ └── security_test.go ├── main.go ├── pkg/ │ └── dingtalk/ │ ├── actionCard.go │ ├── actionCard_test.go │ ├── client.go │ ├── client_test.go │ ├── feedCard.go │ ├── feedCard_test.go │ ├── link.go │ ├── link_test.go │ ├── markdown.go │ ├── markdown_test.go │ ├── message.go │ ├── text.go │ └── text_test.go ├── scripts/ │ ├── mock.sh │ └── test.sh ├── sonar-project.properties └── test/ └── mocks/ └── message/ └── message.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.go] indent_style = tab indent_size = 2 [{Makefile, Dockerfile}] indent_style = tab indent_size = 4 [*.{yml, yaml, json}] indent_style = space indent_size = 2 ================================================ FILE: .github/workflows/go.yml ================================================ name: Go on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Set up Go 1.x uses: actions/setup-go@v2 with: go-version: ^1.16 id: go - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Get dependencies run: | make mod - name: Test run: make test - name: codecoe run: bash <(curl -s https://codecov.io/bash) ================================================ FILE: .gitignore ================================================ dingtalk-darwin-amd64.zip dingtalk-darwin-arm64.zip dingtalk-linux-amd64.zip dingtalk-windows-386.zip dingtalk-windows-amd64.zip dingtalk.exe .idea coverage.txt coverage.data .scannerwork ================================================ FILE: .vscode/settings.json ================================================ { "cSpell.words": ["btns", "dingtalk", "gomock", "mitchellh", "OAPI", "Unpatch"] } ================================================ FILE: CHANGELOG.md ================================================ # CHANGELOG ## v1.5.0 ### Added - Support the environment variable prefix ## v1.4.0 ### Added - Support environment variables ### Refactor - update to golang 1.18.1 ## v1.2.0 ### Refactor - project layout ### Added - support config.xml - validate function - .editconfig file ## v1.1.1 ### Fixed - Fix release response body resource, see more at . - Fix bug about timestamp, see more at . ## v1.1.0 ### Added - actionCard - feedCard ## v1.0.0 ### Added - text - link - markdown ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2020 Catch Zeng Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ SHELL := /bin/bash BASEDIR = $(shell pwd) APP_NAME=dingtalk APP_VERSION=1.5.0 IMAGE_NAME="catchzeng/${APP_NAME}:${APP_VERSION}" IMAGE_LATEST="catchzeng/${APP_NAME}:latest" all: mod fmt imports lint test first: go install golang.org/x/tools/cmd/goimports@latest go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest fmt: gofmt -w . mod: go mod tidy imports: goimports -w . lint: golangci-lint run .PHONY: test test: sh scripts/test.sh test-sonar: go test -gcflags=-l -coverpkg=./... -coverprofile=coverage.data ./... mock: sh scripts/mock.sh .PHONY: build build: rm -f dingtalk go build -o dingtalk main.go build-darwin-amd64: rm -f dingtalk dingtalk-darwin-amd64.zip CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o dingtalk main.go zip dingtalk-darwin-amd64.zip dingtalk build-darwin-arm64: rm -f dingtalk dingtalk-darwin-arm64.zip CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o dingtalk main.go zip dingtalk-darwin-arm64.zip dingtalk build-linux: rm -f dingtalk dingtalk-linux-amd64.zip CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dingtalk main.go zip dingtalk-linux-amd64.zip dingtalk build-win: rm -f dingtalk.exe dingtalk-windows-amd64.zip CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o dingtalk.exe main.go zip dingtalk-windows-amd64.zip dingtalk.exe build-win32: rm -f dingtalk.exe dingtalk-windows-386.zip CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -o dingtalk.exe main.go zip dingtalk-windows-386.zip dingtalk.exe build-release: make build-darwin-amd64 make build-darwin-arm64 make build-linux make build-win make build-win32 rm -f dingtalk dingtalk.exe build-docker: sh build/package/build.sh ${IMAGE_NAME} push-docker: docker tag ${IMAGE_NAME} ${IMAGE_LATEST}; docker push ${IMAGE_NAME}; docker push ${IMAGE_LATEST}; help: @echo "first - first time" @echo "fmt - go format" @echo "mod - go mod tidy" @echo "imports - go imports" @echo "lint - run golangci-lint" @echo "test - unit test" @echo "mock - mockgen" @echo "build - build binary" @echo "build-mac - build mac binary" @echo "build-linux - build linux amd64 binary" @echo "build-win - build win amd64 binary" @echo "build-win32 - build win 386 binary" @echo "build-docker - build docker image" @echo "push-docker - push docker image to docker hub" ================================================ FILE: README.md ================================================ # dingtalk ![Go](https://github.com/CatchZeng/dingtalk/workflows/Go/badge.svg) [![codecov](https://codecov.io/gh/CatchZeng/dingtalk/branch/master/graph/badge.svg)](https://codecov.io/gh/CatchZeng/dingtalk) [![Go Report Card](https://goreportcard.com/badge/github.com/CatchZeng/dingtalk)](https://goreportcard.com/report/github.com/CatchZeng/dingtalk) [![Release](https://img.shields.io/github/release/CatchZeng/dingtalk.svg)](https://github.com/CatchZeng/dingtalk/releases) [![GoDoc](https://godoc.org/github.com/CatchZeng/dingtalk?status.svg)](https://pkg.go.dev/github.com/CatchZeng/dingtalk?tab=doc) [English](https://github.com/CatchZeng/dingtalk/blob/master/READMEEN.md) > DingTalk(dingding) 是钉钉机器人的 go 实现。支持 **Docker、Jenkinsfile、命令行模式,module 模式**;支持**加签**安全设置,支持**链式语法**创建消息;支持**文本、链接、Markdown、ActionCard、FeedCard** 消息类型。 > 注:使用飞书的小伙伴,可以使用[飞书(feishu)版](https://github.com/CatchZeng/feishu)。 ## 文档 [钉钉文档](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq) ## 特性 - [x] 支持[Docker](https://github.com/CatchZeng/dingtalk#Docker) - [x] 支持[Jenkinsfile](https://github.com/CatchZeng/dingtalk#Jenkinsfile) - [x] 支持[module](https://github.com/CatchZeng/dingtalk#%E4%BD%9C%E4%B8%BA-module) - [x] 支持[命令行模式](https://github.com/CatchZeng/dingtalk#%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7) - [x] 支持[配置文件](https://github.com/CatchZeng/dingtalk#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6) - [x] 支持[环境变量](https://github.com/CatchZeng/dingtalk#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F) - [x] 支持加签 - [x] Text 消息 - [x] Link 消息 - [x] Markdown 消息 - [x] ActionCard 消息 - [x] FeedCard 消息 ## 安装 ## Docker 安装 ```shell docker pull catchzeng/dingtalk ``` ### 二进制安装 到 [releases](https://github.com/CatchZeng/dingtalk/releases/) 下载相应平台的二进制可执行文件,然后加入到 PATH 环境变量即可。 ### go install 安装 ```sh # Go 1.16+ go install github.com/CatchZeng/dingtalk@v1.5.0 # Go version < 1.16 go get -u github.com/CatchZeng/dingtalk@v1.5.0 ``` ## 使用方法 ### 配置文件 可以在 `$/HOME/.dingtalk` 下创建 `config.yaml` 填入 `access_token` 和 `secret` 默认值。 ```yaml access_token: "1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" secret: "SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" ``` ### 环境变量 ```sh $ export ACCESS_TOKEN="1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" $ export SECRET="SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" $ dingtalk link -i "标题" -e "信息" -u "https://makeoptim.com/" -p "https://makeoptim.com/assets/img/logo.png" -a ``` 你也可以为环境变量设置一个**前缀** ```sh $ export DINGTALK_ENV_PREFIX="DINGTALK_" $ export DINGTALK_ACCESS_TOKEN="1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" $ export DINGTALK_SECRET="SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" $ dingtalk link -i "标题" -e "信息" -u "https://makeoptim.com/" -p "https://makeoptim.com/assets/img/logo.png" -a ``` ### Docker ```shell docker run catchzeng/dingtalk dingtalk text -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 -c "docker test" ``` ### Jenkinsfile ```shell pipeline { agent { docker { image 'catchzeng/dingtalk:latest' } } environment { DING_TOKEN = '1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f' DING_SECRET = 'SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68' } stages { stage('notify') { steps { sh 'dingtalk link -t ${DING_TOKEN} -s ${DING_SECRET} -i "标题" -e "信息" -u "https://makeoptim.com/" -p "https://makeoptim.com/assets/img/logo.png" -a' } } } } ``` ### 作为 module ```sh go get github.com/CatchZeng/dingtalk ``` ```go package main import ( "log" "github.com/CatchZeng/dingtalk/pkg/dingtalk" ) func main() { accessToken := "1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" secret := "SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" client := dingtalk.NewClient(accessToken, secret) msg := dingtalk.NewTextMessage().SetContent("测试文本&at 某个人").SetAt([]string{"177010xxx60"}, false) client.Send(msg) } ``` ### 命令行工具 #### Demo ```shell $ dingtalk text -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 -c "测试命令行 & at 某个人" -m "177010xxx60","177010xxx61" ``` ```shell $ dingtalk markdown -D -i "杭州天气" -e '## 杭州天气 @150XXXXXXXX > 9度,西北风1级,空气良89,相对温度73% > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png) > ###### 10点20分发布 [天气](https://www.dingtalk.com)' -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 {"msgtype":"markdown","markdown":{"title":"杭州天气","text":"## 杭州天气 @150XXXXXXXX\n \u003e 9度,西北风1级,空气良89,相对温度73%\n \u003e ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n \u003e ###### 10点20分发布 [天气](https://www.dingtalk.com)"},"at":{"atMobiles":[],"isAtAll":false}} ``` > -D 参数:打印发送的消息内容 #### Help ```shell $ dingtalk -h dingtalk is a command line tool for DingTalk Usage: dingtalk [command] Available Commands: actionCard send actionCard message with DingTalk robot feedCard send feedCard message with DingTalk robot help Help about any command link send link message with DingTalk robot markdown send markdown message with DingTalk robot text send text message with DingTalk robot version dingtalk version Flags: -t, --access_token string access_token -m, --atMobiles strings atMobiles -D, --debug debug -h, --help help for dingtalk -a, --isAtAll isAtAll -s, --secret string secret Use "dingtalk [command] --help" for more information about a command. ``` ## Stargazers [![Stargazers over time](https://starchart.cc/CatchZeng/dingtalk.svg)](https://starchart.cc/CatchZeng/dingtalk) ================================================ FILE: READMEEN.md ================================================ # dingtalk ![Go](https://github.com/CatchZeng/dingtalk/workflows/Go/badge.svg) [![codecov](https://codecov.io/gh/CatchZeng/dingtalk/branch/master/graph/badge.svg)](https://codecov.io/gh/CatchZeng/dingtalk) [![Go Report Card](https://goreportcard.com/badge/github.com/CatchZeng/dingtalk)](https://goreportcard.com/report/github.com/CatchZeng/dingtalk) [![Release](https://img.shields.io/github/release/CatchZeng/dingtalk.svg)](https://github.com/CatchZeng/dingtalk/releases) [![GoDoc](https://godoc.org/github.com/CatchZeng/dingtalk?status.svg)](https://pkg.go.dev/github.com/CatchZeng/dingtalk?tab=doc) [中文](https://github.com/CatchZeng/dingtalk/blob/master/README.md) > DingTalk (dingding) is the go implementation of the DingTalk robot. Support **Docker, Jenkinsfile,command line mode, module mode**, **signature security settings, chain syntax** to create messages, support **text, link, markdown、ActionCard、FeedCard** message types. ## Doc [ding-doc](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq) ## Feature - [x] Support [Docker](https://github.com/CatchZeng/dingtalk#Docker) - [x] Support [Jenkinsfile](https://github.com/CatchZeng/dingtalk#Jenkinsfile) - [x] Support [module](https://github.com/CatchZeng/dingtalk/blob/master/READMEEN.md#use-as-module) - [x] Support [Command Line Mode](https://github.com/CatchZeng/dingtalk/blob/master/READMEEN.md#use-as-command-line-tool) - [x] Support [config.yaml](https://github.com/CatchZeng/dingtalk/blob/master/READMEEN.md#config.yaml) - [x] Support [environment variables](https://github.com/CatchZeng/dingtalk#environment%20variables) - [x] Support sign - [x] Text message - [x] Link message - [x] Markdown message - [x] ActionCard message - [x] FeedCard message ## Install ## with Docker ```shell docker pull catchzeng/dingtalk ``` ### binary Go to [releases](https://github.com/CatchZeng/dingtalk/releases/) to download the binary executable file of the corresponding platform, and then add it to the PATH environment variable. ### with go install ```sh # Go 1.16+ go install github.com/CatchZeng/dingtalk@v1.5.0 # Go version < 1.16 go get -u github.com/CatchZeng/dingtalk@v1.5.0 ``` ## Usage ### config.yaml You can create `config.yaml` under `$/HOME/.dingtalk` and fill in the default values of `access_token` and `secret`. ```yaml access_token: "1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" secret: "SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" ``` ### environment variables ```sh $ export ACCESS_TOKEN=1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f $ export SECRET=SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 $ dingtalk link -i "标题" -e "信息" -u "https://makeoptim.com/" -p "https://makeoptim.com/assets/img/logo.png" -a ``` You can also set a **prefix** for environment variables. ```sh $ export DINGTALK_ENV_PREFIX="DINGTALK_" $ export DINGTALK_ACCESS_TOKEN="1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" $ export DINGTALK_SECRET="SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" $ dingtalk link -i "标题" -e "信息" -u "https://makeoptim.com/" -p "https://makeoptim.com/assets/img/logo.png" -a ``` ### Docker ```shell docker run catchzeng/dingtalk dingtalk text -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 -c "docker test" ``` ### Jenkinsfile ```shell pipeline { agent { docker { image 'catchzeng/dingtalk:latest' } } environment { DING_TOKEN = '1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f' DING_SECRET = 'SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68' } stages { stage('notify') { steps { sh 'dingtalk link -t ${DING_TOKEN} -s ${DING_SECRET} -i "标题" -e "信息" -u "https://makeoptim.com/" -p "https://makeoptim.com/assets/img/logo.png" -a' } } } } ``` ### Use as module ```sh go get github.com/CatchZeng/dingtalk ``` ```go package main import ( "log" "github.com/CatchZeng/dingtalk/pkg/dingtalk" ) func main() { accessToken := "1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" secret := "SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" client := dingtalk.NewClient(accessToken, secret) msg := dingtalk.NewTextMessage().SetContent("测试文本&at 某个人").SetAt([]string{"177010xxx60"}, false) client.Send(msg) } ``` ### Use as command line tool #### Demo ```shell dingtalk text -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 -c "测试命令行 & at 某个人" -m "177010xxx60","177010xxx61" ``` ```shell $ dingtalk markdown -D -i "杭州天气" -e '## 杭州天气 @150XXXXXXXX > 9度,西北风1级,空气良89,相对温度73% > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png) > ###### 10点20分发布 [天气](https://www.dingtalk.com)' -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 {"msgtype":"markdown","markdown":{"title":"杭州天气","text":"## 杭州天气 @150XXXXXXXX\n \u003e 9度,西北风1级,空气良89,相对温度73%\n \u003e ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n \u003e ###### 10点20分发布 [天气](https://www.dingtalk.com)"},"at":{"atMobiles":[],"isAtAll":false}} ``` > -D: print the message content #### Help ```shell dingtalk is a command line tool for DingTalk Usage: dingtalk [command] Available Commands: actionCard send actionCard message with DingTalk robot feedCard send feedCard message with DingTalk robot help Help about any command link send link message with DingTalk robot markdown send markdown message with DingTalk robot text send text message with DingTalk robot version dingtalk version Flags: -t, --access_token string access_token -m, --atMobiles strings atMobiles -D, --debug debug -h, --help help for dingtalk -a, --isAtAll isAtAll -s, --secret string secret Use "dingtalk [command] --help" for more information about a command. ``` ## Stargazers [![Stargazers over time](https://starchart.cc/CatchZeng/dingtalk.svg)](https://starchart.cc/CatchZeng/dingtalk) ================================================ FILE: build/package/Dockerfile ================================================ FROM golang:1.18.1-alpine as builder RUN mkdir /build ADD . /build/ WORKDIR /build RUN go mod tidy && go build -o dingtalk main.go FROM alpine:3.7 COPY --from=builder /build/dingtalk /usr/local/bin/dingtalk RUN chmod +x /usr/local/bin/dingtalk ================================================ FILE: build/package/build.sh ================================================ #!/bin/bash shell_dir=$(dirname $0) cd ${shell_dir} # check params if [[ ! $1 ]]; then echo "image tag is null"; exit 1; else echo "image tag: $1" fi cd ../../ docker build -t $1 -f build/package/Dockerfile . ================================================ FILE: cmd/dingtalk/actionCard.go ================================================ package dingtalk import ( "log" "github.com/CatchZeng/dingtalk/pkg/dingtalk" "github.com/spf13/cobra" ) var actionCardCmd = &cobra.Command{ Use: "actionCard", Short: "send actionCard message with DingTalk robot", Long: `send actionCard message with DingTalk robot`, Args: cobra.MinimumNArgs(0), Run: runActionCardCmd, } func runActionCardCmd(_ *cobra.Command, args []string) { if len(actionCardVars.Title) < 1 { log.Fatal("title can not be empty") return } if len(actionCardVars.Text) < 1 { log.Fatal("text can not be empty") return } var isOverallJump = false if len(actionCardVars.SingleTitle) < 1 { if len(btnTitles) < 1 { log.Fatal("btns can not be empty when singleTitle is empty") return } } else { isOverallJump = true if len(actionCardVars.SingleURL) < 1 { log.Fatal("singleURL can not be empty") return } } client, err := newClient() if err != nil { log.Fatal(err.Error()) return } msg := dingtalk.NewActionCardMessage() if isOverallJump { msg.SetOverallJump( actionCardVars.Title, actionCardVars.Text, actionCardVars.SingleTitle, actionCardVars.SingleURL, actionCardVars.BtnOrientation, actionCardVars.HideAvatar) } else { if len(btnTitles) != len(btnActionURLs) { log.Fatal("btnTitles & btnActionURLs count must be equal") return } for i := 0; i < len(btnTitles); i++ { actionCardVars.Btns = append(actionCardVars.Btns, dingtalk.Btn{ Title: btnTitles[i], ActionURL: btnActionURLs[i], }) } msg.SetIndependentJump( actionCardVars.Title, actionCardVars.Text, actionCardVars.Btns, actionCardVars.BtnOrientation, actionCardVars.HideAvatar) } req, _, err := client.Send(msg) if debug { log.Print(req) } if err != nil { log.Fatal(err.Error()) } } var actionCardVars dingtalk.ActionCard var btnTitles, btnActionURLs []string func init() { rootCmd.AddCommand(actionCardCmd) actionCardCmd.Flags().StringVarP(&actionCardVars.Title, "title", "i", "", "title") actionCardCmd.Flags().StringVarP(&actionCardVars.Text, "text", "e", "", "text") actionCardCmd.Flags().StringVarP(&actionCardVars.SingleTitle, "singleTitle", "n", "", "singleTitle") actionCardCmd.Flags().StringVarP(&actionCardVars.SingleURL, "singleURL", "u", "", "singleURL") actionCardCmd.Flags().StringSliceVarP(&btnTitles, "btnTitles", "b", []string{}, "btnTitles") actionCardCmd.Flags().StringSliceVarP(&btnActionURLs, "btnActionURLs", "c", []string{}, "btnActionURLs") actionCardCmd.Flags().StringVarP(&actionCardVars.BtnOrientation, "btnOrientation", "o", "", "btnOrientation") actionCardCmd.Flags().StringVarP(&actionCardVars.HideAvatar, "hideAvatar", "d", "", "hideAvatar") } ================================================ FILE: cmd/dingtalk/actionCard_test.go ================================================ package dingtalk import ( "bytes" "errors" "log" "os" "strings" "testing" "bou.ke/monkey" "github.com/CatchZeng/dingtalk/pkg/dingtalk" "github.com/spf13/cobra" ) func Test_runActionCardCmd(t *testing.T) { fakeExit := func(int) { log.Print("fake exit") } patch := monkey.Patch(os.Exit, fakeExit) defer patch.Unpatch() t.Run("title is empty", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() runActionCardCmd(&cobra.Command{}, []string{}) got := buf.String() want := "title can not be empty" if !strings.Contains(got, want) { t.Errorf("runActionCardCmd() = %v, want %v", got, want) } }) t.Run("text is empty", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() actionCardVars.Title = "123" runActionCardCmd(&cobra.Command{}, []string{}) got := buf.String() want := "text can not be empty" if !strings.Contains(got, want) { t.Errorf("runActionCardCmd() = %v, want %v", got, want) } }) t.Run("singleTitle is empty", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() actionCardVars.Title = "123" actionCardVars.Text = "123" actionCardVars.SingleTitle = "" btnTitles = []string{} runActionCardCmd(&cobra.Command{}, []string{}) got := buf.String() want := "btns can not be empty when singleTitle is empty" if !strings.Contains(got, want) { t.Errorf("runActionCardCmd() = %v, want %v", got, want) } }) t.Run("singleTitle is not empty", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() actionCardVars.Title = "123" actionCardVars.Text = "123" actionCardVars.SingleTitle = "123" actionCardVars.SingleURL = "" runActionCardCmd(&cobra.Command{}, []string{}) got := buf.String() want := "singleURL can not be empty" if !strings.Contains(got, want) { t.Errorf("runActionCardCmd() = %v, want %v", got, want) } }) t.Run("new client error", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() actionCardVars.Title = "123" actionCardVars.Text = "123" actionCardVars.SingleTitle = "123" actionCardVars.SingleURL = "123" msg := "new client error" monkey.Patch(newClient, func() (*dingtalk.Client, error) { return nil, errors.New(msg) }) defer monkey.Unpatch(newClient) runActionCardCmd(&cobra.Command{}, []string{}) got := buf.String() if !strings.Contains(got, msg) { t.Errorf("runActionCardCmd() = %v, want %v", got, msg) } }) t.Run("btnTitles & btnActionURLs different count ", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() actionCardVars.Title = "123" actionCardVars.Text = "123" actionCardVars.SingleTitle = "" actionCardVars.SingleURL = "" btnTitles = []string{"1"} btnActionURLs = []string{"1", "2"} msg := "btnTitles & btnActionURLs count must be equal" client := &dingtalk.Client{} monkey.Patch(newClient, func() (*dingtalk.Client, error) { return client, nil }) defer monkey.Unpatch(newClient) runActionCardCmd(&cobra.Command{}, []string{}) got := buf.String() if !strings.Contains(got, msg) { t.Errorf("runActionCardCmd() = %v, want %v", got, msg) } }) t.Run("client send", func(t *testing.T) { actionCardVars.Title = "123" actionCardVars.Text = "123" actionCardVars.SingleTitle = "123" actionCardVars.SingleURL = "123" client := &dingtalk.Client{} monkey.Patch(newClient, func() (*dingtalk.Client, error) { return client, nil }) defer monkey.Unpatch(newClient) runActionCardCmd(&cobra.Command{}, []string{}) }) } ================================================ FILE: cmd/dingtalk/feedCard.go ================================================ package dingtalk import ( "log" "github.com/CatchZeng/dingtalk/pkg/dingtalk" "github.com/spf13/cobra" ) var feedCardCmd = &cobra.Command{ Use: "feedCard", Short: "send feedCard message with DingTalk robot", Long: `send feedCard message with DingTalk robot`, Args: cobra.MinimumNArgs(0), Run: runFeedCardCmd, } func runFeedCardCmd(_ *cobra.Command, args []string) { if len(feedCardVars.titles) < 1 || len(feedCardVars.picURLs) < 1 || len(feedCardVars.messageURLs) < 1 { log.Fatal("titles & picURLs & messageURLs can not be empty") return } if len(feedCardVars.titles) == len(feedCardVars.picURLs) && len(feedCardVars.picURLs) == len(feedCardVars.messageURLs) { client, err := newClient() if err != nil { log.Fatal(err.Error()) return } msg := dingtalk.NewFeedCardMessage() for i := 0; i < len(feedCardVars.titles); i++ { msg.AppendLink(feedCardVars.titles[i], feedCardVars.messageURLs[i], feedCardVars.picURLs[i]) } req, _, err := client.Send(msg) if debug { log.Print(req) } if err != nil { log.Fatal(err.Error()) return } } else { log.Fatal("titles & picURLs & messageURLs count must be equal") } } // FeedCardVars struct type FeedCardVars struct { titles []string picURLs []string messageURLs []string } var feedCardVars FeedCardVars func init() { rootCmd.AddCommand(feedCardCmd) feedCardCmd.Flags().StringSliceVarP(&feedCardVars.titles, "titles", "i", []string{}, "titles") feedCardCmd.Flags().StringSliceVarP(&feedCardVars.picURLs, "picURLs", "p", []string{}, "picURLs") feedCardCmd.Flags().StringSliceVarP(&feedCardVars.messageURLs, "messageURLs", "u", []string{}, "messageURLs") } ================================================ FILE: cmd/dingtalk/feedCard_test.go ================================================ package dingtalk import ( "bytes" "errors" "log" "os" "strings" "testing" "bou.ke/monkey" "github.com/CatchZeng/dingtalk/pkg/dingtalk" "github.com/spf13/cobra" ) func Test_runFeedCardCmd(t *testing.T) { fakeExit := func(int) { log.Print("fake exit") } patch := monkey.Patch(os.Exit, fakeExit) defer patch.Unpatch() const emptyMsg = "titles & picURLs & messageURLs can not be empty" const differentCountMsg = "titles & picURLs & messageURLs count must be equal" t.Run("titles is empty", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() runFeedCardCmd(&cobra.Command{}, []string{}) got := buf.String() want := emptyMsg if !strings.Contains(got, want) { t.Errorf("runFeedCardCmd() = %v, want %v", got, want) } }) t.Run("titles & picURLs different count", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() feedCardVars.titles = []string{"1", "2"} feedCardVars.picURLs = []string{"1"} feedCardVars.messageURLs = []string{"1"} runFeedCardCmd(&cobra.Command{}, []string{}) got := buf.String() want := differentCountMsg if !strings.Contains(got, want) { t.Errorf("runFeedCardCmd() = %v, want %v", got, want) } }) t.Run("new client error", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() feedCardVars.titles = []string{"1"} feedCardVars.picURLs = []string{"1"} feedCardVars.messageURLs = []string{"1"} msg := "new client error" monkey.Patch(newClient, func() (*dingtalk.Client, error) { return nil, errors.New(msg) }) defer monkey.Unpatch(newClient) runFeedCardCmd(&cobra.Command{}, []string{}) got := buf.String() if !strings.Contains(got, msg) { t.Errorf("runFeedCardCmd() = %v, want %v", got, msg) } }) t.Run("client send", func(t *testing.T) { feedCardVars.titles = []string{"1"} feedCardVars.picURLs = []string{"1"} feedCardVars.messageURLs = []string{"1"} client := &dingtalk.Client{} monkey.Patch(newClient, func() (*dingtalk.Client, error) { return client, nil }) defer monkey.Unpatch(newClient) runFeedCardCmd(&cobra.Command{}, []string{}) }) } ================================================ FILE: cmd/dingtalk/link.go ================================================ package dingtalk import ( "log" "github.com/CatchZeng/dingtalk/pkg/dingtalk" "github.com/spf13/cobra" ) var linkCmd = &cobra.Command{ Use: "link", Short: "send link message with DingTalk robot", Long: `send link message with DingTalk robot`, Args: cobra.MinimumNArgs(0), Run: runLinkCmd, } func runLinkCmd(_ *cobra.Command, args []string) { if len(linkVars.title) < 1 { log.Fatal("title can not be empty") return } if len(linkVars.text) < 1 { log.Fatal("text can not be empty") return } if len(linkVars.messageURL) < 1 { log.Fatal("messageURL can not be empty") return } client, err := newClient() if err != nil { log.Fatal(err.Error()) return } msg := dingtalk.NewLinkMessage(). SetLink(linkVars.title, linkVars.text, linkVars.picURL, linkVars.messageURL) req, _, err := client.Send(msg) if debug { log.Print(req) } if err != nil { log.Fatal(err.Error()) } } // LinkVars struct type LinkVars struct { title string text string picURL string messageURL string } var linkVars LinkVars func init() { rootCmd.AddCommand(linkCmd) linkCmd.Flags().StringVarP(&linkVars.title, "title", "i", "", "title") linkCmd.Flags().StringVarP(&linkVars.text, "text", "e", "", "text") linkCmd.Flags().StringVarP(&linkVars.picURL, "picURL", "p", "", "picURL") linkCmd.Flags().StringVarP(&linkVars.messageURL, "messageURL", "u", "", "messageURL") } ================================================ FILE: cmd/dingtalk/link_test.go ================================================ package dingtalk import ( "bytes" "errors" "log" "os" "strings" "testing" "bou.ke/monkey" "github.com/CatchZeng/dingtalk/pkg/dingtalk" "github.com/spf13/cobra" ) func Test_runLinkCmd(t *testing.T) { fakeExit := func(int) { log.Print("fake exit") } patch := monkey.Patch(os.Exit, fakeExit) defer patch.Unpatch() t.Run("title is empty", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() runLinkCmd(&cobra.Command{}, []string{}) got := buf.String() want := "title can not be empty" if !strings.Contains(got, want) { t.Errorf("runLinkCmd() = %v, want %v", got, want) } }) t.Run("text is empty", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() linkVars.title = "123" runLinkCmd(&cobra.Command{}, []string{}) got := buf.String() want := "text can not be empty" if !strings.Contains(got, want) { t.Errorf("runLinkCmd() = %v, want %v", got, want) } }) t.Run("messageURL is empty", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() linkVars.title = "123" linkVars.text = "123" runLinkCmd(&cobra.Command{}, []string{}) got := buf.String() want := "messageURL can not be empty" if !strings.Contains(got, want) { t.Errorf("runLinkCmd() = %v, want %v", got, want) } }) t.Run("new client error", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() linkVars.title = "123" linkVars.text = "123" linkVars.messageURL = "123" msg := "new client error" monkey.Patch(newClient, func() (*dingtalk.Client, error) { return nil, errors.New(msg) }) defer monkey.Unpatch(newClient) runLinkCmd(&cobra.Command{}, []string{}) got := buf.String() if !strings.Contains(got, msg) { t.Errorf("runLinkCmd() = %v, want %v", got, msg) } }) t.Run("client send", func(t *testing.T) { linkVars.title = "123" linkVars.text = "123" linkVars.messageURL = "123" client := &dingtalk.Client{} monkey.Patch(newClient, func() (*dingtalk.Client, error) { return client, nil }) defer monkey.Unpatch(newClient) runLinkCmd(&cobra.Command{}, []string{}) }) } ================================================ FILE: cmd/dingtalk/markdown.go ================================================ package dingtalk import ( "log" "github.com/CatchZeng/dingtalk/pkg/dingtalk" "github.com/spf13/cobra" ) var markdownCmd = &cobra.Command{ Use: "markdown", Short: "send markdown message with DingTalk robot", Long: `send markdown message with DingTalk robot`, Args: cobra.MinimumNArgs(0), Run: runMarkdownCmd, } func runMarkdownCmd(_ *cobra.Command, args []string) { if len(markdownVars.title) < 1 { log.Fatal("title can not be empty") return } if len(markdownVars.text) < 1 { log.Fatal("text can not be empty") return } client, err := newClient() if err != nil { log.Fatal(err.Error()) return } msg := dingtalk.NewMarkdownMessage(). SetMarkdown(markdownVars.title, markdownVars.text). SetAt(atMobiles, isAtAll) req, _, err := client.Send(msg) if debug { log.Print(req) } if err != nil { log.Fatal(err.Error()) } } // MarkdownVars struct type MarkdownVars struct { title string text string } var markdownVars MarkdownVars func init() { rootCmd.AddCommand(markdownCmd) markdownCmd.Flags().StringVarP(&markdownVars.title, "title", "i", "", "title") markdownCmd.Flags().StringVarP(&markdownVars.text, "text", "e", "", "text") } ================================================ FILE: cmd/dingtalk/markdown_test.go ================================================ package dingtalk import ( "bytes" "errors" "log" "os" "strings" "testing" "bou.ke/monkey" "github.com/CatchZeng/dingtalk/pkg/dingtalk" "github.com/spf13/cobra" ) func Test_runMarkdownCmd(t *testing.T) { fakeExit := func(int) { log.Print("fake exit") } patch := monkey.Patch(os.Exit, fakeExit) defer patch.Unpatch() t.Run("title is empty", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() runMarkdownCmd(&cobra.Command{}, []string{}) got := buf.String() want := "title can not be empty" if !strings.Contains(got, want) { t.Errorf("runMarkdownCmd() = %v, want %v", got, want) } }) t.Run("text is empty", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() markdownVars.title = "123" runMarkdownCmd(&cobra.Command{}, []string{}) got := buf.String() want := "text can not be empty" if !strings.Contains(got, want) { t.Errorf("runMarkdownCmd() = %v, want %v", got, want) } }) t.Run("new client error", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() markdownVars.title = "123" markdownVars.text = "123" msg := "new client error" monkey.Patch(newClient, func() (*dingtalk.Client, error) { return nil, errors.New(msg) }) defer monkey.Unpatch(newClient) runMarkdownCmd(&cobra.Command{}, []string{}) got := buf.String() if !strings.Contains(got, msg) { t.Errorf("runMarkdownCmd() = %v, want %v", got, msg) } }) t.Run("client send", func(t *testing.T) { markdownVars.title = "123" markdownVars.text = "123" client := &dingtalk.Client{} monkey.Patch(newClient, func() (*dingtalk.Client, error) { return client, nil }) defer monkey.Unpatch(newClient) runMarkdownCmd(&cobra.Command{}, []string{}) }) } ================================================ FILE: cmd/dingtalk/root.go ================================================ package dingtalk import ( "errors" "fmt" "log" "os" "github.com/CatchZeng/dingtalk/configs" "github.com/CatchZeng/dingtalk/pkg/dingtalk" "github.com/spf13/viper" "github.com/spf13/cobra" ) var rootCmd = &cobra.Command{ Use: "dingtalk", Short: "dingtalk is a command line tool for DingTalk", Long: "dingtalk is a command line tool for DingTalk", } // 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() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } func newClient() (*dingtalk.Client, error) { token := getAccessToken() secret := getSecret() if len(token) < 1 { return nil, errors.New("access_token can not be empty") } client := dingtalk.NewClient(token, secret) return client, nil } func getAccessToken() string { if len(accessToken) > 0 { return accessToken } value, err := configs.GetConfig(configs.AccessToken) if err == nil { return value } return "" } func getSecret() string { if len(secret) > 0 { return secret } value, err := configs.GetConfig(configs.Secret) if err == nil { return value } return "" } var accessToken, secret string var isAtAll bool var atMobiles []string var debug bool func init() { cobra.OnInitialize(configs.InitConfig) rootCmd.PersistentFlags().StringVarP(&accessToken, configs.AccessToken, "t", "", configs.AccessToken) rootCmd.PersistentFlags().StringVarP(&secret, configs.Secret, "s", "", configs.Secret) rootCmd.PersistentFlags().BoolVarP(&isAtAll, "isAtAll", "a", false, "isAtAll") rootCmd.PersistentFlags().StringSliceVarP(&atMobiles, "atMobiles", "m", []string{}, "atMobiles") rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "D", false, "debug") if err := viper.BindPFlag(configs.AccessToken, rootCmd.PersistentFlags().Lookup(configs.AccessToken)); err != nil { log.Print(err) } if err := viper.BindPFlag(configs.Secret, rootCmd.PersistentFlags().Lookup(configs.Secret)); err != nil { log.Print(err) } } ================================================ FILE: cmd/dingtalk/root_test.go ================================================ package dingtalk import ( "errors" "log" "os" "testing" "bou.ke/monkey" "github.com/CatchZeng/dingtalk/configs" ) func Test_newClient(t *testing.T) { fakeExit := func(int) { log.Print("fake exit") } patch := monkey.Patch(os.Exit, fakeExit) defer patch.Unpatch() t.Run("getAccessToken return empty", func(t *testing.T) { accessToken = "" _, err := newClient() if err == nil { t.Error("newClient() error") } }) t.Run("getAccessToken return token", func(t *testing.T) { accessToken = "123" client, err := newClient() if err != nil || client == nil { t.Error("newClient() error") } }) } func Test_getAccessToken(t *testing.T) { t.Run("get from accessToken", func(t *testing.T) { accessToken = "123" got := getAccessToken() if got != accessToken { t.Errorf("getAccessToken() = %v, want %v", got, accessToken) } }) t.Run("GetConfig error", func(t *testing.T) { accessToken = "" monkey.Patch(configs.GetConfig, func(key string) (string, error) { return "", errors.New("GetConfig error") }) defer monkey.Unpatch(configs.GetConfig) got := getAccessToken() if got != "" { t.Errorf("getAccessToken() = %v, want %v", got, "") } }) t.Run("get from config", func(t *testing.T) { accessToken = "" want := "123" monkey.Patch(configs.GetConfig, func(key string) (string, error) { return want, nil }) defer monkey.Unpatch(configs.GetConfig) got := getAccessToken() if got != want { t.Errorf("getAccessToken() = %v, want %v", got, want) } }) } func Test_getSecret(t *testing.T) { t.Run("get from secret", func(t *testing.T) { secret = "123" got := getSecret() if got != secret { t.Errorf("getSecret() = %v, want %v", got, secret) } }) t.Run("GetConfig error", func(t *testing.T) { secret = "" monkey.Patch(configs.GetConfig, func(key string) (string, error) { return "", errors.New("GetConfig error") }) defer monkey.Unpatch(configs.GetConfig) got := getSecret() if got != "" { t.Errorf("getSecret() = %v, want %v", got, "") } }) t.Run("get from config", func(t *testing.T) { secret = "" want := "123" monkey.Patch(configs.GetConfig, func(key string) (string, error) { return want, nil }) defer monkey.Unpatch(configs.GetConfig) got := getSecret() if got != want { t.Errorf("getSecret() = %v, want %v", got, want) } }) } ================================================ FILE: cmd/dingtalk/text.go ================================================ package dingtalk import ( "log" "github.com/CatchZeng/dingtalk/pkg/dingtalk" "github.com/spf13/cobra" ) var textCmd = &cobra.Command{ Use: "text", Short: "send text message with DingTalk robot", Long: `send text message with DingTalk robot`, Args: cobra.MinimumNArgs(0), Run: runTextCmd, } func runTextCmd(_ *cobra.Command, _ []string) { if len(textVars.content) < 1 { log.Fatal("content can not be empty") return } client, err := newClient() if err != nil { log.Fatal(err.Error()) return } msg := dingtalk.NewTextMessage(). SetContent(textVars.content). SetAt(atMobiles, isAtAll) req, _, err := client.Send(msg) if debug { log.Print(req) } if err != nil { log.Fatal(err.Error()) } } // TextVars struct type TextVars struct { content string } var textVars TextVars func init() { rootCmd.AddCommand(textCmd) textCmd.Flags().StringVarP(&textVars.content, "content", "c", "", "content") } ================================================ FILE: cmd/dingtalk/text_test.go ================================================ package dingtalk import ( "bytes" "errors" "log" "os" "strings" "testing" "bou.ke/monkey" "github.com/CatchZeng/dingtalk/pkg/dingtalk" "github.com/spf13/cobra" ) func Test_runTextCmd(t *testing.T) { fakeExit := func(int) { log.Print("fake exit") } patch := monkey.Patch(os.Exit, fakeExit) defer patch.Unpatch() t.Run("content is empty", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() runTextCmd(&cobra.Command{}, []string{}) got := buf.String() want := "content can not be empty" if !strings.Contains(got, want) { t.Errorf("runTextCmd() = %v, want %v", got, want) } }) t.Run("new client error", func(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() textVars.content = "123" msg := "new client error" monkey.Patch(newClient, func() (*dingtalk.Client, error) { return nil, errors.New(msg) }) defer monkey.Unpatch(newClient) runTextCmd(&cobra.Command{}, []string{}) got := buf.String() if !strings.Contains(got, msg) { t.Errorf("runTextCmd() = %v, want %v", got, msg) } }) t.Run("client send", func(t *testing.T) { textVars.content = "123" client := &dingtalk.Client{} monkey.Patch(newClient, func() (*dingtalk.Client, error) { return client, nil }) defer monkey.Unpatch(newClient) runTextCmd(&cobra.Command{}, []string{}) }) } ================================================ FILE: cmd/dingtalk/version.go ================================================ package dingtalk import ( "log" v "github.com/CatchZeng/gutils/version" "github.com/spf13/cobra" ) const ( version = "1.5.0" buildTime = "2022/04/20" ) // versionCmd represents the version command var versionCmd = &cobra.Command{ Use: "version", Short: "dingtalk version", Long: `dingtalk version`, Run: runVersionCmd, } func runVersionCmd(_ *cobra.Command, _ []string) { v := v.Stringify(version, buildTime) log.Println(v) } func init() { rootCmd.AddCommand(versionCmd) } ================================================ FILE: cmd/dingtalk/version_test.go ================================================ package dingtalk import ( "bytes" "log" "os" "strings" "testing" v "github.com/CatchZeng/gutils/version" "github.com/spf13/cobra" ) func Test_runVersionCmd(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) defer func() { log.SetOutput(os.Stderr) }() runVersionCmd(&cobra.Command{}, []string{}) got := buf.String() want := v.Stringify(version, buildTime) if !strings.Contains(got, want) { t.Errorf("runVersionCmd() = %v, want %v", got, want) } } ================================================ FILE: configs/app.go ================================================ package configs const ( // AccessToken access token key AccessToken string = "access_token" // Secret secret key Secret string = "secret" ) ================================================ FILE: configs/configs.go ================================================ package configs import ( "log" "os" "path" "strings" "github.com/mitchellh/go-homedir" "github.com/spf13/viper" ) // InitConfig reads in configs file and ENV variables if set. func InitConfig() { // Find home directory. home, err := homedir.Dir() if err != nil { log.Panic(err) } // Search configs in home directory with name ".dingtalk" (without extension). configPath := path.Join(home, ".dingtalk") viper.AddConfigPath(configPath) viper.SetConfigName("config") envPrefix := os.Getenv("DINGTALK_ENV_PREFIX") viper.SetEnvPrefix(envPrefix) viper.AutomaticEnv() // read in environment variables that match // If a configs file is found, read it in. if err := viper.ReadInConfig(); err != nil { return } log.Println("using configs file:", viper.ConfigFileUsed()) } // GetConfig get configs with key func GetConfig(key string) (string, error) { // Check the environment variable envPrefix := os.Getenv("DINGTALK_ENV_PREFIX") envKey := envPrefix + strings.ToUpper(key) result := os.Getenv(envKey) if result != "" { return result, nil } // If a configs file is found, read it in. err := viper.ReadInConfig() if err == nil { return viper.GetString(key), nil } return "", err } ================================================ FILE: configs/configs_test.go ================================================ package configs import ( "errors" "testing" "bou.ke/monkey" "github.com/mitchellh/go-homedir" "github.com/spf13/viper" ) func TestInitConfig(t *testing.T) { t.Run("homedir.Dir() return error", func(t *testing.T) { monkey.Patch(homedir.Dir, func() (string, error) { return "", errors.New("homedir error") }) shouldPanic(t, InitConfig) }) t.Run("viper.ReadInConfig() return error", func(t *testing.T) { monkey.Patch(homedir.Dir, func() (string, error) { return "/catchzeng", nil }) monkey.Patch(viper.ReadInConfig, func() error { return errors.New("ReadInConfig error") }) InitConfig() }) t.Run("viper.ReadInConfig() return nil", func(t *testing.T) { monkey.Patch(homedir.Dir, func() (string, error) { return "/catchzeng", nil }) monkey.Patch(viper.ReadInConfig, func() error { return nil }) InitConfig() }) } func shouldPanic(t *testing.T, f func()) { defer func() { if r := recover(); r == nil { t.Errorf("should have panicked") } }() f() } func TestGetConfig(t *testing.T) { t.Run("viper.ReadInConfig() return error", func(t *testing.T) { monkey.Patch(viper.ReadInConfig, func() error { return errors.New("ReadInConfig error") }) key := "123" if _, err := GetConfig(key); err == nil { t.Error("GetConfig error") } }) t.Run("viper.ReadInConfig() return nil", func(t *testing.T) { monkey.Patch(viper.ReadInConfig, func() error { return nil }) monkey.Patch(viper.GetString, func(key string) string { return key }) key := "456" if value, err := GetConfig(key); err != nil || value != key { t.Error("GetConfig error") } }) } ================================================ FILE: go.mod ================================================ module github.com/CatchZeng/dingtalk go 1.18 require ( bou.ke/monkey v1.0.2 github.com/CatchZeng/gutils v0.1.4 github.com/golang/mock v1.4.4 github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v1.4.0 github.com/spf13/viper v1.11.0 ) require ( github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect github.com/spf13/afero v1.8.2 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.2.0 // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) ================================================ FILE: go.sum ================================================ bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CatchZeng/gutils v0.1.4 h1:Xj1oqB0Rg3F6rymxW1d+ajPl0wHtw2KvDlcquhnCNAc= github.com/CatchZeng/gutils v0.1.4/go.mod h1:Uz8tJTZDM9XWGTFQt3oIYs+zY3/2fOy0TVQaQTgPtWg= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.1/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0= github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 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.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ================================================ FILE: internal/security/security.go ================================================ package security import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "fmt" "io" "math" "net/url" "strconv" "time" ) // https://oapi.dingtalk.com/robot/send?access_token=xxx const dingTalkOAPI = "oapi.dingtalk.com" var dingTalkURL url.URL = url.URL{ Scheme: "https", Host: dingTalkOAPI, Path: "robot/send", } // URL get DingTalk URL with accessToken & secret // If no signature is set, the secret is set to "" // 如果没有加签,secret 设置为 "" 即可 func URL(accessToken string, secret string) (string, error) { timestamp := strconv.FormatInt(time.Now().Unix()*1000, 10) return URLWithTimestamp(timestamp, accessToken, secret) } // URLWithTimestamp get DingTalk URL with timestamp & accessToken & secret func URLWithTimestamp(timestamp string, accessToken string, secret string) (string, error) { dtu := dingTalkURL value := url.Values{} value.Set("access_token", accessToken) if secret == "" { dtu.RawQuery = value.Encode() return dtu.String(), nil } sign, err := sign(timestamp, secret) if err != nil { dtu.RawQuery = value.Encode() return dtu.String(), err } value.Set("timestamp", timestamp) value.Set("sign", sign) dtu.RawQuery = value.Encode() return dtu.String(), nil } // Validate validate // https://ding-doc.dingtalk.com/doc#/serverapi2/elzz1p func Validate(signStr, timestamp, secret string) (bool, error) { t, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return false, err } timeGap := time.Since(time.Unix(t, 0)) if math.Abs(timeGap.Hours()) > 1 { return false, fmt.Errorf("specified timestamp is expired") } ourSign, err := sign(timestamp, secret) if err != nil { return false, err } return ourSign == signStr, nil } func sign(timestamp string, secret string) (string, error) { stringToSign := fmt.Sprintf("%s\n%s", timestamp, secret) h := hmac.New(sha256.New, []byte(secret)) if _, err := io.WriteString(h, stringToSign); err != nil { return "", err } return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil } ================================================ FILE: internal/security/security_test.go ================================================ package security import ( "fmt" "strconv" "testing" "time" "bou.ke/monkey" ) const ( timestamp = "1582163555000" accessToken = "1c53e149ba5de6597ca2442f0e901fd86156780b8ac141e4a75afdc44c85ca4f" secret = "SECb90923e19e58b466481e9e7b7a5b4f108a4531abde590ad3967fb29f0eae5c68" signed = "BQKsG%2BQOCl%2BbYJOLc6pxDHxjVquzlZPWgvRzeN2J5zY%3D" ) func TestURL(t *testing.T) { monkey.Patch(strconv.FormatInt, func(i int64, base int) string { return timestamp }) defer monkey.Unpatch(strconv.FormatInt) got, err := URL(accessToken, secret) if err != nil { t.Errorf("URL() error = %v", err) } want := fmt.Sprintf("https://oapi.dingtalk.com/robot/send?access_token=%v&sign=%v×tamp=%v", accessToken, signed, timestamp) if got != want { t.Errorf("URL() = %v, want %v", got, want) } } func TestURLWithTimestamp(t *testing.T) { type args struct { accessToken string secret string } tests := []struct { name string args args want string wantErr bool }{ { name: "without sign", args: args{ accessToken: accessToken, }, want: fmt.Sprintf("https://oapi.dingtalk.com/robot/send?access_token=%v", accessToken), wantErr: false, }, { name: "with sign", args: args{ accessToken: accessToken, secret: secret, }, want: fmt.Sprintf("https://oapi.dingtalk.com/robot/send?access_token=%v&sign=%v×tamp=%v", accessToken, signed, timestamp), wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := URLWithTimestamp(timestamp, tt.args.accessToken, tt.args.secret) if (err != nil) != tt.wantErr { t.Errorf("URLWithTimestamp() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("URLWithTimestamp() = %v, want %v", got, tt.want) } }) } } func TestValidate(t *testing.T) { validateTimestamp := strconv.FormatInt(time.Now().Add(60*time.Second).Unix(), 10) result, err := sign(validateTimestamp, secret) if err != nil { t.Error(err) } _, err = Validate(result, strconv.FormatInt(time.Now().Add(-3601*time.Second).Unix(), 10), secret) if err == nil { t.Error("this should be err, but not") } _, err = Validate(result, strconv.FormatInt(time.Now().Add(3601*time.Second).Unix(), 10), secret) if err == nil { t.Error("this should be err, but not") } b, err := Validate(result, validateTimestamp, secret) if err != nil { t.Error(err) } else { if !b { t.Error("token is not the same") } } } ================================================ FILE: main.go ================================================ package main import ( "log" "os" "github.com/CatchZeng/dingtalk/cmd/dingtalk" ) func main() { log.SetOutput(os.Stdout) log.SetFlags(0) dingtalk.Execute() } ================================================ FILE: pkg/dingtalk/actionCard.go ================================================ package dingtalk import "encoding/json" // ActionCardMessage struct type ActionCardMessage struct { MsgType MsgType `json:"msgtype"` ActionCard ActionCard `json:"actionCard"` } // ActionCard actionCard struct type ActionCard struct { Title string `json:"title"` Text string `json:"text"` SingleTitle string `json:"singleTitle"` SingleURL string `json:"singleURL"` Btns []Btn `json:"btns"` BtnOrientation string `json:"btnOrientation"` HideAvatar string `json:"hideAvatar"` } // Btn struct type Btn struct { Title string `json:"title"` ActionURL string `json:"actionURL"` } // ToByte to byte func (m *ActionCardMessage) ToByte() ([]byte, error) { m.MsgType = MsgTypeActionCard jsonByte, err := json.Marshal(m) return jsonByte, err } // NewActionCardMessage new message func NewActionCardMessage() *ActionCardMessage { msg := ActionCardMessage{} return &msg } // SetOverallJump set overall jump actionCard func (m *ActionCardMessage) SetOverallJump( title string, text string, singleTitle string, singleURL string, btnOrientation string, hideAvatar string) *ActionCardMessage { m.ActionCard = ActionCard{ Title: title, Text: text, SingleTitle: singleTitle, SingleURL: singleURL, BtnOrientation: btnOrientation, HideAvatar: hideAvatar, } return m } // SetIndependentJump set independent jump actionCard func (m *ActionCardMessage) SetIndependentJump( title string, text string, btns []Btn, btnOrientation string, hideAvatar string) *ActionCardMessage { m.ActionCard = ActionCard{ Title: title, Text: text, Btns: btns, BtnOrientation: btnOrientation, HideAvatar: hideAvatar, } return m } ================================================ FILE: pkg/dingtalk/actionCard_test.go ================================================ package dingtalk import ( "reflect" "testing" ) func TestActionCardMessage_ToByte(t *testing.T) { msg := NewActionCardMessage() _, _ = msg.ToByte() if msg.MsgType != MsgTypeActionCard { t.Errorf("ActionCardMessage.ToByte() type error") } } func TestNewActionCardMessage(t *testing.T) { tests := []struct { name string want *ActionCardMessage }{ { name: "Should return a ActionCardMessage instance", want: &ActionCardMessage{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := NewActionCardMessage(); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewActionCardMessage() = %v, want %v", got, tt.want) } }) } } func TestActionCardMessage_SetOverallJump(t *testing.T) { got := NewActionCardMessage() got.SetOverallJump("title", "text", "singleTitle", "singleURL", "btnOrientation", "hideAvatar") card := ActionCard{ Title: "title", Text: "text", SingleTitle: "singleTitle", SingleURL: "singleURL", BtnOrientation: "btnOrientation", HideAvatar: "hideAvatar", } want := NewActionCardMessage() want.ActionCard = card if !reflect.DeepEqual(got, want) { t.Errorf("SetOverallJump() = %v, want %v", got, want) } } func TestActionCardMessage_SetIndependentJump(t *testing.T) { got := NewActionCardMessage() got.SetIndependentJump("title", "text", []Btn{{ Title: "title", ActionURL: "actionURL", }}, "btnOrientation", "hideAvatar") card := ActionCard{ Title: "title", Text: "text", Btns: []Btn{{ Title: "title", ActionURL: "actionURL", }}, BtnOrientation: "btnOrientation", HideAvatar: "hideAvatar", } want := NewActionCardMessage() want.ActionCard = card if !reflect.DeepEqual(got, want) { t.Errorf("SetIndependentJump() = %v, want %v", got, want) } } ================================================ FILE: pkg/dingtalk/client.go ================================================ package dingtalk import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "time" "github.com/CatchZeng/dingtalk/internal/security" ) // Client dingtalk client type Client struct { AccessToken string Secret string } // NewClient new dingtalk client func NewClient(accessToken, secret string) *Client { return &Client{ AccessToken: accessToken, Secret: secret, } } // Response response struct type Response struct { ErrMsg string `json:"errmsg"` ErrCode int64 `json:"errcode"` } const httpTimoutSecond = time.Duration(30) * time.Second // Send message func (d *Client) Send(message Message) (string, *Response, error) { res := &Response{} reqBytes, err := message.ToByte() if err != nil { return "", res, err } reqString := string(reqBytes) pushURL, err := security.URL(d.AccessToken, d.Secret) if err != nil { return reqString, res, err } req, err := http.NewRequest(http.MethodPost, pushURL, bytes.NewReader(reqBytes)) if err != nil { return reqString, res, err } req.Header.Add("Accept-Charset", "utf8") req.Header.Add("Content-Type", "application/json") client := new(http.Client) client.Timeout = httpTimoutSecond resp, err := client.Do(req) if err != nil { return reqString, res, err } defer resp.Body.Close() resultByte, err := ioutil.ReadAll(resp.Body) if err != nil { return reqString, res, err } err = json.Unmarshal(resultByte, &res) if err != nil { return reqString, res, fmt.Errorf("unmarshal http response body from json error = %v", err) } if res.ErrCode != 0 { return reqString, res, fmt.Errorf("send message to dingtalk error = %s", res.ErrMsg) } return reqString, res, nil } ================================================ FILE: pkg/dingtalk/client_test.go ================================================ package dingtalk import ( "errors" "io" "net/http" "reflect" "testing" "bou.ke/monkey" "github.com/CatchZeng/dingtalk/internal/security" mock_message "github.com/CatchZeng/dingtalk/test/mocks/message" "github.com/golang/mock/gomock" ) func TestNewClient(t *testing.T) { type args struct { accessToken string secret string } tests := []struct { name string args args want *Client }{ { name: "", args: args{ accessToken: "123456", secret: "111111", }, want: &Client{ AccessToken: "123456", Secret: "111111", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := NewClient(tt.args.accessToken, tt.args.secret); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewClient() = %v, want %v", got, tt.want) } }) } } func TestClient_Send(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() message := mock_message.NewMockMessage(ctrl) t.Run("message return error", func(t *testing.T) { c := &Client{} message.EXPECT().ToByte().Return([]byte{}, errors.New("test")) if _, _, err := c.Send(message); err == nil { t.Error("send error") } }) t.Run("security.URL return error", func(t *testing.T) { c := &Client{ AccessToken: "test-access-token", Secret: "test-secret", } message.EXPECT().ToByte().Return([]byte{}, nil) monkey.Patch(security.URL, func(accessToken string, secret string) (string, error) { return "", errors.New("URL error") }) if _, _, err := c.Send(message); err == nil { t.Error("send error") } }) t.Run("http.NewRequest return error", func(t *testing.T) { c := &Client{ AccessToken: "test-access-token", Secret: "test-secret", } message.EXPECT().ToByte().Return([]byte{}, nil) monkey.Patch(security.URL, func(accessToken string, secret string) (string, error) { return "https://oapi.dingtalk.com/robot/send?access_token=ewfewfwfwefwafew", nil }) monkey.Patch(http.NewRequest, func(method, url string, body io.Reader) (*http.Request, error) { return nil, errors.New("NewRequest error") }) if _, _, err := c.Send(message); err == nil { t.Error("send error") } }) } ================================================ FILE: pkg/dingtalk/feedCard.go ================================================ package dingtalk import "encoding/json" // FeedCardMessage feed message struct type FeedCardMessage struct { MsgType MsgType `json:"msgtype"` FeedCard FeedCard `json:"feedCard"` } // FeedCard feedCard struct type FeedCard struct { Links []FeedCardLink `json:"links"` } // FeedCardLink struct type FeedCardLink struct { Title string `json:"title"` PicURL string `json:"picURL"` MessageURL string `json:"messageURL"` } // ToByte to byte func (m *FeedCardMessage) ToByte() ([]byte, error) { m.MsgType = MsgTypeFeedCard jsonByte, err := json.Marshal(m) return jsonByte, err } // NewFeedCardMessage new message func NewFeedCardMessage() *FeedCardMessage { msg := FeedCardMessage{} return &msg } // AppendLink append link func (m *FeedCardMessage) AppendLink( title string, messageURL string, picURL string) *FeedCardMessage { var link = FeedCardLink{ Title: title, MessageURL: messageURL, PicURL: picURL, } m.FeedCard.Links = append(m.FeedCard.Links, link) return m } ================================================ FILE: pkg/dingtalk/feedCard_test.go ================================================ package dingtalk import ( "reflect" "testing" ) func TestFeedCardMessage_ToByte(t *testing.T) { msg := NewFeedCardMessage() _, _ = msg.ToByte() if msg.MsgType != MsgTypeFeedCard { t.Errorf("FeedCardMessage.ToByte() type error") } } func TestNewFeedCardMessage(t *testing.T) { tests := []struct { name string want *FeedCardMessage }{ { name: "Should return a FeedCardMessage instance", want: &FeedCardMessage{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := NewFeedCardMessage(); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewFeedCardMessage() = %v, want %v", got, tt.want) } }) } } func TestFeedCardMessage_AppendLink(t *testing.T) { msg := NewFeedCardMessage() msg.AppendLink("title", "messageURL", "picURL") if len(msg.FeedCard.Links) != 1 { t.Errorf("The number of links after AppendLink should be 1") } msg.AppendLink("title2", "messageURL2", "picURL2") if len(msg.FeedCard.Links) != 2 { t.Errorf("The number of links after AppendLink should be 2") } } ================================================ FILE: pkg/dingtalk/link.go ================================================ package dingtalk import "encoding/json" // LinkMessage link message struct type LinkMessage struct { MsgType MsgType `json:"msgtype"` Link Link `json:"link"` } // Link link struct type Link struct { Title string `json:"title"` Text string `json:"text"` PicURL string `json:"picUrl"` MessageURL string `json:"messageUrl"` } // ToByte to byte func (m *LinkMessage) ToByte() ([]byte, error) { m.MsgType = MsgTypeLink jsonByte, err := json.Marshal(m) return jsonByte, err } // NewLinkMessage new message func NewLinkMessage() *LinkMessage { msg := LinkMessage{} return &msg } // SetLink set link func (m *LinkMessage) SetLink( title string, text string, picURL string, messageURL string) *LinkMessage { m.Link = Link{ Title: title, Text: text, PicURL: picURL, MessageURL: messageURL, } return m } ================================================ FILE: pkg/dingtalk/link_test.go ================================================ package dingtalk import ( "reflect" "testing" ) func TestLinkMessage_ToByte(t *testing.T) { msg := NewLinkMessage() _, _ = msg.ToByte() if msg.MsgType != MsgTypeLink { t.Errorf("LinkMessage.ToByte() type error") } } func TestNewLinkMessage(t *testing.T) { tests := []struct { name string want *LinkMessage }{ { name: "Should return a LinkMessage instance", want: &LinkMessage{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := NewLinkMessage(); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewLinkMessage() = %v, want %v", got, tt.want) } }) } } func TestLinkMessage_SetLink(t *testing.T) { got := NewLinkMessage() got.SetLink("title", "text", "picURL", "messageURL") want := NewLinkMessage() want.Link = Link{ Title: "title", Text: "text", PicURL: "picURL", MessageURL: "messageURL", } if !reflect.DeepEqual(got, want) { t.Errorf("SetLink() = %v, want %v", got, want) } } ================================================ FILE: pkg/dingtalk/markdown.go ================================================ package dingtalk import "encoding/json" // MarkdownMessage markdown message struct type MarkdownMessage struct { MsgType MsgType `json:"msgtype"` Markdown Markdown `json:"markdown"` At At `json:"at"` } // Markdown markdown struct type Markdown struct { Title string `json:"title"` Text string `json:"text"` } // ToByte to byte func (m *MarkdownMessage) ToByte() ([]byte, error) { m.MsgType = MsgTypeMarkdown jsonByte, err := json.Marshal(m) return jsonByte, err } // NewMarkdownMessage new message func NewMarkdownMessage() *MarkdownMessage { msg := MarkdownMessage{} return &msg } // SetMarkdown set markdown func (m *MarkdownMessage) SetMarkdown(title string, text string) *MarkdownMessage { m.Markdown = Markdown{ Title: title, Text: text, } return m } // SetAt set at func (m *MarkdownMessage) SetAt(atMobiles []string, isAtAll bool) *MarkdownMessage { m.At = At{ AtMobiles: atMobiles, IsAtAll: isAtAll, } return m } ================================================ FILE: pkg/dingtalk/markdown_test.go ================================================ package dingtalk import ( "reflect" "testing" ) func TestMarkdownMessage_ToByte(t *testing.T) { msg := NewMarkdownMessage() _, _ = msg.ToByte() if msg.MsgType != MsgTypeMarkdown { t.Errorf("MarkdownMessage.ToByte() type error") } } func TestNewMarkdownMessage(t *testing.T) { tests := []struct { name string want *MarkdownMessage }{ { name: "Should return a MarkdownMessage instance", want: &MarkdownMessage{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := NewMarkdownMessage(); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewMarkdownMessage() = %v, want %v", got, tt.want) } }) } } func TestMarkdownMessage_SetMarkdown(t *testing.T) { got := NewMarkdownMessage() got.SetMarkdown("title", "text") want := NewMarkdownMessage() want.Markdown = Markdown{ Title: "title", Text: "text", } if !reflect.DeepEqual(got, want) { t.Errorf("SetMarkdown() = %v, want %v", got, want) } } func TestMarkdownMessage_SetAt(t *testing.T) { got := NewMarkdownMessage() got.SetAt([]string{"atMobiles"}, false) want := NewMarkdownMessage() want.At = At{ AtMobiles: []string{"atMobiles"}, IsAtAll: false, } if !reflect.DeepEqual(got, want) { t.Errorf("SetMarkdown() = %v, want %v", got, want) } } ================================================ FILE: pkg/dingtalk/message.go ================================================ package dingtalk // MsgType message type enum type MsgType string const ( // MsgTypeText text MsgTypeText MsgType = "text" // MsgTypeMarkdown markdown MsgTypeMarkdown MsgType = "markdown" // MsgTypeLink link MsgTypeLink MsgType = "link" // MsgTypeActionCard actionCard MsgTypeActionCard MsgType = "actionCard" // MsgTypeFeedCard feedCard MsgTypeFeedCard MsgType = "feedCard" ) // Message interface type Message interface { ToByte() ([]byte, error) } // At at struct type At struct { AtMobiles []string `json:"atMobiles"` IsAtAll bool `json:"isAtAll"` } ================================================ FILE: pkg/dingtalk/text.go ================================================ package dingtalk import "encoding/json" // TextMessage text message struct type TextMessage struct { MsgType MsgType `json:"msgtype"` Text Text `json:"text"` At At `json:"at"` } // Text text struct type Text struct { Content string `json:"content"` } // ToByte to byte func (m *TextMessage) ToByte() ([]byte, error) { m.MsgType = MsgTypeText jsonByte, err := json.Marshal(m) return jsonByte, err } // NewTextMessage new message func NewTextMessage() *TextMessage { msg := TextMessage{} return &msg } // SetContent set content func (m *TextMessage) SetContent(content string) *TextMessage { m.Text = Text{ Content: content, } return m } // SetAt set at func (m *TextMessage) SetAt(atMobiles []string, isAtAll bool) *TextMessage { m.At = At{ AtMobiles: atMobiles, IsAtAll: isAtAll, } return m } ================================================ FILE: pkg/dingtalk/text_test.go ================================================ package dingtalk import ( "reflect" "testing" ) func TestTextMessage_ToByte(t *testing.T) { msg := NewTextMessage() _, _ = msg.ToByte() if msg.MsgType != MsgTypeText { t.Errorf("TextMessage.ToByte() type error") } } func TestNewTextMessage(t *testing.T) { tests := []struct { name string want *TextMessage }{ { name: "Should return a TextMessage instance", want: &TextMessage{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := NewTextMessage(); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewTextMessage() = %v, want %v", got, tt.want) } }) } } func TestTextMessage_SetContent(t *testing.T) { got := NewTextMessage() got.SetContent("content") want := NewTextMessage() want.Text = Text{ Content: "content", } if !reflect.DeepEqual(got, want) { t.Errorf("SetContent() = %v, want %v", got, want) } } func TestTextMessage_SetAt(t *testing.T) { got := NewTextMessage() got.SetAt([]string{"atMobiles"}, false) want := NewTextMessage() want.At = At{ AtMobiles: []string{"atMobiles"}, IsAtAll: false, } if !reflect.DeepEqual(got, want) { t.Errorf("SetMarkdown() = %v, want %v", got, want) } } ================================================ FILE: scripts/mock.sh ================================================ #!/bin/bash shell_dir=$(dirname $0) cd ${shell_dir} cd .. rm -rf test/mocks mkdir -p test/mocks/message # open with vscode if which mockgen >/dev/null; then echo "mockgen has installed in PATH" else echo "warning: 'mockgen' command has not installed in PATH" GO111MODULE=on go get github.com/golang/mock/mockgen@v1.4.3 fi mockgen -package=mock_message -source=message.go > test/mocks/message/message.go ================================================ FILE: scripts/test.sh ================================================ #!/bin/bash shell_dir=$(dirname $0) cd ${shell_dir} cd .. set -e echo "" > coverage.txt for d in $(go list ./... | grep -v vendor); do go test -gcflags=-l -race -coverprofile=profile.out -covermode=atomic $d if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi done ================================================ FILE: sonar-project.properties ================================================ # must be unique in a given SonarQube instance sonar.projectKey=dingtalk # --- optional properties --- # defaults to project key #sonar.projectName=My project # defaults to 'not provided' sonar.projectVersion=1.0 # Path is relative to the sonar-project.properties file. Defaults to . sonar.sources=. # Encoding of the source code. Default is default system encoding sonar.sourceEncoding=UTF-8 sonar.exclusions=**/proto/** sonar.language=go sonar.tests=. sonar.test.inclusions=**/*_test.go sonar.test.exclusions=**/vendor/**,**/proto/** sonar.go.coverage.reportPaths=coverage.data Dsonar.coverage.dtdVerification=false ================================================ FILE: test/mocks/message/message.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: message.go // Package mock_message is a generated GoMock package. package mock_message import ( reflect "reflect" gomock "github.com/golang/mock/gomock" ) // MockMessage is a mock of Message interface type MockMessage struct { ctrl *gomock.Controller recorder *MockMessageMockRecorder } // MockMessageMockRecorder is the mock recorder for MockMessage type MockMessageMockRecorder struct { mock *MockMessage } // NewMockMessage creates a new mock instance func NewMockMessage(ctrl *gomock.Controller) *MockMessage { mock := &MockMessage{ctrl: ctrl} mock.recorder = &MockMessageMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use func (m *MockMessage) EXPECT() *MockMessageMockRecorder { return m.recorder } // ToByte mocks base method func (m *MockMessage) ToByte() ([]byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ToByte") ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } // ToByte indicates an expected call of ToByte func (mr *MockMessageMockRecorder) ToByte() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToByte", reflect.TypeOf((*MockMessage)(nil).ToByte)) }