Repository: kubesphere/ks-devops
Pull request #742 (2022-07-15 05:28:20)

Set a generic response for the jenkinsfile update API
<!-- Thanks for sending a pull request! Here are some tips for you:

1. If you want **faster** PR reviews, read how: https://github.com/kubesphere/community/blob/master/developer-guide/development/the-pr-author-guide-to-getting-through-code-review.md
2. In case you want to know how your PR got reviewed, read: https://github.com/kubesphere/community/blob/master/developer-guide/development/code-review-guide.md
3. Here are some coding conventions followed by the KubeSphere community: https://github.com/kubesphere/community/blob/master/developer-guide/development/coding-conventions.md
4. Additional open-source best practice: https://github.com/LinuxSuRen/open-source-best-practice
-->

### What type of PR is this?
<!-- 
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design
/kind chore

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
/kind cleanup

### What this PR does / why we need it:

### Which issue(s) this PR fixes:
<!--
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
Please leave it or change # to be None if there is no corresponding issue that exists
-->
Fixes #

### Special notes for reviewers:
<!--
You can use the following command to let the DevOps SIG members help you to review your PR.
/cc @kubesphere/sig-devops 
And please avoid cc any individual.
-->
Please check the following list before waiting reviewers:

- [ ] Already committed the CRD files to [the Helm Chart](https://github.com/kubesphere-sigs/ks-devops-helm-chart/) if you created some new CRDs
- [ ] Already [added the permission](https://github.com/kubesphere/ks-installer/blob/9e063b085a0e43fdb3d0d9e3e7f4149146f14b9c/roles/ks-core/prepare/files/ks-init/role-templates.yaml) for the new API
- [ ] Already added the RBAC markers for the new controllers

### Does this PR introduce a user-facing change??
<!--
If no, just write "None" in the release-note block below.
If yes, a release note is required:
Enter your extended-release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

Please keep the note be same as your PR title if you believe it should be in the release notes.
-->
```release-note
None
```


ks-ci-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please ask for approval from **linuxsuren** after the PR has been reviewed.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubesphere%2Fks-devops).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubesphere/ks-devops/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["linuxsuren"]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/742?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#742](https://codecov.io/gh/kubesphere/ks-devops/pull/742?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (32917f9) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/59547bbae728f21875e2229db15753a0d91af2a6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (59547bb) will **decrease** coverage by `0.02%`.
> The diff coverage is `25.00%`.

```diff
@@            Coverage Diff             @@
##           master     #742      +/-   ##
==========================================
- Coverage   78.06%   78.03%   -0.03%     
==========================================
  Files         157      157              
  Lines       23729    23730       +1     
==========================================
- Hits        18523    18517       -6     
- Misses       4969     4976       +7     
  Partials      237      237              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `78.03% <25.00%> (-0.03%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/kubesphere/ks-devops/pull/742?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [pkg/kapis/devops/v1alpha3/handler.go](https://codecov.io/gh/kubesphere/ks-devops/pull/742/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy9oYW5kbGVyLmdv) | `63.35% <0.00%> (-0.68%)` | :arrow_down: |
| [pkg/kapis/devops/v1alpha3/register.go](https://codecov.io/gh/kubesphere/ks-devops/pull/742/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy9yZWdpc3Rlci5nbw==) | `100.00% <100.00%> (ø)` | |
| [...ns/devopscredential/devopscredential\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/742/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvamVua2lucy9kZXZvcHNjcmVkZW50aWFsL2Rldm9wc2NyZWRlbnRpYWxfY29udHJvbGxlci5nbw==) | `33.33% <0.00%> (-3.41%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/742?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/742?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Last update [59547bb...32917f9](https://codecov.io/gh/kubesphere/ks-devops/pull/742?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).


Patch:
diff --git a/pkg/kapis/devops/v1alpha3/handler.go b/pkg/kapis/devops/v1alpha3/handler.go
index 91dea78..de1bea6 100644
--- a/pkg/kapis/devops/v1alpha3/handler.go
+++ b/pkg/kapis/devops/v1alpha3/handler.go
@@ -209,6 +209,18 @@ type GenericPayload struct {
 	Data string `json:"data"`
 }
 
+// GenericResponse represents a generic HTTP response data structure
+type GenericResponse struct {
+	Result string `json:"result"`
+}
+
+// NewSuccessResponse creates a response for the success case
+func NewSuccessResponse() *GenericResponse {
+	return &GenericResponse{
+		Result: "success",
+	}
+}
+
 func (h *devopsHandler) UpdateJenkinsfile(request *restful.Request, response *restful.Response) {
 	projectName := request.PathParameter("devops")
 	pipelineName := request.PathParameter("pipeline")
@@ -225,7 +237,7 @@ func (h *devopsHandler) UpdateJenkinsfile(request *restful.Request, response *re
 	if client, err = h.getDevOps(request); err == nil {
 		err = client.UpdateJenkinsfile(projectName, pipelineName, mode, payload.Data)
 	}
-	errorHandle(request, response, nil, err)
+	errorHandle(request, response, NewSuccessResponse(), err)
 }
 
 func (h *devopsHandler) DeletePipeline(request *restful.Request, response *restful.Response) {
diff --git a/pkg/kapis/devops/v1alpha3/register.go b/pkg/kapis/devops/v1alpha3/register.go
index 1406179..ff3a931 100644
--- a/pkg/kapis/devops/v1alpha3/register.go
+++ b/pkg/kapis/devops/v1alpha3/register.go
@@ -178,8 +178,7 @@ func registerRoutersForPipelines(handler *devopsHandler, ws *restful.WebService)
 		Param(ws.QueryParameter("mode", "the mode(json or raw) that you expect to update the Jenkinsfile")).
 		Reads(&GenericPayload{}, "The Jenkinsfile content should be in the 'data' field").
 		Doc("Update the Jenkinsfile of a Pipeline").
-		Returns(http.StatusOK, api.StatusOK, v1alpha3.Pipeline{}).
-		Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectTag}))
+		Returns(http.StatusOK, api.StatusOK, GenericResponse{}))
 
 	ws.Route(ws.DELETE("/devops/{devops}/pipelines/{pipeline}").
 		To(handler.DeletePipeline).
diff --git a/pkg/models/devops/devops.go b/pkg/models/devops/devops.go
index cffd89f..c862c64 100644
--- a/pkg/models/devops/devops.go
+++ b/pkg/models/devops/devops.go
@@ -225,8 +225,8 @@ func (d devopsOperator) DeleteDevOpsProject(workspace string, projectName string
 }
 
 func (d devopsOperator) UpdateDevOpsProject(workspace string, project *v1alpha3.DevOpsProject) (*v1alpha3.DevOpsProject, error) {
-	if project.Labels == nil {
-		project.Labels = make(map[string]string)
+	if project.Annotations == nil {
+		project.Annotations = make(map[string]string)
 	}
 	project.Annotations[devopsv1alpha3.DevOpeProjectSyncStatusAnnoKey] = StatusPending
 	project.Annotations[devopsv1alpha3.DevOpeProjectSyncTimeAnnoKey] = GetSyncNowTime()
@@ -255,14 +255,17 @@ func (d devopsOperator) ListDevOpsProject(workspace string, limit, offset int) (
 }
 
 // pipelineobj in crd
-func (d devopsOperator) CreatePipelineObj(projectName string, pipeline *v1alpha3.Pipeline) (*v1alpha3.Pipeline, error) {
+func (d devopsOperator) CreatePipelineObj(projectName string, pipeline *v1alpha3.Pipeline) (pip *v1alpha3.Pipeline, err error) {
 	projectObj, err := d.ksclient.DevopsV1alpha3().DevOpsProjects().Get(d.context, projectName, metav1.GetOptions{})
-	if err != nil {
-		return nil, err
+	if err == nil {
+		if projectObj.Annotations == nil {
+			projectObj.Annotations = map[string]string{}
+		}
+		projectObj.Annotations[devopsv1alpha3.PipelineSyncStatusAnnoKey] = StatusPending
+		projectObj.Annotations[devopsv1alpha3.PipelineSyncTimeAnnoKey] = GetSyncNowTime()
+		pip, err = d.ksclient.DevopsV1alpha3().Pipelines(projectObj.Status.AdminNamespace).Create(d.context, pipeline, metav1.CreateOptions{})
 	}
-	projectObj.Annotations[devopsv1alpha3.PipelineSyncStatusAnnoKey] = StatusPending
-	projectObj.Annotations[devopsv1alpha3.PipelineSyncTimeAnnoKey] = GetSyncNowTime()
-	return d.ksclient.DevopsV1alpha3().Pipelines(projectObj.Status.AdminNamespace).Create(d.context, pipeline, metav1.CreateOptions{})
+	return
 }
 
 func (d devopsOperator) GetPipelineObj(projectName string, pipelineName string) (*v1alpha3.Pipeline, error) {
@@ -355,6 +358,9 @@ func (d devopsOperator) CreateCredentialObj(projectName string, secret *v1.Secre
 	if err != nil {
 		return nil, err
 	}
+	if secret.Annotations == nil {
+		secret.Annotations = map[string]string{}
+	}
 	secret.Annotations[devopsv1alpha3.CredentialAutoSyncAnnoKey] = "true"
 	secret.Annotations[devopsv1alpha3.CredentialSyncStatusAnnoKey] = StatusPending
 	secret.Annotations[devopsv1alpha3.CredentialSyncTimeAnnoKey] = GetSyncNowTime()
@@ -390,6 +396,9 @@ func (d devopsOperator) UpdateCredentialObj(projectName string, secret *v1.Secre
 	if err != nil {
 		return nil, err
 	}
+	if secret.Annotations == nil {
+		secret.Annotations = map[string]string{}
+	}
 	secret.Annotations[devopsv1alpha3.CredentialAutoSyncAnnoKey] = "true"
 	secret.Annotations[devopsv1alpha3.CredentialSyncStatusAnnoKey] = StatusPending
 	secret.Annotations[devopsv1alpha3.CredentialSyncTimeAnnoKey] = GetSyncNowTime()


Test patch:
diff --git a/controllers/jenkins/pipeline/pipeline_metadata_controller_test.go b/controllers/jenkins/pipeline/pipeline_metadata_controller_test.go
new file mode 100644
index 0000000..a49d691
--- /dev/null
+++ b/controllers/jenkins/pipeline/pipeline_metadata_controller_test.go
@@ -0,0 +1,372 @@
+/*
+Copyright 2022 The KubeSphere Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package pipeline
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strconv"
+
+	"github.com/golang/mock/gomock"
+	"github.com/jenkins-zh/jenkins-client/pkg/core"
+	"github.com/jenkins-zh/jenkins-client/pkg/job"
+	"github.com/jenkins-zh/jenkins-client/pkg/mock/mhttp"
+	"github.com/jenkins-zh/jenkins-client/pkg/util"
+	. "github.com/onsi/ginkgo"
+	"github.com/onsi/gomega"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	"sigs.k8s.io/controller-runtime/pkg/event"
+)
+
+const (
+	organizationAPIPrefix = "/blue/rest/organizations"
+	organization          = "jenkins"
+)
+
+func getPipelineAPI(pipelineName string, folders ...string) string {
+	api := fmt.Sprintf("%s/%s", organizationAPIPrefix, organization)
+	folders = append(folders, pipelineName)
+	for _, folder := range folders {
+		api = fmt.Sprintf("%s/pipelines/%s", api, folder)
+	}
+	return api
+}
+
+func getPipelineBranchAPI(option *job.GetBranchesOption) string {
+	api := getPipelineAPI(option.PipelineName, option.Folders...)
+	api = api + "/branches/"
+	apiURL := &url.URL{
+		Path: api,
+	}
+	query := apiURL.Query()
+	if option.Filter != "" {
+		query.Add("filter", string(option.Filter))
+	}
+	if option.Start > 0 {
+		query.Add("start", strconv.Itoa(option.Start))
+	}
+	if option.Limit > 0 {
+		query.Add("limit", strconv.Itoa(option.Limit))
+	}
+	apiURL.RawQuery = query.Encode()
+	return apiURL.String()
+}
+
+var _ = Describe("Pipeline metadata", func() {
+	var (
+		ctrl         *gomock.Controller
+		c            Reconciler
+		roundTripper *mhttp.MockRoundTripper
+	)
+
+	BeforeEach(func() {
+		ctrl = gomock.NewController(GinkgoT())
+		c = Reconciler{}
+		roundTripper = mhttp.NewMockRoundTripper(ctrl)
+		c.JenkinsCore.RoundTripper = roundTripper
+		c.JenkinsCore.URL = "http://localhost"
+	})
+	AfterEach(func() {
+		ctrl.Finish()
+	})
+
+	Context("Pipeline Metadata", func() {
+		It("Metadata with default namespace", func() {
+			pipelineName := "pipelineA"
+			api := getPipelineAPI(pipelineName, metav1.NamespaceDefault)
+			requestURL, _ := util.URLJoinAsString(c.JenkinsCore.URL, api)
+			request, _ := http.NewRequest(http.MethodGet, requestURL, nil)
+			response := &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       io.NopCloser(bytes.NewBufferString(`{"name":"pipelineA"}`)),
+			}
+			roundTripper.EXPECT().
+				RoundTrip(core.NewRequestMatcher(request)).
+				Return(response, nil)
+
+			pipeline := &v1alpha3.Pipeline{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:        pipelineName,
+					Namespace:   metav1.NamespaceDefault,
+					Annotations: map[string]string{},
+				},
+			}
+			err := c.obtainAndUpdatePipelineMetadata(pipeline)
+
+			gomega.Expect(err).To(gomega.Succeed())
+			gomega.Expect(pipeline.Annotations).NotTo(gomega.BeNil())
+			gomega.Expect(pipeline.Annotations[v1alpha3.PipelineJenkinsMetadataAnnoKey]).NotTo(gomega.BeNil())
+			expectMetadataAnno := `{"weatherScore":0,"name":"pipelineA"}`
+			gomega.Expect(pipeline.Annotations[v1alpha3.PipelineJenkinsMetadataAnnoKey]).To(gomega.Equal(expectMetadataAnno))
+		})
+		It("Metadata with custom namespace", func() {
+			pipelineName := "pipelineA"
+			customNamespace := "namespaceA"
+			api := getPipelineAPI(pipelineName, customNamespace)
+			requestURL, _ := util.URLJoinAsString(c.JenkinsCore.URL, api)
+			request, _ := http.NewRequest(http.MethodGet, requestURL, nil)
+			response := &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       io.NopCloser(bytes.NewBufferString(`{"name":"pipelineA"}`)),
+			}
+			roundTripper.EXPECT().
+				RoundTrip(core.NewRequestMatcher(request)).
+				Return(response, nil)
+
+			pipeline := &v1alpha3.Pipeline{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:        pipelineName,
+					Namespace:   customNamespace,
+					Annotations: map[string]string{},
+				},
+			}
+			err := c.obtainAndUpdatePipelineMetadata(pipeline)
+
+			gomega.Expect(err).To(gomega.Succeed())
+			gomega.Expect(pipeline.Annotations).NotTo(gomega.BeNil())
+			gomega.Expect(pipeline.Annotations[v1alpha3.PipelineJenkinsMetadataAnnoKey]).NotTo(gomega.BeNil())
+			expectMetadataAnno := `{"weatherScore":0,"name":"pipelineA"}`
+			gomega.Expect(pipeline.Annotations[v1alpha3.PipelineJenkinsMetadataAnnoKey]).To(gomega.Equal(expectMetadataAnno))
+		})
+	})
+
+	Context("Pipeline Branches", func() {
+		given := func(api string, expectedStatus int, responseBody string) {
+			request, _ := http.NewRequest(http.MethodGet, api, nil)
+			reponse := &http.Response{
+				StatusCode: expectedStatus,
+				Body:       io.NopCloser(bytes.NewBufferString(responseBody)),
+			}
+			roundTripper.EXPECT().RoundTrip(core.NewRequestMatcher(request)).Return(reponse, nil)
+		}
+		It("Multi Branch Pipeline default namespace", func() {
+			pipelineName := "pipelineA"
+			pipeline := &v1alpha3.Pipeline{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:        pipelineName,
+					Namespace:   metav1.NamespaceDefault,
+					Annotations: map[string]string{},
+				},
+				Spec: v1alpha3.PipelineSpec{
+					Type: v1alpha3.MultiBranchPipelineType,
+				},
+			}
+			option := &job.GetBranchesOption{
+				Folders:      []string{pipeline.Namespace},
+				PipelineName: pipeline.Name,
+				Limit:        100000,
+			}
+			branchAPI := getPipelineBranchAPI(option)
+			requestURL, _ := util.URLJoinAsString(c.JenkinsCore.URL, branchAPI)
+			given(requestURL, http.StatusOK, `[]`)
+
+			err := c.obtainAndUpdatePipelineBranches(pipeline)
+
+			gomega.Expect(err).To(gomega.Succeed())
+			gomega.Expect(pipeline.Annotations).NotTo(gomega.BeNil())
+			gomega.Expect(pipeline.Annotations[v1alpha3.PipelineJenkinsBranchesAnnoKey]).NotTo(gomega.BeNil())
+			expectBranchAnno := `[]`
+			gomega.Expect(pipeline.Annotations[v1alpha3.PipelineJenkinsBranchesAnnoKey]).To(gomega.Equal(expectBranchAnno))
+		})
+		It("Multi Branch Pipeline custom namespace", func() {
+			pipelineName := "pipelineA"
+			namespace := "namespaceA"
+			pipeline := &v1alpha3.Pipeline{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:        pipelineName,
+					Namespace:   namespace,
+					Annotations: map[string]string{},
+				},
+				Spec: v1alpha3.PipelineSpec{
+					Type: v1alpha3.MultiBranchPipelineType,
+				},
+			}
+
+			option := &job.GetBranchesOption{
+				Folders:      []string{pipeline.Namespace},
+				PipelineName: pipeline.Name,
+				Limit:        100000,
+			}
+			branchAPI := getPipelineBranchAPI(option)
+			requestURL, _ := util.URLJoinAsString(c.JenkinsCore.URL, branchAPI)
+			given(requestURL, http.StatusOK, `[]`)
+
+			err := c.obtainAndUpdatePipelineBranches(pipeline)
+
+			gomega.Expect(err).To(gomega.Succeed())
+			gomega.Expect(pipeline.Annotations).NotTo(gomega.BeNil())
+			gomega.Expect(pipeline.Annotations[v1alpha3.PipelineJenkinsBranchesAnnoKey]).NotTo(gomega.BeNil())
+			expectBranchAnno := `[]`
+			gomega.Expect(pipeline.Annotations[v1alpha3.PipelineJenkinsBranchesAnnoKey]).To(gomega.Equal(expectBranchAnno))
+		})
+		It("Multi Branch Pipeline with query", func() {
+			pipelineName := "pipelineA"
+			namespace := "namespaceA"
+			pipeline := &v1alpha3.Pipeline{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:        pipelineName,
+					Namespace:   namespace,
+					Annotations: map[string]string{},
+				},
+				Spec: v1alpha3.PipelineSpec{
+					Type: v1alpha3.MultiBranchPipelineType,
+				},
+			}
+
+			option := &job.GetBranchesOption{
+				Folders:      []string{pipeline.Namespace},
+				PipelineName: pipeline.Name,
+				Filter:       job.OriginFilter,
+				Start:        123,
+				Limit:        456,
+			}
+			branchAPI := getPipelineBranchAPI(option)
+			requestURL, _ := util.URLJoinAsString(c.JenkinsCore.URL, branchAPI)
+			given(requestURL, http.StatusOK, `[]`)
+
+			err := c.obtainAndUpdatePipelineBranches(pipeline)
+
+			gomega.Expect(err).To(gomega.Succeed())
+			gomega.Expect(pipeline.Annotations).NotTo(gomega.BeNil())
+			gomega.Expect(pipeline.Annotations[v1alpha3.PipelineJenkinsBranchesAnnoKey]).NotTo(gomega.BeNil())
+			expectBranchAnno := `[]`
+			gomega.Expect(pipeline.Annotations[v1alpha3.PipelineJenkinsBranchesAnnoKey]).To(gomega.Equal(expectBranchAnno))
+		})
+		It("Multi Branch Pipeline Response a branch", func() {
+			pipelineName := "pipelineA"
+			namespace := "namespaceA"
+			pipeline := &v1alpha3.Pipeline{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:        pipelineName,
+					Namespace:   namespace,
+					Annotations: map[string]string{},
+				},
+				Spec: v1alpha3.PipelineSpec{
+					Type: v1alpha3.MultiBranchPipelineType,
+				},
+			}
+
+			option := &job.GetBranchesOption{
+				Folders:      []string{pipeline.Namespace},
+				PipelineName: pipeline.Name,
+				Limit:        100000,
+			}
+			branchAPI := getPipelineBranchAPI(option)
+			requestURL, _ := util.URLJoinAsString(c.JenkinsCore.URL, branchAPI)
+			given(requestURL, http.StatusOK, `
+[{
+   "disabled":false,
+   "displayName":"v0.0.1",
+   "estimatedDurationInMillis":-1,
+   "fullDisplayName":"my-devops-projectsg945/github-pipeline/v0.0.1",
+   "fullName":"my-devops-projectsg945/github-pipeline/v0.0.1",
+   "latestRun":null,
+   "name":"v0.0.1",
+   "organization":"jenkins",
+   "weatherScore":100,
+   "branch":{
+      "isPrimary":false,
+      "issues":[],
+      "url":"https://github.com/JohnNiang/devops-java-thin-sample/tree/v0.0.1"
+   }
+}]
+`)
+
+			err := c.obtainAndUpdatePipelineBranches(pipeline)
+
+			gomega.Expect(err).To(gomega.Succeed())
+			gomega.Expect(pipeline.Annotations).NotTo(gomega.BeNil())
+			gomega.Expect(pipeline.Annotations[v1alpha3.PipelineJenkinsBranchesAnnoKey]).NotTo(gomega.BeNil())
+			expectBranchAnno := `[{"name":"v0.0.1","rawName":"v0.0.1","weatherScore":100,"branch":{"url":"https://github.com/JohnNiang/devops-java-thin-sample/tree/v0.0.1"}}]`
+			gomega.Expect(pipeline.Annotations[v1alpha3.PipelineJenkinsBranchesAnnoKey]).To(gomega.Equal(expectBranchAnno))
+		})
+
+		It("single branch", func() {
+			pipelineName := "pipelineA"
+			pipeline := &v1alpha3.Pipeline{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: pipelineName,
+				},
+			}
+			err := c.obtainAndUpdatePipelineBranches(pipeline)
+			gomega.Expect(err).To(gomega.Succeed())
+			gomega.Expect(len(pipeline.Annotations)).To(gomega.Equal(0))
+		})
+
+		It("pipeline Metadata Predicate should call create", func() {
+			pipelineName := "pipelineA"
+			pipeline := &v1alpha3.Pipeline{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: pipelineName,
+				},
+			}
+			instance := pipelineMetadataPredicate
+			evt := event.CreateEvent{
+				Object: pipeline,
+			}
+
+			gomega.Expect(instance.Create(evt)).To(gomega.BeTrue())
+		})
+		It("pipeline Metadata Predicate should not call delete", func() {
+			pipelineName := "pipelineA"
+			pipeline := &v1alpha3.Pipeline{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: pipelineName,
+				},
+			}
+			instance := pipelineMetadataPredicate
+			evt := event.DeleteEvent{
+				Object: pipeline,
+			}
+
+			gomega.Expect(instance.Delete(evt)).To(gomega.BeFalse())
+		})
+		It("pipeline Metadata Predicate should not call update", func() {
+			pipelineName := "pipelineA"
+			pipeline := &v1alpha3.Pipeline{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: pipelineName,
+				},
+			}
+			instance := pipelineMetadataPredicate
+			evt := event.UpdateEvent{
+				ObjectOld: pipeline,
+				ObjectNew: pipeline,
+			}
+
+			gomega.Expect(instance.Update(evt)).To(gomega.BeFalse())
+		})
+		It("pipeline Metadata Predicate should not call Generic", func() {
+			pipelineName := "pipelineA"
+			pipeline := &v1alpha3.Pipeline{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: pipelineName,
+				},
+			}
+			instance := pipelineMetadataPredicate
+			evt := event.GenericEvent{
+				Object: pipeline,
+			}
+
+			gomega.Expect(instance.Generic(evt)).To(gomega.BeFalse())
+		})
+	})
+})
diff --git a/controllers/jenkins/pipeline/setup_test.go b/controllers/jenkins/pipeline/setup_test.go
new file mode 100644
index 0000000..4bb0bd7
--- /dev/null
+++ b/controllers/jenkins/pipeline/setup_test.go
@@ -0,0 +1,31 @@
+/*
+Copyright 2022 The KubeSphere Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package pipeline_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	"github.com/onsi/ginkgo/reporters"
+	. "github.com/onsi/gomega"
+)
+
+func TestPipeline(t *testing.T) {
+	RegisterFailHandler(Fail)
+	junitReporter := reporters.NewJUnitReporter("pipelinerun-test.xml")
+	RunSpecsWithDefaultAndCustomReporters(t, "test PipelineRun controller", []Reporter{junitReporter})
+}
diff --git a/pkg/kapis/devops/v1alpha3/register_test.go b/pkg/kapis/devops/v1alpha3/register_test.go
index 56590fd..969384c 100644
--- a/pkg/kapis/devops/v1alpha3/register_test.go
+++ b/pkg/kapis/devops/v1alpha3/register_test.go
@@ -22,6 +22,7 @@ import (
 	"kubesphere.io/devops/pkg/jwt/token"
 	"net/http"
 	"net/http/httptest"
+	"strings"
 	"testing"
 
 	"github.com/emicklei/go-restful"
@@ -67,8 +68,10 @@ func TestAPIsExist(t *testing.T) {
 		uri    string
 	}
 	tests := []struct {
-		name string
-		args args
+		name       string
+		args       args
+		body       string
+		expectCode int
 	}{{
 		name: "credential list",
 		args: args{
@@ -81,6 +84,15 @@ func TestAPIsExist(t *testing.T) {
 			method: http.MethodPost,
 			uri:    "/devops/fake/credentials",
 		},
+		expectCode: 400,
+	}, {
+		name: "create a credential with body",
+		args: args{
+			method: http.MethodPost,
+			uri:    "/devops/fake/credentials",
+		},
+		body:       `{}`,
+		expectCode: 200,
 	}, {
 		name: "get a credential",
 		args: args{
@@ -93,6 +105,15 @@ func TestAPIsExist(t *testing.T) {
 			method: http.MethodPut,
 			uri:    "/devops/fake/credentials/fake",
 		},
+		expectCode: 400,
+	}, {
+		name: "update a credential with body",
+		args: args{
+			method: http.MethodPut,
+			uri:    "/devops/fake/credentials/fake",
+		},
+		body:       `{}`,
+		expectCode: 200,
 	}, {
 		name: "delete a credential",
 		args: args{
@@ -111,6 +132,15 @@ func TestAPIsExist(t *testing.T) {
 			method: http.MethodPost,
 			uri:    "/devops/fake/pipelines",
 		},
+		expectCode: 400,
+	}, {
+		name: "create a pipeline with body",
+		args: args{
+			method: http.MethodPost,
+			uri:    "/devops/fake/pipelines",
+		},
+		body:       `{}`,
+		expectCode: 200,
 	}, {
 		name: "get a pipeline",
 		args: args{
@@ -118,11 +148,20 @@ func TestAPIsExist(t *testing.T) {
 			uri:    "/devops/fake/pipelines/fake",
 		},
 	}, {
-		name: "update a pipeline",
+		name: "update a pipeline without body",
+		args: args{
+			method: http.MethodPut,
+			uri:    "/devops/fake/pipelines/fake",
+		},
+		expectCode: 400,
+	}, {
+		name: "update a pipeline with body",
 		args: args{
 			method: http.MethodPut,
 			uri:    "/devops/fake/pipelines/fake",
 		},
+		body:       `{}`,
+		expectCode: 200,
 	}, {
 		name: "delete a pipeline",
 		args: args{
@@ -141,6 +180,15 @@ func TestAPIsExist(t *testing.T) {
 			method: http.MethodPost,
 			uri:    "/workspaces/fake/devops",
 		},
+		expectCode: 400,
+	}, {
+		name: "create a devops with body",
+		args: args{
+			method: http.MethodPost,
+			uri:    "/workspaces/fake/devops",
+		},
+		body:       `{}`,
+		expectCode: 400,
 	}, {
 		name: "get a devops",
 		args: args{
@@ -153,28 +201,51 @@ func TestAPIsExist(t *testing.T) {
 			method: http.MethodPut,
 			uri:    "/workspaces/fake/devops/fake",
 		},
+		expectCode: 400,
+	}, {
+		name: "update a devops with body",
+		args: args{
+			method: http.MethodPut,
+			uri:    "/workspaces/fake/devops/fake",
+		},
+		body:       `{}`,
+		expectCode: 404,
 	}, {
 		name: "delete a devops",
 		args: args{
 			method: http.MethodDelete,
 			uri:    "/workspaces/fake/devops/fake",
 		},
+	}, {
+		name: "update jenkinsfile without body",
+		args: args{
+			method: http.MethodPut,
+			uri:    "/devops/fake/pipelines/fake-pipeline/jenkinsfile",
+		},
+		expectCode: 400,
 	}, {
 		name: "update jenkinsfile",
 		args: args{
 			method: http.MethodPut,
-			uri:    "/devops/fake/pipelines/fake/jenkinsfile",
+			uri:    "/devops/fake/pipelines/fake-pipeline/jenkinsfile",
 		},
+		body:       `{"data":"fake-jenkinsfile"}`,
+		expectCode: 404,
 	}}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			httpRequest, _ := http.NewRequest(tt.args.method,
-				"http://fake.com/kapis/devops.kubesphere.io/v1alpha3"+tt.args.uri, nil)
+				"http://fake.com/kapis/devops.kubesphere.io/v1alpha3"+tt.args.uri, strings.NewReader(tt.body))
 			httpRequest = httpRequest.WithContext(context.WithValue(context.TODO(), constants.K8SToken, constants.ContextKeyK8SToken("")))
+			httpRequest.Header.Set("Content-Type", "application/json")
+
+			if tt.expectCode == 0 {
+				tt.expectCode = http.StatusOK
+			}
 
 			httpWriter := httptest.NewRecorder()
 			container.Dispatch(httpWriter, httpRequest)
-			assert.NotEqual(t, 404, httpWriter.Code)
+			assert.Equal(t, tt.expectCode, httpWriter.Code)
 		})
 	}
 }