From 8c3052f2ae68d992dd2812ecb2eaa7199f3f8d98 Mon Sep 17 00:00:00 2001 From: Zhao Xiaojie Date: Thu, 29 Aug 2019 20:54:48 +0800 Subject: [PATCH] Add support to download Jenkins (#129) * Add support to download Jenkins * Add test cases for http downloader * Exclued mock files * Exclued mock files * Add test cases for center download * Remove mock files * Fix gen-mock * Add init stage for building * Add test case for jenkins upgrade cmd * Add test casee for center watch cmd * Add test case for center cmd --- .gitignore | 2 + .travis.yml | 2 +- Jenkinsfile | 9 ++ Makefile | 7 ++ app/cmd/center.go | 22 +++-- app/cmd/center_download.go | 42 ++++++++ app/cmd/center_download_test.go | 65 ++++++++++++ app/cmd/center_test.go | 77 +++++++++++++++ app/cmd/center_upgrade.go | 14 ++- app/cmd/center_upgrade_test.go | 75 ++++++++++++++ app/cmd/center_watch.go | 10 +- app/cmd/center_watch_test.go | 66 +++++++++++++ app/cmd/common.go | 7 ++ app/cmd/config.go | 7 +- app/cmd/root.go | 28 ++++-- client/common.go | 40 +++++--- client/updateCenter.go | 21 ++++ client/updateCenter_test.go | 120 +++++++++++++++++++++++ mock/mhttp/roundtripper.go | 49 ++++++++++ sonar-project.properties | 2 +- util/http.go | 12 ++- util/http_test.go | 168 ++++++++++++++++++++++++++++++++ 22 files changed, 804 insertions(+), 41 deletions(-) create mode 100644 app/cmd/center_download.go create mode 100644 app/cmd/center_download_test.go create mode 100644 app/cmd/center_test.go create mode 100644 app/cmd/center_upgrade_test.go create mode 100644 app/cmd/center_watch_test.go create mode 100644 client/updateCenter_test.go create mode 100644 mock/mhttp/roundtripper.go create mode 100644 util/http_test.go diff --git a/.gitignore b/.gitignore index 36cfbb17..d9ff0916 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ bin/ release/ *.xml + +mock diff --git a/.travis.yml b/.travis.yml index a159b6ba..753c0463 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ addons: script: - make clean # Execute some tests - - make test + - make init test # And finally run the SonarQube analysis - read the "sonar-project.properties" # file to see the specific configuration - curl -LsS https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.0.0.1744-linux.zip > sonar-scanner-cli-4.0.0.1744-linux.zip diff --git a/Jenkinsfile b/Jenkinsfile index 9f8346ae..80c8008a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,6 +6,15 @@ pipeline { } stages { + stage('Init') { + steps { + script { + entry.container_x('golang', 'go version'){ + sh label: 'make init', script: 'make init' + } + } + } + } stage('Build') { parallel { stage('MacOS') { diff --git a/Makefile b/Makefile index c4ac8cfb..eb585464 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,13 @@ VERSION := dev-$(shell git describe --tags $(shell git rev-list --tags --max-cou BUILDFLAGS = -ldflags "-X github.com/jenkins-zh/jenkins-cli/app.version=$(VERSION) -X github.com/jenkins-zh/jenkins-cli/app.commit=$(COMMIT)" COVERED_MAIN_SRC_FILE=./main +gen-mock: + go get github.com/golang/mock/gomock + go install github.com/golang/mock/mockgen + mockgen -destination ./mock/mhttp/roundtripper.go -package mhttp net/http RoundTripper + +init: gen-mock + darwin: ## Build for OSX GO111MODULE=on CGO_ENABLED=$(CGO_ENABLED) GOOS=darwin GOARCH=amd64 $(GO) $(BUILD_TARGET) $(BUILDFLAGS) -o bin/darwin/$(NAME) $(MAIN_SRC_FILE) chmod +x bin/darwin/$(NAME) diff --git a/app/cmd/center.go b/app/cmd/center.go index 00dc8127..c03cf22b 100644 --- a/app/cmd/center.go +++ b/app/cmd/center.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "log" + "net/http" "github.com/jenkins-zh/jenkins-cli/client" "github.com/spf13/cobra" @@ -11,6 +12,7 @@ import ( type CenterOption struct { WatchOption + RoundTripper http.RoundTripper CeneterStatus string } @@ -26,14 +28,18 @@ var centerCmd = &cobra.Command{ Long: `Manage your update center`, Run: func(_ *cobra.Command, _ []string) { jenkins := getCurrentJenkinsFromOptionsOrDie() - printJenkinsStatus(jenkins) + printJenkinsStatus(jenkins, centerOption.RoundTripper) - printUpdateCenter(jenkins) + printUpdateCenter(jenkins, centerOption.RoundTripper) }, } -func printUpdateCenter(jenkins *JenkinsServer) { - jclient := &client.UpdateCenterManager{} +func printUpdateCenter(jenkins *JenkinsServer, roundTripper http.RoundTripper) { + jclient := &client.UpdateCenterManager{ + JenkinsCore: client.JenkinsCore{ + RoundTripper: roundTripper, + }, + } jclient.URL = jenkins.URL jclient.UserName = jenkins.UserName jclient.Token = jenkins.Token @@ -63,8 +69,12 @@ func printUpdateCenter(jenkins *JenkinsServer) { } } -func printJenkinsStatus(jenkins *JenkinsServer) { - jclient := &client.JenkinsStatusClient{} +func printJenkinsStatus(jenkins *JenkinsServer, roundTripper http.RoundTripper) { + jclient := &client.JenkinsStatusClient{ + JenkinsCore: client.JenkinsCore{ + RoundTripper: roundTripper, + }, + } jclient.URL = jenkins.URL jclient.UserName = jenkins.UserName jclient.Token = jenkins.Token diff --git a/app/cmd/center_download.go b/app/cmd/center_download.go new file mode 100644 index 00000000..104b33ce --- /dev/null +++ b/app/cmd/center_download.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "log" + "net/http" + + "github.com/jenkins-zh/jenkins-cli/client" + "github.com/spf13/cobra" +) + +// CenterDownloadOption as the options of download command +type CenterDownloadOption struct { + LTS bool + Output string + + RoundTripper http.RoundTripper +} + +var centerDownloadOption CenterDownloadOption + +func init() { + centerCmd.AddCommand(centerDownloadCmd) + centerDownloadCmd.Flags().BoolVarP(¢erDownloadOption.LTS, "lts", "", true, "If you want to download Jenkins as LTS") + centerDownloadCmd.Flags().StringVarP(¢erDownloadOption.Output, "output", "o", "jenkins.war", "The file of output") +} + +var centerDownloadCmd = &cobra.Command{ + Use: "download", + Short: "Download Jenkins", + Long: `Download Jenkins`, + Run: func(_ *cobra.Command, _ []string) { + jclient := &client.UpdateCenterManager{ + JenkinsCore: client.JenkinsCore{ + RoundTripper: centerDownloadOption.RoundTripper, + }, + } + + if err := jclient.DownloadJenkins(centerDownloadOption.LTS, centerDownloadOption.Output); err != nil { + log.Fatal(err) + } + }, +} diff --git a/app/cmd/center_download_test.go b/app/cmd/center_download_test.go new file mode 100644 index 00000000..e0f70aed --- /dev/null +++ b/app/cmd/center_download_test.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "bytes" + "io/ioutil" + "net/http" + "os" + + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/jenkins-zh/jenkins-cli/mock/mhttp" +) + +var _ = Describe("center download command", func() { + var ( + ctrl *gomock.Controller + roundTripper *mhttp.MockRoundTripper + targetFilePath string + responseBody string + ) + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + roundTripper = mhttp.NewMockRoundTripper(ctrl) + targetFilePath = "jenkins.war" + responseBody = "fake response" + }) + + AfterEach(func() { + rootCmd.SetArgs([]string{}) + os.Remove(targetFilePath) + ctrl.Finish() + }) + + Context("basic cases", func() { + It("should success", func() { + centerDownloadOption.RoundTripper = roundTripper + centerDownloadOption.LTS = false + + request, _ := http.NewRequest("GET", "http://mirrors.jenkins.io/war/latest/jenkins.war", nil) + response := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Header: http.Header{}, + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString(responseBody)), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) + + rootCmd.SetArgs([]string{"center", "download"}) + _, err := rootCmd.ExecuteC() + Expect(err).To(BeNil()) + + _, err = os.Stat(targetFilePath) + Expect(err).To(BeNil()) + + content, readErr := ioutil.ReadFile(targetFilePath) + Expect(readErr).To(BeNil()) + Expect(string(content)).To(Equal(responseBody)) + }) + }) +}) diff --git a/app/cmd/center_test.go b/app/cmd/center_test.go new file mode 100644 index 00000000..b8b697e8 --- /dev/null +++ b/app/cmd/center_test.go @@ -0,0 +1,77 @@ +package cmd + +import ( + "bytes" + "io/ioutil" + "net/http" + "os" + + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/jenkins-zh/jenkins-cli/mock/mhttp" +) + +var _ = Describe("center command", func() { + var ( + ctrl *gomock.Controller + roundTripper *mhttp.MockRoundTripper + ) + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + roundTripper = mhttp.NewMockRoundTripper(ctrl) + rootCmd.SetArgs([]string{}) + rootOptions.Jenkins = "" + rootOptions.ConfigFile = "test.yaml" + + centerOption.RoundTripper = roundTripper + }) + + AfterEach(func() { + rootCmd.SetArgs([]string{}) + os.Remove(rootOptions.ConfigFile) + rootOptions.ConfigFile = "" + ctrl.Finish() + }) + + Context("basic cases", func() { + It("should success", func() { + data, err := generateSampleConfig() + Expect(err).To(BeNil()) + err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) + Expect(err).To(BeNil()) + + requestCrumb, _ := http.NewRequest("GET", "http://localhost:8080/jenkins/api/json", nil) + requestCrumb.SetBasicAuth("admin", "111e3a2f0231198855dceaff96f20540a9") + responseCrumb := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Request: requestCrumb, + Body: ioutil.NopCloser(bytes.NewBufferString(` + {"version":"0"} + `)), + } + roundTripper.EXPECT(). + RoundTrip(requestCrumb).Return(responseCrumb, nil) + + request, _ := http.NewRequest("GET", "http://localhost:8080/jenkins/updateCenter/api/json?pretty=false&depth=1", nil) + request.SetBasicAuth("admin", "111e3a2f0231198855dceaff96f20540a9") + response := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString(` + {"RestartRequiredForCompletion":true} + `)), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) + + rootCmd.SetArgs([]string{"center"}) + _, err = rootCmd.ExecuteC() + Expect(err).To(BeNil()) + }) + }) +}) diff --git a/app/cmd/center_upgrade.go b/app/cmd/center_upgrade.go index 76c18c96..53e407d1 100644 --- a/app/cmd/center_upgrade.go +++ b/app/cmd/center_upgrade.go @@ -2,11 +2,19 @@ package cmd import ( "log" + "net/http" "github.com/jenkins-zh/jenkins-cli/client" "github.com/spf13/cobra" ) +// CenterUpgradeOption option for upgrade Jenkins +type CenterUpgradeOption struct { + RoundTripper http.RoundTripper +} + +var centerUpgradeOption CenterUpgradeOption + func init() { centerCmd.AddCommand(centerUpgradeCmd) } @@ -18,7 +26,11 @@ var centerUpgradeCmd = &cobra.Command{ Run: func(_ *cobra.Command, _ []string) { jenkins := getCurrentJenkinsFromOptionsOrDie() - jclient := &client.UpdateCenterManager{} + jclient := &client.UpdateCenterManager{ + JenkinsCore: client.JenkinsCore{ + RoundTripper: centerUpgradeOption.RoundTripper, + }, + } jclient.URL = jenkins.URL jclient.UserName = jenkins.UserName jclient.Token = jenkins.Token diff --git a/app/cmd/center_upgrade_test.go b/app/cmd/center_upgrade_test.go new file mode 100644 index 00000000..4b2fb501 --- /dev/null +++ b/app/cmd/center_upgrade_test.go @@ -0,0 +1,75 @@ +package cmd + +import ( + "bytes" + "io/ioutil" + "net/http" + "os" + + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/jenkins-zh/jenkins-cli/mock/mhttp" +) + +var _ = Describe("center upgrade command", func() { + var ( + ctrl *gomock.Controller + roundTripper *mhttp.MockRoundTripper + ) + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + roundTripper = mhttp.NewMockRoundTripper(ctrl) + centerUpgradeOption.RoundTripper = roundTripper + rootCmd.SetArgs([]string{}) + rootOptions.Jenkins = "" + rootOptions.ConfigFile = "test.yaml" + }) + + AfterEach(func() { + rootCmd.SetArgs([]string{}) + os.Remove(rootOptions.ConfigFile) + rootOptions.ConfigFile = "" + ctrl.Finish() + }) + + Context("basic cases", func() { + It("should success", func() { + data, err := generateSampleConfig() + Expect(err).To(BeNil()) + err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) + Expect(err).To(BeNil()) + + requestCrumb, _ := http.NewRequest("GET", "http://localhost:8080/jenkins/crumbIssuer/api/json", nil) + requestCrumb.SetBasicAuth("admin", "111e3a2f0231198855dceaff96f20540a9") + responseCrumb := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Request: requestCrumb, + Body: ioutil.NopCloser(bytes.NewBufferString(` + {"crumbRequestField":"CrumbRequestField","crumb":"Crumb"} + `)), + } + roundTripper.EXPECT(). + RoundTrip(requestCrumb).Return(responseCrumb, nil) + + request, _ := http.NewRequest("POST", "http://localhost:8080/jenkins/updateCenter/upgrade", nil) + request.SetBasicAuth("admin", "111e3a2f0231198855dceaff96f20540a9") + request.Header.Add("CrumbRequestField", "Crumb") + response := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString("")), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) + + rootCmd.SetArgs([]string{"center", "upgrade"}) + _, err = rootCmd.ExecuteC() + Expect(err).To(BeNil()) + }) + }) +}) diff --git a/app/cmd/center_watch.go b/app/cmd/center_watch.go index db0edc0b..045cd18c 100644 --- a/app/cmd/center_watch.go +++ b/app/cmd/center_watch.go @@ -1,6 +1,7 @@ package cmd import ( + "net/http" "time" "github.com/spf13/cobra" @@ -10,6 +11,7 @@ import ( type CenterWatchOption struct { WatchOption + RoundTripper http.RoundTripper CeneterStatus string } @@ -17,7 +19,7 @@ var centerWatchOption CenterWatchOption func init() { centerCmd.AddCommand(centerWatchCmd) - centerWatchCmd.Flags().IntVarP(¢erWatchOption.Interval, "interval", "i", 1, "Interval of watch") + centerWatchOption.SetFlag(centerWatchCmd) } var centerWatchCmd = &cobra.Command{ @@ -26,10 +28,10 @@ var centerWatchCmd = &cobra.Command{ Long: `Watch your update center status`, Run: func(_ *cobra.Command, _ []string) { jenkins := getCurrentJenkinsFromOptionsOrDie() - printJenkinsStatus(jenkins) + printJenkinsStatus(jenkins, centerWatchOption.RoundTripper) - for { - printUpdateCenter(jenkins) + for ; centerWatchOption.Count >= 0; centerWatchOption.Count-- { + printUpdateCenter(jenkins, centerOption.RoundTripper) time.Sleep(time.Duration(centerOption.Interval) * time.Second) } diff --git a/app/cmd/center_watch_test.go b/app/cmd/center_watch_test.go new file mode 100644 index 00000000..0ccd9164 --- /dev/null +++ b/app/cmd/center_watch_test.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "bytes" + "io/ioutil" + "net/http" + "os" + + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/jenkins-zh/jenkins-cli/mock/mhttp" +) + +var _ = Describe("center watch command", func() { + var ( + ctrl *gomock.Controller + roundTripper *mhttp.MockRoundTripper + ) + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + roundTripper = mhttp.NewMockRoundTripper(ctrl) + rootCmd.SetArgs([]string{}) + rootOptions.Jenkins = "" + rootOptions.ConfigFile = "test.yaml" + + centerWatchOption.WatchOption.Count = -1 + centerWatchOption.RoundTripper = roundTripper + centerOption.RoundTripper = roundTripper + }) + + AfterEach(func() { + rootCmd.SetArgs([]string{}) + os.Remove(rootOptions.ConfigFile) + rootOptions.ConfigFile = "" + ctrl.Finish() + }) + + Context("basic cases", func() { + It("should success", func() { + data, err := generateSampleConfig() + Expect(err).To(BeNil()) + err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) + Expect(err).To(BeNil()) + + requestCrumb, _ := http.NewRequest("GET", "http://localhost:8080/jenkins/api/json", nil) + requestCrumb.SetBasicAuth("admin", "111e3a2f0231198855dceaff96f20540a9") + responseCrumb := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Request: requestCrumb, + Body: ioutil.NopCloser(bytes.NewBufferString(` + {"version":"0"} + `)), + } + roundTripper.EXPECT(). + RoundTrip(requestCrumb).Return(responseCrumb, nil) + + rootCmd.SetArgs([]string{"center", "watch"}) + _, err = rootCmd.ExecuteC() + Expect(err).To(BeNil()) + }) + }) +}) diff --git a/app/cmd/common.go b/app/cmd/common.go index f16a562b..b1a05a47 100644 --- a/app/cmd/common.go +++ b/app/cmd/common.go @@ -78,6 +78,13 @@ func (b *BatchOption) SetFlag(cmd *cobra.Command) { type WatchOption struct { Watch bool Interval int + Count int +} + +// SetFlag for WatchOption +func (o *WatchOption) SetFlag(cmd *cobra.Command) { + cmd.Flags().IntVarP(&o.Interval, "interval", "i", 1, "Interval of watch") + cmd.Flags().IntVarP(&o.Interval, "count", "", 9999, "Count of watch") } // InteractiveOption allow user to choose whether the mode is interactive diff --git a/app/cmd/config.go b/app/cmd/config.go index 7bd56772..43340692 100644 --- a/app/cmd/config.go +++ b/app/cmd/config.go @@ -105,8 +105,11 @@ func getJenkinsNames() []string { func getCurrentJenkins() (jenkinsServer *JenkinsServer) { config := getConfig() - current := config.Current - jenkinsServer = findJenkinsByName(current) + + if config != nil { + current := config.Current + jenkinsServer = findJenkinsByName(current) + } return } diff --git a/app/cmd/root.go b/app/cmd/root.go index c3f13fbe..cdaf90a0 100644 --- a/app/cmd/root.go +++ b/app/cmd/root.go @@ -14,9 +14,10 @@ import ( // RootOptions is a global option for whole cli type RootOptions struct { - Jenkins string - Version bool - Debug bool + ConfigFile string + Jenkins string + Version bool + Debug bool } var rootCmd = &cobra.Command{ @@ -53,20 +54,31 @@ var rootOptions RootOptions func init() { cobra.OnInitialize(initConfig) + rootCmd.PersistentFlags().StringVarP(&rootOptions.ConfigFile, "configFile", "", "", "An alternative config file") rootCmd.PersistentFlags().StringVarP(&rootOptions.Jenkins, "jenkins", "j", "", "Select a Jenkins server for this time") rootCmd.PersistentFlags().BoolVarP(&rootOptions.Version, "version", "v", false, "Print the version of Jenkins CLI") rootCmd.PersistentFlags().BoolVarP(&rootOptions.Debug, "debug", "", false, "Print the output into debug.html") } func initConfig() { - if err := loadDefaultConfig(); err != nil { - if os.IsNotExist(err) { - log.Printf("No config file found.") - return + if rootOptions.ConfigFile == "" { + if err := loadDefaultConfig(); err != nil { + configLoadErrorHandle(err) + } + } else { + if err := loadConfig(rootOptions.ConfigFile); err != nil { + configLoadErrorHandle(err) } + } +} - log.Fatalf("Config file is invalid: %v", err) +func configLoadErrorHandle(err error) { + if os.IsNotExist(err) { + log.Printf("No config file found.") + return } + + log.Fatalf("Config file is invalid: %v", err) } func getCurrentJenkinsFromOptions() (jenkinsServer *JenkinsServer) { diff --git a/client/common.go b/client/common.go index c1f51b95..0325a776 100644 --- a/client/common.go +++ b/client/common.go @@ -19,7 +19,8 @@ type JenkinsCore struct { Proxy string ProxyAuth string - Debug bool + Debug bool + RoundTripper http.RoundTripper } type JenkinsCrumb struct { @@ -28,23 +29,29 @@ type JenkinsCrumb struct { } func (j *JenkinsCore) GetClient() (client *http.Client) { - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - if j.Proxy != "" { - if proxyURL, err := url.Parse(j.Proxy); err == nil { - tr.Proxy = http.ProxyURL(proxyURL) - } else { - log.Fatal(err) + var roundTripper http.RoundTripper + if j.RoundTripper != nil { + roundTripper = j.RoundTripper + } else { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } + if j.Proxy != "" { + if proxyURL, err := url.Parse(j.Proxy); err == nil { + tr.Proxy = http.ProxyURL(proxyURL) + } else { + log.Fatal(err) + } - if j.ProxyAuth != "" { - basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(j.ProxyAuth)) - tr.ProxyConnectHeader = http.Header{} - tr.ProxyConnectHeader.Add("Proxy-Authorization", basicAuth) + if j.ProxyAuth != "" { + basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(j.ProxyAuth)) + tr.ProxyConnectHeader = http.Header{} + tr.ProxyConnectHeader.Add("Proxy-Authorization", basicAuth) + } } + roundTripper = tr } - client = &http.Client{Transport: tr} + client = &http.Client{Transport: roundTripper} return } @@ -56,7 +63,10 @@ func (j *JenkinsCore) ProxyHandle(request *http.Request) { } func (j *JenkinsCore) AuthHandle(request *http.Request) (err error) { - request.SetBasicAuth(j.UserName, j.Token) + if j.UserName != "" && j.Token != "" { + request.SetBasicAuth(j.UserName, j.Token) + } + j.ProxyHandle(request) if request.Method == "POST" { diff --git a/client/updateCenter.go b/client/updateCenter.go index d284cc61..9df75928 100644 --- a/client/updateCenter.go +++ b/client/updateCenter.go @@ -6,6 +6,8 @@ import ( "io/ioutil" "log" "net/http" + + "github.com/jenkins-zh/jenkins-cli/util" ) // UpdateCenterManager manages the UpdateCenter @@ -132,3 +134,22 @@ func (u *UpdateCenterManager) Upgrade() (err error) { } return } + +// DownloadJenkins download Jenkins +func (u *UpdateCenterManager) DownloadJenkins(lts bool, output string) (err error) { + var url string + if lts { + url = "http://mirrors.jenkins.io/war-stable/latest/jenkins.war" + } else { + url = "http://mirrors.jenkins.io/war/latest/jenkins.war" + } + + downloader := util.HTTPDownloader{ + RoundTripper: u.RoundTripper, + TargetFilePath: output, + URL: url, + ShowProgress: true, + } + err = downloader.DownloadFile() + return +} diff --git a/client/updateCenter_test.go b/client/updateCenter_test.go new file mode 100644 index 00000000..bb333fab --- /dev/null +++ b/client/updateCenter_test.go @@ -0,0 +1,120 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "os" + + "github.com/golang/mock/gomock" + "github.com/jenkins-zh/jenkins-cli/mock/mhttp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("update center test", func() { + var ( + ctrl *gomock.Controller + manager *UpdateCenterManager + roundTripper *mhttp.MockRoundTripper + responseBody string + donwloadFile string + ) + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + manager = &UpdateCenterManager{} + roundTripper = mhttp.NewMockRoundTripper(ctrl) + responseBody = "fake response" + donwloadFile = "downloadfile.log" + }) + + AfterEach(func() { + os.Remove(donwloadFile) + ctrl.Finish() + }) + + Context("DownloadJenkins", func() { + It("should success with basic cases", func() { + manager.RoundTripper = roundTripper + + request, _ := http.NewRequest("GET", "http://mirrors.jenkins.io/war/latest/jenkins.war", nil) + response := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Header: http.Header{}, + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString(responseBody)), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) + err := manager.DownloadJenkins(false, donwloadFile) + Expect(err).To(BeNil()) + + _, err = os.Stat(donwloadFile) + Expect(err).To(BeNil()) + + content, readErr := ioutil.ReadFile(donwloadFile) + Expect(readErr).To(BeNil()) + Expect(string(content)).To(Equal(responseBody)) + }) + }) + + Context("Upgrade", func() { + It("basic cases", func() { + manager.RoundTripper = roundTripper + manager.URL = "" + + requestCrumb, _ := http.NewRequest("GET", fmt.Sprintf("%s/crumbIssuer/api/json", ""), nil) + responseCrumb := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Request: requestCrumb, + Body: ioutil.NopCloser(bytes.NewBufferString(` + {"crumbRequestField":"CrumbRequestField","crumb":"Crumb"} + `)), + } + roundTripper.EXPECT(). + RoundTrip(requestCrumb).Return(responseCrumb, nil) + + request, _ := http.NewRequest("POST", "/updateCenter/upgrade", nil) + request.Header.Add("CrumbRequestField", "Crumb") + response := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString("")), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) + + err := manager.Upgrade() + Expect(err).To(BeNil()) + }) + }) + + Context("Status", func() { + It("should success", func() { + manager.RoundTripper = roundTripper + manager.URL = "" + + request, _ := http.NewRequest("GET", "/updateCenter/api/json?pretty=false&depth=1", nil) + response := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString(` + {"RestartRequiredForCompletion": true} + `)), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) + + status, err := manager.Status() + Expect(err).To(BeNil()) + Expect(status).NotTo(BeNil()) + Expect(status.RestartRequiredForCompletion).Should(BeTrue()) + }) + }) +}) diff --git a/mock/mhttp/roundtripper.go b/mock/mhttp/roundtripper.go new file mode 100644 index 00000000..c6a347c8 --- /dev/null +++ b/mock/mhttp/roundtripper.go @@ -0,0 +1,49 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: net/http (interfaces: RoundTripper) + +// Package mhttp is a generated GoMock package. +package mhttp + +import ( + gomock "github.com/golang/mock/gomock" + http "net/http" + reflect "reflect" +) + +// MockRoundTripper is a mock of RoundTripper interface +type MockRoundTripper struct { + ctrl *gomock.Controller + recorder *MockRoundTripperMockRecorder +} + +// MockRoundTripperMockRecorder is the mock recorder for MockRoundTripper +type MockRoundTripperMockRecorder struct { + mock *MockRoundTripper +} + +// NewMockRoundTripper creates a new mock instance +func NewMockRoundTripper(ctrl *gomock.Controller) *MockRoundTripper { + mock := &MockRoundTripper{ctrl: ctrl} + mock.recorder = &MockRoundTripperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockRoundTripper) EXPECT() *MockRoundTripperMockRecorder { + return m.recorder +} + +// RoundTrip mocks base method +func (m *MockRoundTripper) RoundTrip(arg0 *http.Request) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RoundTrip", arg0) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RoundTrip indicates an expected call of RoundTrip +func (mr *MockRoundTripperMockRecorder) RoundTrip(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RoundTrip", reflect.TypeOf((*MockRoundTripper)(nil).RoundTrip), arg0) +} diff --git a/sonar-project.properties b/sonar-project.properties index 025f1361..568e70ee 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -10,4 +10,4 @@ sonar.sources=. # Encoding of the source code. Default is default system encoding #sonar.sourceEncoding=UTF-8 -sonar.go.exclusions=**/vendor/**,**/**/*_test.go +sonar.go.exclusions=**/vendor/**,**/**/*_test.go,mock/**/** diff --git a/util/http.go b/util/http.go index 2b3e1c37..573d9cc3 100644 --- a/util/http.go +++ b/util/http.go @@ -25,7 +25,8 @@ type HTTPDownloader struct { UserName string Password string - Debug bool + Debug bool + RoundTripper http.RoundTripper } // DownloadFile download a file with the progress @@ -40,8 +41,13 @@ func (h *HTTPDownloader) DownloadFile() error { if h.UserName != "" && h.Password != "" { req.SetBasicAuth(h.UserName, h.Password) } - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + var tr http.RoundTripper + if h.RoundTripper != nil { + tr = h.RoundTripper + } else { + tr = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } } client := &http.Client{Transport: tr} resp, err := client.Do(req) diff --git a/util/http_test.go b/util/http_test.go new file mode 100644 index 00000000..a71ee53a --- /dev/null +++ b/util/http_test.go @@ -0,0 +1,168 @@ +package util + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "os" + + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/jenkins-zh/jenkins-cli/mock/mhttp" +) + +var _ = Describe("http test", func() { + var ( + ctrl *gomock.Controller + roundTripper *mhttp.MockRoundTripper + downloader HTTPDownloader + targetFilePath string + responseBody string + ) + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + roundTripper = mhttp.NewMockRoundTripper(ctrl) + targetFilePath = "test.log" + downloader = HTTPDownloader{ + TargetFilePath: targetFilePath, + RoundTripper: roundTripper, + } + responseBody = "fake body" + }) + + AfterEach(func() { + os.Remove(targetFilePath) + ctrl.Finish() + }) + + Context("DownloadFile", func() { + It("no progress indication", func() { + request, _ := http.NewRequest("GET", "", nil) + response := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Header: http.Header{}, + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString(responseBody)), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) + err := downloader.DownloadFile() + Expect(err).To(BeNil()) + + _, err = os.Stat(targetFilePath) + Expect(err).To(BeNil()) + + content, readErr := ioutil.ReadFile(targetFilePath) + Expect(readErr).To(BeNil()) + Expect(string(content)).To(Equal(responseBody)) + return + }) + + It("with BasicAuth", func() { + downloader = HTTPDownloader{ + TargetFilePath: targetFilePath, + RoundTripper: roundTripper, + UserName: "UserName", + Password: "Password", + } + + request, _ := http.NewRequest("GET", "", nil) + request.SetBasicAuth(downloader.UserName, downloader.Password) + response := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Header: http.Header{}, + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString(responseBody)), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) + err := downloader.DownloadFile() + Expect(err).To(BeNil()) + + _, err = os.Stat(targetFilePath) + Expect(err).To(BeNil()) + + content, readErr := ioutil.ReadFile(targetFilePath) + Expect(readErr).To(BeNil()) + Expect(string(content)).To(Equal(responseBody)) + return + }) + + It("with error request", func() { + downloader = HTTPDownloader{ + URL: "fake url", + } + err := downloader.DownloadFile() + Expect(err).To(HaveOccurred()) + }) + + It("with error response", func() { + downloader = HTTPDownloader{ + RoundTripper: roundTripper, + } + + request, _ := http.NewRequest("GET", "", nil) + response := &http.Response{} + roundTripper.EXPECT(). + RoundTrip(request).Return(response, fmt.Errorf("fake error")) + err := downloader.DownloadFile() + Expect(err).To(HaveOccurred()) + }) + + It("status code isn't 200", func() { + downloader = HTTPDownloader{ + RoundTripper: roundTripper, + Debug: true, + } + + request, _ := http.NewRequest("GET", "", nil) + response := &http.Response{ + StatusCode: 400, + Proto: "HTTP/1.1", + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString(responseBody)), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) + err := downloader.DownloadFile() + Expect(err).To(HaveOccurred()) + + const debugFile = "debug-download.html" + + _, err = os.Stat(debugFile) + Expect(err).To(BeNil()) + + content, readErr := ioutil.ReadFile(debugFile) + Expect(readErr).To(BeNil()) + Expect(string(content)).To(Equal(responseBody)) + + defer os.Remove(debugFile) + }) + + It("showProgress", func() { + downloader = HTTPDownloader{ + RoundTripper: roundTripper, + ShowProgress: true, + TargetFilePath: targetFilePath, + } + + request, _ := http.NewRequest("GET", "", nil) + response := &http.Response{ + StatusCode: 200, + Proto: "HTTP/1.1", + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString(responseBody)), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) + err := downloader.DownloadFile() + Expect(err).To(BeNil()) + }) + }) +})