go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/api/gerrit/rest_test.go (about)

     1  // Copyright 2018 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gerrit
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"net/url"
    25  	"testing"
    26  	"time"
    27  
    28  	"google.golang.org/grpc"
    29  	"google.golang.org/grpc/codes"
    30  	"google.golang.org/protobuf/types/known/timestamppb"
    31  
    32  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    33  	"go.chromium.org/luci/grpc/grpcutil"
    34  
    35  	. "github.com/smartystreets/goconvey/convey"
    36  	. "go.chromium.org/luci/common/testing/assertions"
    37  )
    38  
    39  func TestBuildURL(t *testing.T) {
    40  	t.Parallel()
    41  
    42  	Convey("buildURL works correctly", t, func() {
    43  		cPB, err := NewRESTClient(nil, "x-review.googlesource.com", true)
    44  		So(err, ShouldBeNil)
    45  		c, ok := cPB.(*client)
    46  		So(ok, ShouldBeTrue)
    47  
    48  		So(c.buildURL("/changes/project~123", nil, nil), ShouldResemble,
    49  			"https://x-review.googlesource.com/a/changes/project~123")
    50  		So(c.buildURL("/changes/project~123", url.Values{"o": []string{"ONE", "TWO"}}, nil), ShouldResemble,
    51  			"https://x-review.googlesource.com/a/changes/project~123?o=ONE&o=TWO")
    52  
    53  		opt := UseGerritMirror(func(host string) string { return "mirror-" + host })
    54  		So(c.buildURL("/changes/project~123", nil, []grpc.CallOption{opt}), ShouldResemble,
    55  			"https://mirror-x-review.googlesource.com/a/changes/project~123")
    56  
    57  		c.auth = false
    58  		So(c.buildURL("/path", nil, nil), ShouldResemble,
    59  			"https://x-review.googlesource.com/path")
    60  	})
    61  }
    62  
    63  func TestListChanges(t *testing.T) {
    64  	t.Parallel()
    65  	ctx := context.Background()
    66  
    67  	Convey("ListChanges", t, func() {
    68  		Convey("Validates Limit number", func() {
    69  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {})
    70  			defer srv.Close()
    71  
    72  			_, err := c.ListChanges(ctx, &gerritpb.ListChangesRequest{
    73  				Query: "label:Commit-Queue",
    74  				Limit: -1,
    75  			})
    76  			So(err, ShouldErrLike, "must be nonnegative")
    77  
    78  			_, err = c.ListChanges(ctx, &gerritpb.ListChangesRequest{
    79  				Query: "label:Commit-Queue",
    80  				Limit: 1001,
    81  			})
    82  			So(err, ShouldErrLike, "should be at most")
    83  		})
    84  
    85  		req := &gerritpb.ListChangesRequest{
    86  			Query: "label:Code-Review",
    87  			Limit: 1,
    88  		}
    89  
    90  		Convey("OK case with one change, _more_changes set in response", func() {
    91  			expectedResponse := &gerritpb.ListChangesResponse{
    92  				Changes: []*gerritpb.ChangeInfo{
    93  					{
    94  						Number: 1,
    95  						Owner: &gerritpb.AccountInfo{
    96  							AccountId: 1000096,
    97  							Name:      "John Doe",
    98  							Email:     "jdoe@example.com",
    99  							Username:  "jdoe",
   100  						},
   101  						Status:    gerritpb.ChangeStatus_MERGED,
   102  						Project:   "example/repo",
   103  						Ref:       "refs/heads/master",
   104  						Created:   timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")),
   105  						Updated:   timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")),
   106  						Submitted: timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")),
   107  						Branch:    "master",
   108  					},
   109  				},
   110  				MoreChanges: true,
   111  			}
   112  			var actualRequest *http.Request
   113  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
   114  				actualRequest = r
   115  				w.WriteHeader(200)
   116  				w.Header().Set("Content-Type", "application/json")
   117  				fmt.Fprint(w, `)]}'[
   118  					{
   119  						"_number": 1,
   120  						"owner": {
   121  							"_account_id":      1000096,
   122  							"name":             "John Doe",
   123  							"email":            "jdoe@example.com",
   124  							"username":         "jdoe"
   125  						},
   126  						"status": "MERGED",
   127  						"project": "example/repo",
   128  						"branch":  "master",
   129  						"created":   "2014-05-05 07:15:44.639000000",
   130  						"updated":   "2014-05-05 07:15:44.639000000",
   131  						"submitted": "2014-05-05 07:15:44.639000000",
   132  						"_more_changes": true
   133  					}
   134  				]`)
   135  			})
   136  			defer srv.Close()
   137  
   138  			Convey("Response and request are as expected", func() {
   139  				res, err := c.ListChanges(ctx, req)
   140  				So(err, ShouldBeNil)
   141  				So(res, ShouldResemble, expectedResponse)
   142  				So(actualRequest.URL.Query()["q"], ShouldResemble, []string{"label:Code-Review"})
   143  				So(actualRequest.URL.Query()["S"], ShouldResemble, []string{"0"})
   144  				So(actualRequest.URL.Query()["n"], ShouldResemble, []string{"1"})
   145  			})
   146  
   147  			Convey("Options are included in the request", func() {
   148  				req.Options = append(req.Options, gerritpb.QueryOption_DETAILED_ACCOUNTS, gerritpb.QueryOption_ALL_COMMITS)
   149  				_, err := c.ListChanges(ctx, req)
   150  				So(err, ShouldBeNil)
   151  				So(
   152  					actualRequest.URL.Query()["o"],
   153  					ShouldResemble,
   154  					[]string{"DETAILED_ACCOUNTS", "ALL_COMMITS"},
   155  				)
   156  			})
   157  		})
   158  	})
   159  }
   160  
   161  func TestGetChange(t *testing.T) {
   162  	t.Parallel()
   163  	ctx := context.Background()
   164  
   165  	Convey("GetChange", t, func() {
   166  		Convey("Validate args", func() {
   167  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {})
   168  			defer srv.Close()
   169  
   170  			_, err := c.GetChange(ctx, &gerritpb.GetChangeRequest{})
   171  			So(err, ShouldErrLike, "number must be positive")
   172  		})
   173  
   174  		req := &gerritpb.GetChangeRequest{Number: 1}
   175  
   176  		Convey("OK", func() {
   177  			expectedChange := &gerritpb.ChangeInfo{
   178  				Number: 1,
   179  				Owner: &gerritpb.AccountInfo{
   180  					AccountId:       1000096,
   181  					Name:            "John Doe",
   182  					Email:           "jdoe@example.com",
   183  					SecondaryEmails: []string{"johndoe@chromium.org"},
   184  					Username:        "jdoe",
   185  					Tags:            []string{"SERVICE_USER"},
   186  				},
   187  				Project:         "example/repo",
   188  				Ref:             "refs/heads/master",
   189  				Subject:         "Added new feature",
   190  				Status:          gerritpb.ChangeStatus_NEW,
   191  				CurrentRevision: "deadbeef",
   192  				Submittable:     true,
   193  				IsPrivate:       true,
   194  				MetaRevId:       "cafecafe",
   195  				Hashtags:        []string{"example_tag"},
   196  				Revisions: map[string]*gerritpb.RevisionInfo{
   197  					"deadbeef": {
   198  						Number: 1,
   199  						Kind:   gerritpb.RevisionInfo_REWORK,
   200  						Uploader: &gerritpb.AccountInfo{
   201  							AccountId:       1000096,
   202  							Name:            "John Doe",
   203  							Email:           "jdoe@example.com",
   204  							SecondaryEmails: []string{"johndoe@chromium.org"},
   205  							Username:        "jdoe",
   206  							Tags:            []string{"SERVICE_USER"},
   207  						},
   208  						Ref:         "refs/changes/123",
   209  						Created:     timestamppb.New(parseTime("2016-03-29T17:47:23.751000000Z")),
   210  						Description: "first upload",
   211  						Files: map[string]*gerritpb.FileInfo{
   212  							"go/to/file.go": {
   213  								LinesInserted: 32,
   214  								LinesDeleted:  44,
   215  								SizeDelta:     -567,
   216  								Size:          11984,
   217  							},
   218  						},
   219  						Commit: &gerritpb.CommitInfo{
   220  							Id:      "", // Gerrit doesn't set it, as it duplicates key in revisions map.
   221  							Message: "Title.\n\nBody is here.\n\nChange-Id: I100deadbeef",
   222  							Parents: []*gerritpb.CommitInfo_Parent{
   223  								{Id: "deadbeef00"},
   224  							},
   225  							Author: &gerritpb.GitPersonInfo{
   226  								Name:  "John Doe",
   227  								Email: "jdoe@example.com",
   228  							},
   229  						},
   230  					},
   231  				},
   232  				Labels: map[string]*gerritpb.LabelInfo{
   233  					"Code-Review": {
   234  						Approved: &gerritpb.AccountInfo{
   235  							Name:  "Rubber Stamper",
   236  							Email: "rubberstamper@example.com",
   237  						},
   238  					},
   239  					"Commit-Queue": {
   240  						Optional:     true,
   241  						DefaultValue: 0,
   242  						Values:       map[int32]string{0: "Not ready", 1: "Dry run", 2: "Commit"},
   243  						All: []*gerritpb.ApprovalInfo{
   244  							{
   245  								User: &gerritpb.AccountInfo{
   246  									AccountId: 1010101,
   247  									Name:      "Dry Runner",
   248  									Email:     "dry-runner@example.com",
   249  								},
   250  								Value:                1,
   251  								PermittedVotingRange: &gerritpb.VotingRangeInfo{Min: 0, Max: 2},
   252  								Date:                 timestamppb.New(parseTime("2020-12-13T18:32:35.000000000Z")),
   253  							},
   254  						},
   255  					},
   256  				},
   257  				Created:   timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")),
   258  				Updated:   timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")),
   259  				Submitted: timestamppb.New(parseTime("0001-01-01T00:00:00.00000000Z")),
   260  				Messages: []*gerritpb.ChangeMessageInfo{
   261  					{
   262  						Id: "YH-egE",
   263  						Author: &gerritpb.AccountInfo{
   264  							AccountId: 1000096,
   265  							Name:      "John Doe",
   266  							Email:     "john.doe@example.com",
   267  							Username:  "jdoe",
   268  						},
   269  						Date:    timestamppb.New(parseTime("2013-03-23T21:34:02.419000000Z")),
   270  						Message: "Patch Set 1:\n\nThis is the first message.",
   271  						Tag:     "autogenerated:gerrit:test",
   272  					},
   273  					{
   274  						Id: "WEEdhU",
   275  						Author: &gerritpb.AccountInfo{
   276  							AccountId: 1000097,
   277  							Name:      "Jane Roe",
   278  							Email:     "jane.roe@example.com",
   279  							Username:  "jroe",
   280  						},
   281  						Date:    timestamppb.New(parseTime("2013-03-23T21:36:52.332000000Z")),
   282  						Message: "Patch Set 1:\n\nThis is the second message.\n\nWith a line break.",
   283  					},
   284  				},
   285  				Requirements: []*gerritpb.Requirement{
   286  					{
   287  						Status:       gerritpb.Requirement_REQUIREMENT_STATUS_OK,
   288  						FallbackText: "nothing more required",
   289  						Type:         "alpha-numer1c-type",
   290  					},
   291  				},
   292  				SubmitRequirements: []*gerritpb.SubmitRequirementResultInfo{
   293  					{
   294  						Name:        "Verified",
   295  						Description: "Submit requirement for the 'Verified' label",
   296  						Status:      gerritpb.SubmitRequirementResultInfo_NOT_APPLICABLE,
   297  						IsLegacy:    false,
   298  						ApplicabilityExpressionResult: &gerritpb.SubmitRequirementExpressionInfo{
   299  							Fulfilled: false,
   300  						},
   301  					},
   302  					{
   303  						Name:        "Code-Owners",
   304  						Description: "Code Owners overrides approval",
   305  						Status:      gerritpb.SubmitRequirementResultInfo_SATISFIED,
   306  						IsLegacy:    false,
   307  						ApplicabilityExpressionResult: &gerritpb.SubmitRequirementExpressionInfo{
   308  							Fulfilled: true,
   309  						},
   310  						SubmittabilityExpressionResult: &gerritpb.SubmitRequirementExpressionInfo{
   311  							Expression:   "has:approval_code-owners",
   312  							Fulfilled:    true,
   313  							PassingAtoms: []string{"has:approval_code-owners"},
   314  							FailingAtoms: []string{},
   315  						},
   316  						OverrideExpressionResult: &gerritpb.SubmitRequirementExpressionInfo{
   317  							Expression:   "label:Owners-Override=+1",
   318  							Fulfilled:    false,
   319  							PassingAtoms: []string{},
   320  							FailingAtoms: []string{"label:Owners-Override=+1"},
   321  						},
   322  					},
   323  					{
   324  						Name:        "Code-Review",
   325  						Description: "Submit requirement for the 'Code-Review' label",
   326  						Status:      gerritpb.SubmitRequirementResultInfo_UNSATISFIED,
   327  						IsLegacy:    false,
   328  						SubmittabilityExpressionResult: &gerritpb.SubmitRequirementExpressionInfo{
   329  							Expression:   "label:Code-Review=MAX,user=non_uploader -label:Code-Review=MIN",
   330  							Fulfilled:    false,
   331  							PassingAtoms: []string{},
   332  							FailingAtoms: []string{
   333  								"label:Code-Review=MAX,user=non_uploader",
   334  								"label:Code-Review=MIN",
   335  							},
   336  						},
   337  						OverrideExpressionResult: &gerritpb.SubmitRequirementExpressionInfo{
   338  							Expression:   "label:Bot-Commit=+1",
   339  							Fulfilled:    false,
   340  							PassingAtoms: []string{},
   341  							FailingAtoms: []string{"label:Bot-Commit=+1"},
   342  						},
   343  					},
   344  				},
   345  				Reviewers: &gerritpb.ReviewerStatusMap{
   346  					Reviewers: []*gerritpb.AccountInfo{
   347  						{
   348  							AccountId: 1000096,
   349  							Name:      "John Doe",
   350  							Email:     "john.doe@example.com",
   351  							Username:  "jdoe",
   352  						},
   353  						{
   354  							AccountId: 1000097,
   355  							Name:      "Jane Roe",
   356  							Email:     "jane.roe@example.com",
   357  							Username:  "jroe",
   358  						},
   359  					},
   360  				},
   361  				Branch: "master",
   362  			}
   363  			var actualRequest *http.Request
   364  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
   365  				actualRequest = r
   366  				w.WriteHeader(200)
   367  				w.Header().Set("Content-Type", "application/json")
   368  				fmt.Fprint(w, `)]}'{
   369  					"_number": 1,
   370  					"status": "NEW",
   371  					"owner": {
   372  						"_account_id":      1000096,
   373  						"name":             "John Doe",
   374  						"email":            "jdoe@example.com",
   375  						"secondary_emails": ["johndoe@chromium.org"],
   376  						"username":         "jdoe",
   377  						"tags":             ["SERVICE_USER"]
   378  					},
   379  					"created":   "2014-05-05 07:15:44.639000000",
   380  					"updated":   "2014-05-05 07:15:44.639000000",
   381  					"project": "example/repo",
   382  					"branch":  "master",
   383  					"current_revision": "deadbeef",
   384  					"submittable": true,
   385  					"is_private": true,
   386  					"meta_rev_id": "cafecafe",
   387  					"hashtags": ["example_tag"],
   388  					"subject": "Added new feature",
   389  					"revisions": {
   390  						"deadbeef": {
   391  							"_number": 1,
   392  							"kind": "REWORK",
   393  							"ref": "refs/changes/123",
   394  							"uploader": {
   395  								"_account_id":      1000096,
   396  								"name":             "John Doe",
   397  								"email":            "jdoe@example.com",
   398  								"secondary_emails": ["johndoe@chromium.org"],
   399  								"username":         "jdoe",
   400  								"tags":             ["SERVICE_USER"]
   401  							},
   402  							"created": "2016-03-29 17:47:23.751000000",
   403  							"description": "first upload",
   404  							"files": {
   405  								"go/to/file.go": {
   406  									"lines_inserted": 32,
   407  									"lines_deleted": 44,
   408  									"size_delta": -567,
   409  									"size": 11984
   410  								}
   411  							},
   412  							"commit": {
   413  								"parents": [{"commit": "deadbeef00"}],
   414  								"author": {
   415  									"name": "John Doe",
   416  									"email": "jdoe@example.com",
   417  									"date": "2014-05-05 07:15:44.639000000",
   418  									"tz": 60
   419  								},
   420  								"committer": {
   421  									"name": "John Doe",
   422  									"email": "jdoe@example.com",
   423  									"date": "2014-05-05 07:15:44.639000000",
   424  									"tz": 60
   425  								},
   426  								"subject": "Title.",
   427  								"message": "Title.\n\nBody is here.\n\nChange-Id: I100deadbeef"
   428  							}
   429  						}
   430  					},
   431  					"labels": {
   432  						"Code-Review": {
   433  							"approved": {
   434  								"name": "Rubber Stamper",
   435  								"email": "rubberstamper@example.com"
   436  							}
   437  						},
   438  						"Commit-Queue": {
   439  							"all": [
   440  								{
   441  									"value": 1,
   442  									"date": "2020-12-13 18:32:35.000000000",
   443  									"permitted_voting_range": {
   444  										"min": 0,
   445  										"max": 2
   446  									},
   447  									"_account_id": 1010101,
   448  									"name": "Dry Runner",
   449  									"email": "dry-runner@example.com",
   450  									"avatars": [
   451  										{
   452  											"url": "https://example.com/photo.jpg",
   453  											"height": 32
   454  										}
   455  									]
   456  								}
   457  							],
   458  							"values": {
   459  								" 0": "Not ready",
   460  								"+1": "Dry run",
   461  								"+2": "Commit"
   462  							},
   463  							"default_value": 0,
   464  							"optional": true
   465  						}
   466  					},
   467  					"messages": [
   468  						{
   469  							"id": "YH-egE",
   470  							"author": {
   471  								"_account_id": 1000096,
   472  								"name": "John Doe",
   473  								"email": "john.doe@example.com",
   474  								"username": "jdoe"
   475  							},
   476  							"date": "2013-03-23 21:34:02.419000000",
   477  							"message": "Patch Set 1:\n\nThis is the first message.",
   478  							"_revision_number": 1,
   479  							"tag": "autogenerated:gerrit:test"
   480  						},
   481  						{
   482  							"id": "WEEdhU",
   483  							"author": {
   484  								"_account_id": 1000097,
   485  								"name": "Jane Roe",
   486  								"email": "jane.roe@example.com",
   487  								"username": "jroe"
   488  							},
   489  							"date": "2013-03-23 21:36:52.332000000",
   490  							"message": "Patch Set 1:\n\nThis is the second message.\n\nWith a line break.",
   491  							"_revision_number": 1
   492  						}
   493  					],
   494  					"requirements": [
   495  						{
   496  							"status": "OK",
   497  							"fallback_text": "nothing more required",
   498  							"type": "alpha-numer1c-type"
   499  						}
   500  					],
   501  					"submit_requirements": [
   502  						{
   503  							"name": "Verified",
   504  							"description": "Submit requirement for the 'Verified' label",
   505  							"status": "NOT_APPLICABLE",
   506  							"is_legacy": false,
   507  							"applicability_expression_result": {
   508  								"fulfilled": false
   509  							}
   510  						},
   511  						{
   512  							"name": "Code-Owners",
   513  							"description": "Code Owners overrides approval",
   514  							"status": "SATISFIED",
   515  							"is_legacy": false,
   516  							"applicability_expression_result": {
   517  								"fulfilled": true
   518  							},
   519  							"submittability_expression_result": {
   520  								"expression": "has:approval_code-owners",
   521  								"fulfilled": true,
   522  								"passing_atoms": ["has:approval_code-owners"],
   523  								"failing_atoms": []
   524  							},
   525  							"override_expression_result": {
   526  								"expression": "label:Owners-Override=+1",
   527  								"fulfilled": false,
   528  								"passing_atoms": [],
   529  								"failing_atoms": ["label:Owners-Override=+1"]
   530  							}
   531  						},
   532  						{
   533  							"name": "Code-Review",
   534  							"description": "Submit requirement for the 'Code-Review' label",
   535  							"status": "UNSATISFIED",
   536  							"is_legacy": false,
   537  							"submittability_expression_result": {
   538  								"expression": "label:Code-Review=MAX,user=non_uploader -label:Code-Review=MIN",
   539  								"fulfilled": false,
   540  								"passing_atoms": [],
   541  								"failing_atoms": [
   542  									"label:Code-Review=MAX,user=non_uploader",
   543  									"label:Code-Review=MIN"
   544  								]
   545  							},
   546  							"override_expression_result": {
   547  								"expression": "label:Bot-Commit=+1",
   548  								"fulfilled": false,
   549  								"passing_atoms": [],
   550  								"failing_atoms": ["label:Bot-Commit=+1"]
   551  							}
   552  						}
   553  					],
   554  					"reviewers": {
   555  						"REVIEWER": [
   556  							{
   557  								"_account_id": 1000096,
   558  								"name": "John Doe",
   559  								"email": "john.doe@example.com",
   560  								"username": "jdoe"
   561  							},
   562  							{
   563  								"_account_id": 1000097,
   564  								"name": "Jane Roe",
   565  								"email": "jane.roe@example.com",
   566  								"username": "jroe"
   567  							}
   568  						]
   569  					}
   570  				}`)
   571  			})
   572  			defer srv.Close()
   573  
   574  			Convey("Basic", func() {
   575  				res, err := c.GetChange(ctx, req)
   576  				So(err, ShouldBeNil)
   577  				So(res, ShouldResemble, expectedChange)
   578  			})
   579  
   580  			Convey("With project", func() {
   581  				req.Project = "infra/luci"
   582  				res, err := c.GetChange(ctx, req)
   583  				So(err, ShouldBeNil)
   584  				So(res, ShouldResembleProto, expectedChange)
   585  				So(actualRequest.URL.EscapedPath(), ShouldEqual, "/changes/infra%2Fluci~1")
   586  			})
   587  
   588  			Convey("Options", func() {
   589  				req.Options = append(req.Options, gerritpb.QueryOption_DETAILED_ACCOUNTS, gerritpb.QueryOption_ALL_COMMITS)
   590  				_, err := c.GetChange(ctx, req)
   591  				So(err, ShouldBeNil)
   592  				So(
   593  					actualRequest.URL.Query()["o"],
   594  					ShouldResemble,
   595  					[]string{"DETAILED_ACCOUNTS", "ALL_COMMITS"},
   596  				)
   597  			})
   598  
   599  			Convey("Meta", func() {
   600  				req.Meta = "deadbeef"
   601  				_, err := c.GetChange(ctx, req)
   602  				So(err, ShouldBeNil)
   603  				So(
   604  					actualRequest.URL.Query()["meta"],
   605  					ShouldResemble,
   606  					[]string{"deadbeef"},
   607  				)
   608  			})
   609  		})
   610  	})
   611  }
   612  
   613  func TestRestCreateChange(t *testing.T) {
   614  	t.Parallel()
   615  	ctx := context.Background()
   616  
   617  	Convey("CreateChange basic", t, func() {
   618  		var actualBody []byte
   619  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
   620  			// ignore errors here, but verify body later.
   621  			actualBody, _ = io.ReadAll(r.Body)
   622  			w.WriteHeader(201)
   623  			w.Header().Set("Content-Type", "application/json")
   624  			fmt.Fprint(w, `)]}'`)
   625  			json.NewEncoder(w).Encode(map[string]any{
   626  				"_number":   1,
   627  				"project":   "example/repo",
   628  				"branch":    "master",
   629  				"change_id": "c1",
   630  				"status":    "NEW",
   631  				"created":   "2014-05-05 07:15:44.639000000",
   632  				"updated":   "2014-05-05 07:15:44.639000000",
   633  			})
   634  		})
   635  		defer srv.Close()
   636  
   637  		req := gerritpb.CreateChangeRequest{
   638  			Project:    "example/repo",
   639  			Ref:        "refs/heads/master",
   640  			Subject:    "example subject",
   641  			BaseCommit: "someOpaqueHash",
   642  		}
   643  		res, err := c.CreateChange(ctx, &req)
   644  		So(err, ShouldBeNil)
   645  		So(res, ShouldResemble, &gerritpb.ChangeInfo{
   646  			Number:      1,
   647  			Project:     "example/repo",
   648  			Ref:         "refs/heads/master",
   649  			Status:      gerritpb.ChangeStatus_NEW,
   650  			Submittable: false,
   651  			Created:     timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")),
   652  			Updated:     timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")),
   653  			Submitted:   timestamppb.New(parseTime("0001-01-01T00:00:00.00000000Z")),
   654  			Branch:      "master",
   655  		})
   656  
   657  		var ci changeInput
   658  		err = json.Unmarshal(actualBody, &ci)
   659  		So(err, ShouldBeNil)
   660  		So(ci, ShouldResemble, changeInput{
   661  			Project:    "example/repo",
   662  			Branch:     "refs/heads/master",
   663  			Subject:    "example subject",
   664  			BaseCommit: "someOpaqueHash",
   665  		})
   666  	})
   667  }
   668  
   669  func TestSubmitRevision(t *testing.T) {
   670  	t.Parallel()
   671  	ctx := context.Background()
   672  
   673  	Convey("SubmitRevision", t, func() {
   674  		var actualURL *url.URL
   675  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
   676  			actualURL = r.URL
   677  			w.WriteHeader(200)
   678  			w.Header().Set("Content-Type", "application/json")
   679  			fmt.Fprint(w, `)]}'`)
   680  			json.NewEncoder(w).Encode(map[string]any{
   681  				"status": "MERGED",
   682  			})
   683  		})
   684  		defer srv.Close()
   685  
   686  		req := &gerritpb.SubmitRevisionRequest{
   687  			Number:     42,
   688  			RevisionId: "someRevision",
   689  			Project:    "someProject",
   690  		}
   691  		res, err := c.SubmitRevision(ctx, req)
   692  		So(err, ShouldBeNil)
   693  		So(res, ShouldResembleProto, &gerritpb.SubmitInfo{
   694  			Status: gerritpb.ChangeStatus_MERGED,
   695  		})
   696  		So(actualURL.Path, ShouldEqual, "/changes/someProject~42/revisions/someRevision/submit")
   697  	})
   698  }
   699  
   700  func TestRestChangeEditFileContent(t *testing.T) {
   701  	t.Parallel()
   702  	ctx := context.Background()
   703  
   704  	Convey("ChangeEditFileContent basic", t, func() {
   705  		// large enough?
   706  		var actualBody []byte
   707  		var actualURL *url.URL
   708  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
   709  			actualURL = r.URL
   710  			// ignore errors here, but verify body later.
   711  			actualBody, _ = io.ReadAll(r.Body)
   712  			// API returns 204 on success.
   713  			w.WriteHeader(204)
   714  		})
   715  		defer srv.Close()
   716  
   717  		_, err := c.ChangeEditFileContent(ctx, &gerritpb.ChangeEditFileContentRequest{
   718  			Number:   42,
   719  			Project:  "some/project",
   720  			FilePath: "some/path+foo",
   721  			Content:  []byte("changed file"),
   722  		})
   723  		So(err, ShouldBeNil)
   724  		So(actualURL.RawPath, ShouldEqual, "/changes/some%2Fproject~42/edit/some%2Fpath%2Bfoo")
   725  		So(actualURL.Path, ShouldEqual, "/changes/some/project~42/edit/some/path+foo")
   726  		So(actualBody, ShouldResemble, []byte("changed file"))
   727  	})
   728  }
   729  
   730  // TODO (yulanlin): Assert body verbatim without decoding
   731  func TestAddReviewer(t *testing.T) {
   732  	t.Parallel()
   733  	ctx := context.Background()
   734  
   735  	Convey("Add reviewer to cc basic", t, func() {
   736  		var actualURL *url.URL
   737  		var actualBody []byte
   738  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
   739  			actualURL = r.URL
   740  			// ignore the error because body contents will be checked
   741  			actualBody, _ = io.ReadAll(r.Body)
   742  			w.WriteHeader(200)
   743  			w.Header().Set("Content-Type", "application/json")
   744  			fmt.Fprint(w, `)]}'
   745  			{
   746  				"input": "ccer@test.com",
   747  				"ccs": [
   748  					{
   749  						"_account_id": 10001,
   750  						"name": "Reviewer Review",
   751  						"approvals": {
   752  							"Code-Review": " 0"
   753  						}
   754  					}
   755  				]
   756  			}`)
   757  		})
   758  		defer srv.Close()
   759  
   760  		req := &gerritpb.AddReviewerRequest{
   761  			Number:    42,
   762  			Project:   "someproject",
   763  			Reviewer:  "ccer@test.com",
   764  			State:     gerritpb.AddReviewerRequest_ADD_REVIEWER_STATE_CC,
   765  			Confirmed: true,
   766  			Notify:    gerritpb.Notify_NOTIFY_OWNER,
   767  		}
   768  		res, err := c.AddReviewer(ctx, req)
   769  		So(err, ShouldBeNil)
   770  
   771  		// assert the request was as expected
   772  		So(actualURL.Path, ShouldEqual, "/changes/someproject~42/reviewers")
   773  		var body addReviewerRequest
   774  		err = json.Unmarshal(actualBody, &body)
   775  		if err != nil {
   776  			t.Logf("failed to decode req body: %v\n", err)
   777  		}
   778  		So(body, ShouldResemble, addReviewerRequest{
   779  			Reviewer:  "ccer@test.com",
   780  			State:     "CC",
   781  			Confirmed: true,
   782  			Notify:    "OWNER",
   783  		})
   784  
   785  		// assert the result was as expected
   786  		So(res, ShouldResemble, &gerritpb.AddReviewerResult{
   787  			Input:     "ccer@test.com",
   788  			Reviewers: []*gerritpb.ReviewerInfo{},
   789  			Ccs: []*gerritpb.ReviewerInfo{
   790  				{
   791  					Account: &gerritpb.AccountInfo{
   792  						Name:      "Reviewer Review",
   793  						AccountId: 10001,
   794  					},
   795  					Approvals: map[string]int32{
   796  						"Code-Review": 0,
   797  					},
   798  				},
   799  			},
   800  		})
   801  	})
   802  }
   803  
   804  func TestDeleteReviewer(t *testing.T) {
   805  	t.Parallel()
   806  	ctx := context.Background()
   807  
   808  	Convey("Delete reviewer", t, func() {
   809  		var actualURL *url.URL
   810  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
   811  			actualURL = r.URL
   812  			// API returns 204 on success.
   813  			w.WriteHeader(204)
   814  		})
   815  		defer srv.Close()
   816  
   817  		_, err := c.DeleteReviewer(ctx, &gerritpb.DeleteReviewerRequest{
   818  			Number:    42,
   819  			Project:   "someproject",
   820  			AccountId: "jdoe@example.com",
   821  		})
   822  		So(err, ShouldBeNil)
   823  		So(actualURL.Path, ShouldEqual, "/changes/someproject~42/reviewers/jdoe@example.com/delete")
   824  	})
   825  }
   826  
   827  func TestSetReview(t *testing.T) {
   828  	t.Parallel()
   829  	ctx := context.Background()
   830  
   831  	Convey("Set Review", t, func() {
   832  		var actualURL *url.URL
   833  		var actualRawBody []byte
   834  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
   835  			actualURL = r.URL
   836  			actualRawBody, _ = io.ReadAll(r.Body)
   837  			w.WriteHeader(200)
   838  			w.Header().Set("Content-Type", "application/json")
   839  			fmt.Fprint(w, `)]}'
   840  			{
   841  				"labels": {
   842  					"Code-Review": -1
   843  				}
   844  			}`)
   845  		})
   846  		defer srv.Close()
   847  
   848  		res, err := c.SetReview(ctx, &gerritpb.SetReviewRequest{
   849  			Number:     42,
   850  			Project:    "someproject",
   851  			RevisionId: "somerevision",
   852  			Message:    "This is a message",
   853  			Labels: map[string]int32{
   854  				"Code-Review": -1,
   855  			},
   856  			Tag:    "autogenerated:cq",
   857  			Notify: gerritpb.Notify_NOTIFY_OWNER,
   858  			NotifyDetails: &gerritpb.NotifyDetails{
   859  				Recipients: []*gerritpb.NotifyDetails_Recipient{
   860  					{
   861  						RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_TO,
   862  						Info: &gerritpb.NotifyDetails_Info{
   863  							Accounts: []int64{4, 5, 3},
   864  						},
   865  					},
   866  					{
   867  						RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_TO,
   868  						Info: &gerritpb.NotifyDetails_Info{
   869  							Accounts: []int64{2, 3, 1},
   870  							// 3 is overlapping with the first recipient,
   871  						},
   872  					},
   873  					{
   874  						RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_BCC,
   875  						Info: &gerritpb.NotifyDetails_Info{
   876  							Accounts: []int64{6, 1},
   877  						},
   878  					},
   879  				},
   880  			},
   881  			OnBehalfOf: 10001,
   882  			Ready:      true,
   883  			AddToAttentionSet: []*gerritpb.AttentionSetInput{
   884  				{User: "10002", Reason: "passed presubmit"},
   885  			},
   886  			RemoveFromAttentionSet: []*gerritpb.AttentionSetInput{
   887  				{User: "10001", Reason: "passed presubmit"},
   888  			},
   889  			IgnoreAutomaticAttentionSetRules: true,
   890  		})
   891  		So(err, ShouldBeNil)
   892  		So(actualURL.Path, ShouldEqual, "/changes/someproject~42/revisions/somerevision/review")
   893  
   894  		var actualBody, expectedBody map[string]any
   895  		So(json.Unmarshal(actualRawBody, &actualBody), ShouldBeNil)
   896  		So(json.Unmarshal([]byte(`{
   897  			"message": "This is a message",
   898  			"labels": {
   899  				"Code-Review": -1
   900  			},
   901  			"tag": "autogenerated:cq",
   902  			"notify": "OWNER",
   903  			"notify_details": {
   904  				"TO":  {"accounts": [1, 2, 3, 4, 5]},
   905  				"BCC": {"accounts": [1, 6]}
   906  			},
   907  			"on_behalf_of": 10001,
   908  			"ready": true,
   909  			"add_to_attention_set": [
   910  				{"user": "10002", "reason": "passed presubmit"}
   911  			],
   912  			"remove_from_attention_set": [
   913  				{"user": "10001", "reason": "passed presubmit"}
   914  			],
   915  			"ignore_automatic_attention_set_rules": true
   916  		}`), &expectedBody), ShouldBeNil)
   917  		So(actualBody, ShouldResemble, expectedBody)
   918  
   919  		So(res, ShouldResembleProto, &gerritpb.ReviewResult{
   920  			Labels: map[string]int32{
   921  				"Code-Review": -1,
   922  			},
   923  		})
   924  	})
   925  
   926  	Convey("Set Review can add reviewers", t, func() {
   927  		var actualURL *url.URL
   928  		var actualRawBody []byte
   929  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
   930  			actualURL = r.URL
   931  			actualRawBody, _ = io.ReadAll(r.Body)
   932  			w.WriteHeader(200)
   933  			w.Header().Set("Content-Type", "application/json")
   934  			fmt.Fprint(w, `)]}'
   935  			{
   936  				"reviewers": {
   937  					"jdoe@example.com": {
   938  						"input": "jdoe@example.com",
   939  						"reviewers": [
   940  							{
   941  								"_account_id": 10001,
   942  								"name": "John Doe",
   943  								"email": "jdoe@example.com",
   944  								"approvals": {
   945  									"Verified": "0",
   946  									"Code-Review": "0"
   947  								}
   948  							}
   949  						]
   950  					},
   951  					"10003": {
   952  						"input": "10003",
   953  						"ccs": [
   954  							{
   955  								"_account_id": 10003,
   956  								"name": "Eve Smith",
   957  								"email": "esmith@example.com",
   958  								"approvals": {
   959  									"Verified": "0",
   960  									"Code-Review": "0"
   961  								}
   962  							}
   963  						]
   964  					}
   965  				}
   966  			}`)
   967  		})
   968  		defer srv.Close()
   969  
   970  		res, err := c.SetReview(ctx, &gerritpb.SetReviewRequest{
   971  			Number:     42,
   972  			Project:    "someproject",
   973  			RevisionId: "somerevision",
   974  			Message:    "This is a message",
   975  			Reviewers: []*gerritpb.ReviewerInput{
   976  				{
   977  					Reviewer: "jdoe@example.com",
   978  				},
   979  				{
   980  					Reviewer: "10003",
   981  					State:    gerritpb.ReviewerInput_REVIEWER_INPUT_STATE_CC,
   982  				},
   983  			},
   984  		})
   985  		So(err, ShouldBeNil)
   986  		So(actualURL.Path, ShouldEqual, "/changes/someproject~42/revisions/somerevision/review")
   987  
   988  		var actualBody, expectedBody map[string]any
   989  		So(json.Unmarshal(actualRawBody, &actualBody), ShouldBeNil)
   990  		So(json.Unmarshal([]byte(`{
   991  			"message": "This is a message",
   992  			"reviewers": [
   993  				{"reviewer": "jdoe@example.com"},
   994  				{"reviewer": "10003", "state": "CC"}
   995  			]
   996  		}`), &expectedBody), ShouldBeNil)
   997  		So(actualBody, ShouldResemble, expectedBody)
   998  
   999  		So(res, ShouldResembleProto, &gerritpb.ReviewResult{
  1000  			Reviewers: map[string]*gerritpb.AddReviewerResult{
  1001  				"jdoe@example.com": {
  1002  					Input: "jdoe@example.com",
  1003  					Reviewers: []*gerritpb.ReviewerInfo{
  1004  						{
  1005  							Account: &gerritpb.AccountInfo{
  1006  								Name:      "John Doe",
  1007  								Email:     "jdoe@example.com",
  1008  								AccountId: 10001,
  1009  							},
  1010  							Approvals: map[string]int32{
  1011  								"Verified":    0,
  1012  								"Code-Review": 0,
  1013  							},
  1014  						},
  1015  					},
  1016  				},
  1017  				"10003": {
  1018  					Input: "10003",
  1019  					Ccs: []*gerritpb.ReviewerInfo{
  1020  						{
  1021  							Account: &gerritpb.AccountInfo{
  1022  								Name:      "Eve Smith",
  1023  								Email:     "esmith@example.com",
  1024  								AccountId: 10003,
  1025  							},
  1026  							Approvals: map[string]int32{
  1027  								"Verified":    0,
  1028  								"Code-Review": 0,
  1029  							},
  1030  						},
  1031  					},
  1032  				},
  1033  			},
  1034  		})
  1035  	})
  1036  }
  1037  
  1038  func TestAddToAttentionSet(t *testing.T) {
  1039  	t.Parallel()
  1040  	ctx := context.Background()
  1041  
  1042  	Convey("Add to attention set", t, func() {
  1043  		var actualURL *url.URL
  1044  		var actualBody []byte
  1045  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1046  			actualURL = r.URL
  1047  			// ignore the error because body contents will be checked
  1048  			actualBody, _ = io.ReadAll(r.Body)
  1049  			w.WriteHeader(200)
  1050  			w.Header().Set("Content-Type", "application/json")
  1051  			fmt.Fprint(w, `)]}'
  1052  				{
  1053  					"_account_id": 10001,
  1054  					"name": "FYI reviewer",
  1055  					"email": "fyi@test.com",
  1056  					"username": "fyi"
  1057  				}`)
  1058  		})
  1059  		defer srv.Close()
  1060  
  1061  		req := &gerritpb.AttentionSetRequest{
  1062  			Project: "someproject",
  1063  			Number:  42,
  1064  			Input: &gerritpb.AttentionSetInput{
  1065  				User:   "fyi@test.com",
  1066  				Reason: "For awareness",
  1067  				Notify: gerritpb.Notify_NOTIFY_ALL,
  1068  			},
  1069  		}
  1070  		res, err := c.AddToAttentionSet(ctx, req)
  1071  		So(err, ShouldBeNil)
  1072  
  1073  		// assert the request was as expected
  1074  		So(actualURL.Path, ShouldEqual, "/changes/someproject~42/attention")
  1075  		expectedBody, err := json.Marshal(attentionSetInput{
  1076  			User:   "fyi@test.com",
  1077  			Reason: "For awareness",
  1078  			Notify: "ALL",
  1079  		})
  1080  		if err != nil {
  1081  			t.Logf("failed to encode expected body: %v\n", err)
  1082  		}
  1083  		So(actualBody, ShouldResemble, expectedBody)
  1084  
  1085  		// assert the result was as expected
  1086  		So(res, ShouldResemble, &gerritpb.AccountInfo{
  1087  			AccountId: 10001,
  1088  			Name:      "FYI reviewer",
  1089  			Email:     "fyi@test.com",
  1090  			Username:  "fyi",
  1091  		})
  1092  	})
  1093  }
  1094  
  1095  func TestRevertChange(t *testing.T) {
  1096  	t.Parallel()
  1097  	ctx := context.Background()
  1098  
  1099  	Convey("RevertChange", t, func() {
  1100  		Convey("Validate args", func() {
  1101  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {})
  1102  			defer srv.Close()
  1103  
  1104  			_, err := c.RevertChange(ctx, &gerritpb.RevertChangeRequest{})
  1105  			So(err, ShouldErrLike, "number must be positive")
  1106  		})
  1107  
  1108  		Convey("OK", func() {
  1109  			req := &gerritpb.RevertChangeRequest{
  1110  				Number:  3964,
  1111  				Message: "This is the message added to the revert CL.",
  1112  			}
  1113  
  1114  			expectedChange := &gerritpb.ChangeInfo{
  1115  				Number: 3965,
  1116  				Owner: &gerritpb.AccountInfo{
  1117  					AccountId:       1000096,
  1118  					Name:            "John Doe",
  1119  					Email:           "jdoe@example.com",
  1120  					SecondaryEmails: []string{"johndoe@chromium.org"},
  1121  					Username:        "jdoe",
  1122  					Tags:            []string{"SERVICE_USER"},
  1123  				},
  1124  				Project:   "example/repo",
  1125  				Ref:       "refs/heads/master",
  1126  				Status:    gerritpb.ChangeStatus_NEW,
  1127  				Created:   timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")),
  1128  				Updated:   timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")),
  1129  				Submitted: timestamppb.New(parseTime("0001-01-01T00:00:00.00000000Z")),
  1130  				Messages: []*gerritpb.ChangeMessageInfo{
  1131  					{
  1132  						Id: "YH-egE",
  1133  						Author: &gerritpb.AccountInfo{
  1134  							AccountId: 1000096,
  1135  							Name:      "John Doe",
  1136  							Email:     "john.doe@example.com",
  1137  							Username:  "jdoe",
  1138  						},
  1139  						Date:    timestamppb.New(parseTime("2013-03-23T21:34:02.419000000Z")),
  1140  						Message: "Patch Set 1:\n\nThis is the message added to the revert CL.",
  1141  					},
  1142  				},
  1143  				Branch: "master",
  1144  			}
  1145  
  1146  			var actualRequest *http.Request
  1147  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1148  				actualRequest = r
  1149  				w.WriteHeader(200)
  1150  				w.Header().Set("Content-Type", "application/json")
  1151  				fmt.Fprint(w, `)]}'{
  1152  					"_number": 3965,
  1153  					"status": "NEW",
  1154  					"owner": {
  1155  						"_account_id":      1000096,
  1156  						"name":             "John Doe",
  1157  						"email":            "jdoe@example.com",
  1158  						"secondary_emails": ["johndoe@chromium.org"],
  1159  						"username":         "jdoe",
  1160  						"tags":             ["SERVICE_USER"]
  1161  					},
  1162  					"created":   "2014-05-05 07:15:44.639000000",
  1163  					"updated":   "2014-05-05 07:15:44.639000000",
  1164  					"project": "example/repo",
  1165  					"branch":  "master",
  1166  					"messages": [
  1167  						{
  1168  							"id": "YH-egE",
  1169  							"author": {
  1170  								"_account_id": 1000096,
  1171  								"name": "John Doe",
  1172  								"email": "john.doe@example.com",
  1173  								"username": "jdoe"
  1174  							},
  1175  							"date": "2013-03-23 21:34:02.419000000",
  1176  							"message": "Patch Set 1:\n\nThis is the message added to the revert CL.",
  1177  							"_revision_number": 1
  1178  						}
  1179  					]
  1180  				}`)
  1181  			})
  1182  			defer srv.Close()
  1183  
  1184  			Convey("Basic", func() {
  1185  				res, err := c.RevertChange(ctx, req)
  1186  				So(err, ShouldBeNil)
  1187  				So(res, ShouldResemble, expectedChange)
  1188  				So(actualRequest.URL.EscapedPath(), ShouldEqual, "/changes/3964/revert")
  1189  			})
  1190  
  1191  			Convey("With project", func() {
  1192  				req.Project = "infra/luci"
  1193  				res, err := c.RevertChange(ctx, req)
  1194  				So(err, ShouldBeNil)
  1195  				So(res, ShouldResembleProto, expectedChange)
  1196  				So(actualRequest.URL.EscapedPath(), ShouldEqual, "/changes/infra%2Fluci~3964/revert")
  1197  			})
  1198  		})
  1199  	})
  1200  }
  1201  
  1202  func TestGetMergeable(t *testing.T) {
  1203  	t.Parallel()
  1204  	ctx := context.Background()
  1205  
  1206  	Convey("GetMergeable basic", t, func() {
  1207  		var actualURL *url.URL
  1208  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1209  			actualURL = r.URL
  1210  			w.WriteHeader(200)
  1211  			w.Header().Set("Content-Type", "application/json")
  1212  			fmt.Fprint(w, `)]}'
  1213          {
  1214            "submit_type": "CHERRY_PICK",
  1215            "strategy": "simple-two-way-in-core",
  1216            "mergeable": true,
  1217            "commit_merged": false,
  1218            "content_merged": false,
  1219            "conflicts": [
  1220              "conflict1",
  1221              "conflict2"
  1222            ],
  1223            "mergeable_into": [
  1224              "my_branch_1"
  1225            ]
  1226          }`)
  1227  		})
  1228  		defer srv.Close()
  1229  
  1230  		mi, err := c.GetMergeable(ctx, &gerritpb.GetMergeableRequest{
  1231  			Number:     42,
  1232  			Project:    "someproject",
  1233  			RevisionId: "somerevision",
  1234  		})
  1235  		So(err, ShouldBeNil)
  1236  		So(actualURL.Path, ShouldEqual, "/changes/someproject~42/revisions/somerevision/mergeable")
  1237  		So(mi, ShouldResemble, &gerritpb.MergeableInfo{
  1238  			SubmitType:    gerritpb.MergeableInfo_CHERRY_PICK,
  1239  			Strategy:      gerritpb.MergeableStrategy_SIMPLE_TWO_WAY_IN_CORE,
  1240  			Mergeable:     true,
  1241  			CommitMerged:  false,
  1242  			ContentMerged: false,
  1243  			Conflicts:     []string{"conflict1", "conflict2"},
  1244  			MergeableInto: []string{"my_branch_1"},
  1245  		})
  1246  	})
  1247  }
  1248  
  1249  func TestListFiles(t *testing.T) {
  1250  	t.Parallel()
  1251  	ctx := context.Background()
  1252  
  1253  	Convey("ListFiles basic", t, func() {
  1254  		var actualURL *url.URL
  1255  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1256  			actualURL = r.URL
  1257  			w.WriteHeader(200)
  1258  			w.Header().Set("Content-Type", "application/json")
  1259  			fmt.Fprint(w, `)]}'
  1260  				{
  1261  					"gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": {
  1262  						"lines_inserted": 123456
  1263  					},
  1264  					"file2": {
  1265  						"size": 7
  1266  					}
  1267  				}`)
  1268  		})
  1269  		defer srv.Close()
  1270  
  1271  		mi, err := c.ListFiles(ctx, &gerritpb.ListFilesRequest{
  1272  			Number:     42,
  1273  			Project:    "someproject",
  1274  			RevisionId: "somerevision",
  1275  			Parent:     999,
  1276  		})
  1277  		So(err, ShouldBeNil)
  1278  		So(actualURL.Path, ShouldEqual, "/changes/someproject~42/revisions/somerevision/files/")
  1279  		So(actualURL.Query().Get("parent"), ShouldEqual, "999")
  1280  		So(mi, ShouldResemble, &gerritpb.ListFilesResponse{
  1281  			Files: map[string]*gerritpb.FileInfo{
  1282  				"gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": {
  1283  					LinesInserted: 123456,
  1284  				},
  1285  				"file2": {
  1286  					Size: 7,
  1287  				},
  1288  			},
  1289  		})
  1290  	})
  1291  }
  1292  
  1293  func TestGetRelatedChanges(t *testing.T) {
  1294  	t.Parallel()
  1295  	ctx := context.Background()
  1296  
  1297  	Convey("GetRelatedChanges works", t, func() {
  1298  		var actualURL *url.URL
  1299  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1300  			actualURL = r.URL
  1301  			w.WriteHeader(200)
  1302  			w.Header().Set("Content-Type", "application/json")
  1303  			// Taken from
  1304  			// https://chromium-review.googlesource.com/changes/playground%2Fgerrit-cq~1563638/revisions/2/related
  1305  			fmt.Fprint(w, `)]}'
  1306  				{
  1307  				  "changes": [
  1308  				    {
  1309  				      "project": "playground/gerrit-cq",
  1310  				      "change_id": "If00fa4f207440d7f12fbfff8c05afa9077ab0c21",
  1311  				      "commit": {
  1312  				        "commit": "4d048b016cb4df4d5d2805f0d3d1042cb1d80671",
  1313  				        "parents": [
  1314  				          {
  1315  				            "commit": "cd7db096c014399c369ddddd319708c3f46752f5"
  1316  				          }
  1317  				        ],
  1318  				        "author": {
  1319  				          "name": "Andrii Shyshkalov",
  1320  				          "email": "tandrii@chromium.org",
  1321  				          "date": "2019-04-11 06:41:01.000000000",
  1322  				          "tz": -420
  1323  				        },
  1324  				        "subject": "p3 change"
  1325  				      },
  1326  				      "_change_number": 1563639,
  1327  				      "_revision_number": 1,
  1328  				      "_current_revision_number": 1,
  1329  				      "status": "NEW"
  1330  				    },
  1331  				    {
  1332  				      "project": "playground/gerrit-cq",
  1333  				      "change_id": "I80bf05eb9124dc126490820ec192c77a24938622",
  1334  				      "commit": {
  1335  				        "commit": "bce1f3beea01b8b282001b01bd9ea442730d578e",
  1336  				        "parents": [
  1337  				          {
  1338  				            "commit": "fdd1f6d3875e68c99303ebfb25dd5d097e91c83f"
  1339  				          }
  1340  				        ],
  1341  				        "author": {
  1342  				          "name": "Andrii Shyshkalov",
  1343  				          "email": "tandrii@chromium.org",
  1344  				          "date": "2019-04-11 06:40:28.000000000",
  1345  				          "tz": -420
  1346  				        },
  1347  				        "subject": "p2 change"
  1348  				      },
  1349  				      "_change_number": 1563638,
  1350  				      "_revision_number": 2,
  1351  				      "_current_revision_number": 2,
  1352  				      "status": "NEW"
  1353  				    },
  1354  				    {
  1355  				      "project": "playground/gerrit-cq",
  1356  				      "change_id": "Icf12c110abc0cbc0c7d01a40dc047683634a62d7",
  1357  				      "commit": {
  1358  				        "commit": "fdd1f6d3875e68c99303ebfb25dd5d097e91c83f",
  1359  				        "parents": [
  1360  				          {
  1361  				            "commit": "f8e5384ee591cd5105113098d24c60e750b6c4f6"
  1362  				          }
  1363  				        ],
  1364  				        "author": {
  1365  				          "name": "Andrii Shyshkalov",
  1366  				          "email": "tandrii@chromium.org",
  1367  				          "date": "2019-04-11 06:40:18.000000000",
  1368  				          "tz": -420
  1369  				        },
  1370  				        "subject": "p1 change"
  1371  				      },
  1372  				      "_change_number": 1563637,
  1373  				      "_revision_number": 1,
  1374  				      "_current_revision_number": 1,
  1375  				      "status": "NEW"
  1376  				    }
  1377  				  ]
  1378  				}
  1379  			`)
  1380  		})
  1381  		defer srv.Close()
  1382  
  1383  		rcs, err := c.GetRelatedChanges(ctx, &gerritpb.GetRelatedChangesRequest{
  1384  			Number:     1563638,
  1385  			Project:    "playground/gerrit-cq",
  1386  			RevisionId: "2",
  1387  		})
  1388  		So(err, ShouldBeNil)
  1389  		So(actualURL.EscapedPath(), ShouldEqual, "/changes/playground%2Fgerrit-cq~1563638/revisions/2/related")
  1390  		So(rcs, ShouldResembleProto, &gerritpb.GetRelatedChangesResponse{
  1391  			Changes: []*gerritpb.GetRelatedChangesResponse_ChangeAndCommit{
  1392  				{
  1393  					Project: "playground/gerrit-cq",
  1394  					Commit: &gerritpb.CommitInfo{
  1395  						Id:      "4d048b016cb4df4d5d2805f0d3d1042cb1d80671",
  1396  						Parents: []*gerritpb.CommitInfo_Parent{{Id: "cd7db096c014399c369ddddd319708c3f46752f5"}},
  1397  						Author: &gerritpb.GitPersonInfo{
  1398  							Name:  "Andrii Shyshkalov",
  1399  							Email: "tandrii@chromium.org",
  1400  						},
  1401  					},
  1402  					Number:          1563639,
  1403  					Patchset:        1,
  1404  					CurrentPatchset: 1,
  1405  					Status:          gerritpb.ChangeStatus_NEW,
  1406  				},
  1407  				{
  1408  					Project: "playground/gerrit-cq",
  1409  					Commit: &gerritpb.CommitInfo{
  1410  						Id:      "bce1f3beea01b8b282001b01bd9ea442730d578e",
  1411  						Parents: []*gerritpb.CommitInfo_Parent{{Id: "fdd1f6d3875e68c99303ebfb25dd5d097e91c83f"}},
  1412  						Author: &gerritpb.GitPersonInfo{
  1413  							Name:  "Andrii Shyshkalov",
  1414  							Email: "tandrii@chromium.org",
  1415  						},
  1416  					},
  1417  					Number:          1563638,
  1418  					Patchset:        2,
  1419  					CurrentPatchset: 2,
  1420  					Status:          gerritpb.ChangeStatus_NEW,
  1421  				},
  1422  				{
  1423  					Project: "playground/gerrit-cq",
  1424  					Commit: &gerritpb.CommitInfo{
  1425  						Id:      "fdd1f6d3875e68c99303ebfb25dd5d097e91c83f",
  1426  						Parents: []*gerritpb.CommitInfo_Parent{{Id: "f8e5384ee591cd5105113098d24c60e750b6c4f6"}},
  1427  						Author: &gerritpb.GitPersonInfo{
  1428  							Name:  "Andrii Shyshkalov",
  1429  							Email: "tandrii@chromium.org",
  1430  						},
  1431  					},
  1432  					Number:          1563637,
  1433  					Patchset:        1,
  1434  					CurrentPatchset: 1,
  1435  					Status:          gerritpb.ChangeStatus_NEW,
  1436  				},
  1437  			},
  1438  		})
  1439  	})
  1440  }
  1441  
  1442  func TestGetFileOwners(t *testing.T) {
  1443  	t.Parallel()
  1444  	ctx := context.Background()
  1445  	Convey("Get Owners: ", t, func() {
  1446  		Convey("Details", func() {
  1447  			var actualURL *url.URL
  1448  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1449  				actualURL = r.URL
  1450  				w.WriteHeader(200)
  1451  				w.Header().Set("Content-Type", "application/json")
  1452  				fmt.Fprint(w, `)]}'
  1453  				{"code_owners":[{
  1454  					"account":{
  1455  						"_account_id":1000096,
  1456  						"name":"User Name",
  1457  						"email":"user@test.com",
  1458  						"avatars":[{"url":"https://test.com/photo.jpg","height":32}]
  1459  					}}]}`)
  1460  			})
  1461  			defer srv.Close()
  1462  
  1463  			resp, err := c.ListFileOwners(ctx, &gerritpb.ListFileOwnersRequest{
  1464  				Project: "projectName",
  1465  				Ref:     "main",
  1466  				Path:    "path/to/file",
  1467  				Options: &gerritpb.AccountOptions{
  1468  					Details: true,
  1469  				},
  1470  			})
  1471  			So(err, ShouldBeNil)
  1472  			So(actualURL.Path, ShouldEqual, "/projects/projectName/branches/main/code_owners/path/to/file")
  1473  			So(actualURL.Query().Get("o"), ShouldEqual, "DETAILS")
  1474  			So(resp, ShouldResemble, &gerritpb.ListOwnersResponse{
  1475  				Owners: []*gerritpb.OwnerInfo{
  1476  					{
  1477  						Account: &gerritpb.AccountInfo{
  1478  							AccountId: 1000096,
  1479  							Name:      "User Name",
  1480  							Email:     "user@test.com",
  1481  						},
  1482  					},
  1483  				},
  1484  			})
  1485  		})
  1486  		Convey("All Emails", func() {
  1487  			var actualURL *url.URL
  1488  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1489  				actualURL = r.URL
  1490  				w.WriteHeader(200)
  1491  				w.Header().Set("Content-Type", "application/json")
  1492  				fmt.Fprint(w, `)]}'
  1493  				{"code_owners": [{
  1494  					"account": {
  1495  						"_account_id": 1000096,
  1496  						"email": "test@test.com",
  1497  						"secondary_emails": ["alt@test.com"]
  1498  					}}]}`)
  1499  			})
  1500  			defer srv.Close()
  1501  
  1502  			resp, err := c.ListFileOwners(ctx, &gerritpb.ListFileOwnersRequest{
  1503  				Project: "projectName",
  1504  				Ref:     "main",
  1505  				Path:    "path/to/file",
  1506  				Options: &gerritpb.AccountOptions{
  1507  					AllEmails: true,
  1508  				},
  1509  			})
  1510  			So(err, ShouldBeNil)
  1511  			So(actualURL.Path, ShouldEqual, "/projects/projectName/branches/main/code_owners/path/to/file")
  1512  			So(actualURL.Query().Get("o"), ShouldEqual, "ALL_EMAILS")
  1513  			So(resp, ShouldResemble, &gerritpb.ListOwnersResponse{
  1514  				Owners: []*gerritpb.OwnerInfo{
  1515  					{
  1516  						Account: &gerritpb.AccountInfo{
  1517  							AccountId:       1000096,
  1518  							Email:           "test@test.com",
  1519  							SecondaryEmails: []string{"alt@test.com"},
  1520  						},
  1521  					},
  1522  				},
  1523  			})
  1524  		})
  1525  	})
  1526  }
  1527  
  1528  func TestListProjects(t *testing.T) {
  1529  	t.Parallel()
  1530  	ctx := context.Background()
  1531  
  1532  	Convey("List Projects", t, func() {
  1533  		Convey("...works for a single ref", func() {
  1534  			var actualURL *url.URL
  1535  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1536  				actualURL = r.URL
  1537  				w.WriteHeader(200)
  1538  				w.Header().Set("Content-Type", "application/json")
  1539  				fmt.Fprint(w, `)]}'
  1540  				{
  1541  					"android_apks": {
  1542  					  "id": "android_apks",
  1543  					  "state": "ACTIVE",
  1544  					  "branches": {
  1545  						"main": "82264ea131fcc2a386b83e38b962b370315c7c93"
  1546  					  },
  1547  					  "web_links": [
  1548  						{
  1549  						  "name": "gitiles",
  1550  						  "url": "https://chromium.googlesource.com/android_apks/",
  1551  						  "target": "_blank"
  1552  						}
  1553  					  ]
  1554  					}
  1555  				  }`)
  1556  			})
  1557  			defer srv.Close()
  1558  
  1559  			projects, err := c.ListProjects(ctx, &gerritpb.ListProjectsRequest{
  1560  				Refs: []string{"refs/heads/main"},
  1561  			})
  1562  			So(err, ShouldBeNil)
  1563  			So(actualURL.Path, ShouldEqual, "/projects/")
  1564  			So(actualURL.Query().Get("b"), ShouldEqual, "refs/heads/main")
  1565  			So(projects, ShouldResemble, &gerritpb.ListProjectsResponse{
  1566  				Projects: map[string]*gerritpb.ProjectInfo{
  1567  					"android_apks": {
  1568  						Name:  "android_apks",
  1569  						State: gerritpb.ProjectInfo_PROJECT_STATE_ACTIVE,
  1570  						Refs: map[string]string{
  1571  							"refs/heads/main": "82264ea131fcc2a386b83e38b962b370315c7c93",
  1572  						},
  1573  						WebLinks: []*gerritpb.WebLinkInfo{
  1574  							{
  1575  								Name: "gitiles",
  1576  								Url:  "https://chromium.googlesource.com/android_apks/",
  1577  							},
  1578  						},
  1579  					},
  1580  				},
  1581  			})
  1582  		})
  1583  
  1584  		Convey("...works for multiple refs", func() {
  1585  			var actualURL *url.URL
  1586  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1587  				actualURL = r.URL
  1588  				w.WriteHeader(200)
  1589  				w.Header().Set("Content-Type", "application/json")
  1590  				fmt.Fprint(w, `)]}'
  1591  				{
  1592  					"android_apks": {
  1593  					  "id": "android_apks",
  1594  					  "state": "ACTIVE",
  1595  					  "branches": {
  1596  						"main": "82264ea131fcc2a386b83e38b962b370315c7c93",
  1597  						"master": "82264ea131fcc2a386b83e38b962b370315c7c93"
  1598  					  },
  1599  					  "web_links": [
  1600  						{
  1601  						  "name": "gitiles",
  1602  						  "url": "https://chromium.googlesource.com/android_apks/",
  1603  						  "target": "_blank"
  1604  						}
  1605  					  ]
  1606  					}
  1607  				  }`)
  1608  			})
  1609  			defer srv.Close()
  1610  
  1611  			projects, err := c.ListProjects(ctx, &gerritpb.ListProjectsRequest{
  1612  				Refs: []string{"refs/heads/main", "refs/heads/master"},
  1613  			})
  1614  			So(err, ShouldBeNil)
  1615  			So(actualURL.Path, ShouldEqual, "/projects/")
  1616  			So(actualURL.Query()["b"], ShouldResemble, []string{"refs/heads/main", "refs/heads/master"})
  1617  			So(projects, ShouldResemble, &gerritpb.ListProjectsResponse{
  1618  				Projects: map[string]*gerritpb.ProjectInfo{
  1619  					"android_apks": {
  1620  						Name:  "android_apks",
  1621  						State: gerritpb.ProjectInfo_PROJECT_STATE_ACTIVE,
  1622  						Refs: map[string]string{
  1623  							"refs/heads/main":   "82264ea131fcc2a386b83e38b962b370315c7c93",
  1624  							"refs/heads/master": "82264ea131fcc2a386b83e38b962b370315c7c93",
  1625  						},
  1626  						WebLinks: []*gerritpb.WebLinkInfo{
  1627  							{
  1628  								Name: "gitiles",
  1629  								Url:  "https://chromium.googlesource.com/android_apks/",
  1630  							},
  1631  						},
  1632  					},
  1633  				},
  1634  			})
  1635  		})
  1636  	})
  1637  }
  1638  
  1639  func TestGetBranchInfo(t *testing.T) {
  1640  	t.Parallel()
  1641  	ctx := context.Background()
  1642  
  1643  	Convey("Get Branch Info", t, func() {
  1644  		var actualURL *url.URL
  1645  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1646  			actualURL = r.URL
  1647  			w.WriteHeader(200)
  1648  			w.Header().Set("Content-Type", "application/json")
  1649  			fmt.Fprint(w, `)]}'
  1650  			{
  1651  				"web_links": [
  1652  				  {
  1653  					"name": "gitiles",
  1654  					"url": "https://chromium.googlesource.com/infra/experimental/+/refs/heads/main",
  1655  					"target": "_blank"
  1656  				  }
  1657  				],
  1658  				"ref": "refs/heads/main",
  1659  				"revision": "10e5c33f63a843440cbe6c9c6cbc1bf513c598eb",
  1660  				"can_delete": true
  1661  			  }`)
  1662  		})
  1663  		defer srv.Close()
  1664  
  1665  		bi, err := c.GetRefInfo(ctx, &gerritpb.RefInfoRequest{
  1666  			Project: "infra/experimental",
  1667  			Ref:     "refs/heads/main",
  1668  		})
  1669  		So(err, ShouldBeNil)
  1670  
  1671  		So(actualURL.Path, ShouldEqual, "/projects/infra/experimental/branches/refs/heads/main")
  1672  		So(bi, ShouldResemble, &gerritpb.RefInfo{
  1673  			Ref:      "refs/heads/main",
  1674  			Revision: "10e5c33f63a843440cbe6c9c6cbc1bf513c598eb",
  1675  		})
  1676  	})
  1677  }
  1678  
  1679  func TestGetPureRevert(t *testing.T) {
  1680  	t.Parallel()
  1681  	ctx := context.Background()
  1682  
  1683  	Convey("Get Pure Revert", t, func() {
  1684  		var actualURL *url.URL
  1685  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1686  			actualURL = r.URL
  1687  			w.WriteHeader(200)
  1688  			w.Header().Set("Content-Type", "application/json")
  1689  			fmt.Fprint(w, `)]}'
  1690  			{
  1691  				"is_pure_revert" : false
  1692  			}`)
  1693  		})
  1694  		defer srv.Close()
  1695  
  1696  		req := &gerritpb.GetPureRevertRequest{
  1697  			Number:  42,
  1698  			Project: "someproject",
  1699  		}
  1700  		res, err := c.GetPureRevert(ctx, req)
  1701  		So(err, ShouldBeNil)
  1702  		So(actualURL.Path, ShouldEqual, "/changes/someproject~42/pure_revert")
  1703  		So(res, ShouldResemble, &gerritpb.PureRevertInfo{
  1704  			IsPureRevert: false,
  1705  		})
  1706  	})
  1707  }
  1708  
  1709  func TestGerritError(t *testing.T) {
  1710  	t.Parallel()
  1711  	ctx := context.Background()
  1712  
  1713  	Convey("Gerrit returns", t, func() {
  1714  		// All APIs share the same error handling code path, so use SubmitChange as
  1715  		// an example.
  1716  		req := &gerritpb.SubmitChangeRequest{Number: 1}
  1717  		Convey("HTTP 400", func() {
  1718  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1719  				w.WriteHeader(400)
  1720  				w.Header().Set("Content-Type", "text/plain")
  1721  				w.Write([]byte("invalid request: xyz is required"))
  1722  			})
  1723  			defer srv.Close()
  1724  			_, err := c.SubmitChange(ctx, req)
  1725  			So(grpcutil.Code(err), ShouldEqual, codes.InvalidArgument)
  1726  		})
  1727  		Convey("HTTP 403", func() {
  1728  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1729  				w.WriteHeader(403)
  1730  			})
  1731  			defer srv.Close()
  1732  			_, err := c.SubmitChange(ctx, req)
  1733  			So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
  1734  		})
  1735  		Convey("HTTP 404 ", func() {
  1736  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1737  				w.WriteHeader(404)
  1738  			})
  1739  			defer srv.Close()
  1740  			_, err := c.SubmitChange(ctx, req)
  1741  			So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
  1742  		})
  1743  		Convey("HTTP 409 ", func() {
  1744  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1745  				w.WriteHeader(409)
  1746  				w.Header().Set("Content-Type", "text/plain")
  1747  				w.Write([]byte("block by Verified"))
  1748  			})
  1749  			defer srv.Close()
  1750  			_, err := c.SubmitChange(ctx, req)
  1751  			So(grpcutil.Code(err), ShouldEqual, codes.FailedPrecondition)
  1752  			So(err, ShouldErrLike, "block by Verified")
  1753  		})
  1754  		Convey("HTTP 412 ", func() {
  1755  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1756  				w.WriteHeader(412)
  1757  				w.Header().Set("Content-Type", "text/plain")
  1758  				_, _ = w.Write([]byte("precondition failed"))
  1759  			})
  1760  			defer srv.Close()
  1761  			_, err := c.SubmitChange(ctx, req)
  1762  			So(grpcutil.Code(err), ShouldEqual, codes.FailedPrecondition)
  1763  			So(err, ShouldErrLike, "precondition failed")
  1764  		})
  1765  		Convey("HTTP 429 ", func() {
  1766  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1767  				w.WriteHeader(429)
  1768  			})
  1769  			defer srv.Close()
  1770  			_, err := c.SubmitChange(ctx, req)
  1771  			So(grpcutil.Code(err), ShouldEqual, codes.ResourceExhausted)
  1772  		})
  1773  		Convey("HTTP 503 ", func() {
  1774  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1775  				w.WriteHeader(503)
  1776  			})
  1777  			defer srv.Close()
  1778  			_, err := c.SubmitChange(ctx, req)
  1779  			So(grpcutil.Code(err), ShouldEqual, codes.Unavailable)
  1780  		})
  1781  	})
  1782  }
  1783  
  1784  func TestGetMetaDiff(t *testing.T) {
  1785  	t.Parallel()
  1786  	ctx := context.Background()
  1787  
  1788  	sampleResp := `)]}'{
  1789  	  "old_change_info": {
  1790  		"_number": 1,
  1791  		"project": "example/repo",
  1792  		"status": "NEW",
  1793  		"branch": "main",
  1794  		"attention_set": {
  1795  		  "22222": {
  1796  			"account": {
  1797  			  "_account_id": 22222
  1798  			},
  1799  			"last_update": "2014-01-02 18:37:10.000000000",
  1800  			"reason": "ps#1: CQ dry run succeeded."
  1801  		  }
  1802  		},
  1803  		"removed_from_attention_set": {
  1804  		  "11111": {
  1805  			"account": {
  1806  			  "_account_id": 11111
  1807  			},
  1808  			"last_update": "2022-05-31 19:02:58.000000000",
  1809  			"reason": "<GERRIT_ACCOUNT_11111> replied on the change",
  1810  			"reason_account": {
  1811  			  "_account_id": 11111
  1812  			}
  1813  		  }
  1814  		},
  1815  		"created": "2014-01-01 18:26:55.000000000",
  1816  		"updated": "2014-01-01 20:23:59.000000000",
  1817  		"meta_rev_id": "cafeefac",
  1818  		"owner": {
  1819  		  "_account_id": 1234567
  1820  		},
  1821  		"requirements": [
  1822  		  {
  1823  			"status": "OK",
  1824  			"fallback_text": "Code-Owners",
  1825  			"type": "code-owners"
  1826  		  }
  1827  		],
  1828  		"submit_records": [
  1829  		  {
  1830  			"rule_name": "Code-Owners",
  1831  			"status": "OK",
  1832  			"requirements": [
  1833  			  {
  1834  				"status": "OK",
  1835  				"fallback_text": "Code-Owners",
  1836  				"type": "code-owners"
  1837  			  }
  1838  			]
  1839  		  }
  1840  		]
  1841  	  },
  1842  	  "new_change_info": {
  1843  		"_number": 1,
  1844  		"project": "example/repo",
  1845  		"status": "NEW",
  1846  		"branch": "main",
  1847  		"attention_set": {
  1848  		  "33333": {
  1849  			"account": {
  1850  			  "_account_id": 33333
  1851  			},
  1852  			"last_update": "2014-01-02 20:25:28.000000000",
  1853  			"reason": "<GERRIT_ACCOUNT_33333> replied on the change",
  1854  			"reason_account": {
  1855  			  "_account_id": 33333
  1856  			}
  1857  		  }
  1858  		},
  1859  		"removed_from_attention_set": {
  1860  		  "22222": {
  1861  			"account": {
  1862  			  "_account_id": 22222
  1863  			},
  1864  			"last_update": "2014-01-02 20:25:28.000000000",
  1865  			"reason": "<GERRIT_ACCOUNT_22222> replied on the change",
  1866  			"reason_account": {
  1867  			  "_account_id": 22222
  1868  			}
  1869  		  }
  1870  		},
  1871  		"created": "2014-01-01 18:26:55.000000000",
  1872  		"updated": "2014-01-01 20:25:28.000000000",
  1873  		"meta_rev_id": "cafecafe",
  1874  		"owner": {
  1875  		  "_account_id": 1234567
  1876  		},
  1877  		"requirements": [
  1878  		  {
  1879  			"status": "OK",
  1880  			"fallback_text": "Code-Owners",
  1881  			"type": "code-owners"
  1882  		  }
  1883  		],
  1884  		"submit_records": [
  1885  		  {
  1886  			"rule_name": "Code-Owners",
  1887  			"status": "OK",
  1888  			"requirements": [
  1889  			  {
  1890  				"status": "OK",
  1891  				"fallback_text": "Code-Owners",
  1892  				"type": "code-owners"
  1893  			  }
  1894  			]
  1895  		  }
  1896  		]
  1897  	  },
  1898  	  "added": {
  1899  		"attention_set": {
  1900  		  "33333": {
  1901  			"account": {
  1902  			  "_account_id": 33333
  1903  			},
  1904  			"last_update": "2022-05-31 20:25:28.000000000",
  1905  			"reason": "<GERRIT_ACCOUNT_33333> replied on the change",
  1906  			"reason_account": {
  1907  			  "_account_id": 33333
  1908  			}
  1909  		  }
  1910  		},
  1911  		"removed_from_attention_set": {
  1912  		  "22222": {
  1913  			"account": {
  1914  			  "_account_id": 22222
  1915  			},
  1916  			"last_update": "2022-05-31 20:25:28.000000000",
  1917  			"reason": "<GERRIT_ACCOUNT_22222> replied on the change",
  1918  			"reason_account": {
  1919  			  "_account_id": 22222
  1920  			}
  1921  		  }
  1922  		},
  1923  		"updated": "2014-01-02 20:25:28.000000000",
  1924  		"meta_rev_id": "cafecafe"
  1925  	  },
  1926  	  "removed": {
  1927  		"attention_set": {
  1928  		  "1147264": {
  1929  			"account": {
  1930  			  "_account_id": 1147264
  1931  			},
  1932  			"last_update": "2022-05-31 18:37:10.000000000",
  1933  			"reason": "ps#1: CQ dry run succeeded."
  1934  		  }
  1935  		},
  1936  		"removed_from_attention_set": {
  1937  		  "22222": {
  1938  			"account": {
  1939  			  "_account_id": 22222
  1940  			},
  1941  			"last_update": "2014-01-02 19:02:58.000000000",
  1942  			"reason": "<GERRIT_ACCOUNT_22222> replied on the change",
  1943  			"reason_account": {
  1944  			  "_account_id": 22222
  1945  			}
  1946  		  }
  1947  		},
  1948  		"updated": "2014-01-02 20:23:59.000000000",
  1949  		"meta_rev_id": "cafeefac"
  1950  	  }
  1951  	}`
  1952  
  1953  	Convey("GetMetaDiff", t, func() {
  1954  		Convey("Validates args", func() {
  1955  			srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {})
  1956  			defer srv.Close()
  1957  			_, err := c.GetMetaDiff(ctx, &gerritpb.GetMetaDiffRequest{})
  1958  			So(err, ShouldErrLike, "number must be positive")
  1959  		})
  1960  
  1961  		var actualRequest *http.Request
  1962  		srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {
  1963  			actualRequest = r
  1964  			w.WriteHeader(200)
  1965  			w.Header().Set("Content-Type", "application/json")
  1966  			fmt.Fprint(w, sampleResp)
  1967  		})
  1968  		defer srv.Close()
  1969  		req := &gerritpb.GetMetaDiffRequest{Number: 1}
  1970  
  1971  		Convey("Works", func() {
  1972  			resp, err := c.GetMetaDiff(ctx, req)
  1973  			So(err, ShouldBeNil)
  1974  			So(resp.OldChangeInfo.Number, ShouldEqual, 1)
  1975  			So(resp.NewChangeInfo.Number, ShouldEqual, 1)
  1976  			So(resp.Added.MetaRevId, ShouldEqual, resp.NewChangeInfo.MetaRevId)
  1977  			So(resp.Removed.MetaRevId, ShouldEqual, resp.OldChangeInfo.MetaRevId)
  1978  		})
  1979  
  1980  		Convey("Passes old and meta", func() {
  1981  			req.Old = "mehmeh"
  1982  			req.Meta = "booboo"
  1983  			_, err := c.GetMetaDiff(ctx, req)
  1984  			So(err, ShouldBeNil)
  1985  			So(actualRequest.URL.Query()["old"], ShouldResemble, []string{"mehmeh"})
  1986  			So(actualRequest.URL.Query()["meta"], ShouldResemble, []string{"booboo"})
  1987  		})
  1988  	})
  1989  
  1990  }
  1991  
  1992  func newMockPbClient(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, gerritpb.GerritClient) {
  1993  	// TODO(tandrii): rename this func once newMockClient name is no longer used in the same package.
  1994  	srv := httptest.NewServer(http.HandlerFunc(handler))
  1995  	return srv, &client{testBaseURL: srv.URL}
  1996  }
  1997  
  1998  // parseTime parses a RFC3339Nano formatted timestamp string.
  1999  // Panics when error occurs during parse.
  2000  func parseTime(t string) time.Time {
  2001  	ret, err := time.Parse(time.RFC3339Nano, t)
  2002  	if err != nil {
  2003  		panic(err)
  2004  	}
  2005  	return ret
  2006  }