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

     1  // Copyright 2017 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  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"fmt"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"net/url"
    25  	"testing"
    26  
    27  	"go.chromium.org/luci/common/retry"
    28  
    29  	. "github.com/smartystreets/goconvey/convey"
    30  )
    31  
    32  func TestGerritURL(t *testing.T) {
    33  	t.Parallel()
    34  	Convey("Malformed", t, func() {
    35  		f := func(arg string) {
    36  			So(ValidateGerritURL(arg), ShouldNotBeNil)
    37  			_, err := NormalizeGerritURL(arg)
    38  			So(err, ShouldNotBeNil)
    39  		}
    40  
    41  		f("what/\\is\this")
    42  		f("https://example.com/")
    43  		f("http://bad-protocol-review.googlesource.com/")
    44  		f("no-protocol-review.googlesource.com/")
    45  		f("https://a-review.googlesource.com/path-and#fragment")
    46  		f("https://a-review.googlesource.com/any-path-actually")
    47  	})
    48  
    49  	Convey("OK", t, func() {
    50  		f := func(arg, exp string) {
    51  			So(ValidateGerritURL(arg), ShouldBeNil)
    52  			act, err := NormalizeGerritURL(arg)
    53  			So(err, ShouldBeNil)
    54  			So(act, ShouldEqual, exp)
    55  		}
    56  		f("https://a-review.googlesource.com", "https://a-review.googlesource.com/")
    57  		f("https://a-review.googlesource.com/", "https://a-review.googlesource.com/")
    58  		f("https://chromium-review.googlesource.com/", "https://chromium-review.googlesource.com/")
    59  		f("https://chromium-review.googlesource.com", "https://chromium-review.googlesource.com/")
    60  	})
    61  }
    62  
    63  func TestNewClient(t *testing.T) {
    64  	t.Parallel()
    65  	Convey("Malformed", t, func() {
    66  		f := func(arg string) {
    67  			_, err := NewClient(http.DefaultClient, arg)
    68  			So(err, ShouldNotBeNil)
    69  		}
    70  		f("badurl")
    71  		f("http://a.googlesource.com")
    72  		f("https://a/")
    73  	})
    74  	Convey("OK", t, func() {
    75  		f := func(arg string) {
    76  			_, err := NewClient(http.DefaultClient, arg)
    77  			So(err, ShouldBeNil)
    78  		}
    79  		f("https://a-review.googlesource.com/")
    80  		f("https://a-review.googlesource.com")
    81  	})
    82  }
    83  
    84  func TestQuery(t *testing.T) {
    85  	t.Parallel()
    86  	ctx := context.Background()
    87  
    88  	Convey("ChangeQuery", t, func() {
    89  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
    90  			w.WriteHeader(200)
    91  			w.Header().Set("Content-Type", "application/json")
    92  			fmt.Fprintf(w, ")]}'\n[%s]\n", fakeCL1Str)
    93  		})
    94  		defer srv.Close()
    95  
    96  		Convey("Basic", func() {
    97  			cls, more, err := c.ChangeQuery(ctx,
    98  				ChangeQueryParams{
    99  					Query: "some_query",
   100  				})
   101  			So(err, ShouldBeNil)
   102  			So(len(cls), ShouldEqual, 1)
   103  			So(cls[0].Owner.AccountID, ShouldEqual, 1118104)
   104  			So(more, ShouldBeFalse)
   105  		})
   106  	})
   107  
   108  	Convey("ChangeQuery with more changes", t, func() {
   109  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   110  			w.WriteHeader(200)
   111  			w.Header().Set("Content-Type", "application/json")
   112  			fmt.Fprintf(w, ")]}'\n[%s]\n", fakeCL2Str)
   113  		})
   114  		defer srv.Close()
   115  
   116  		Convey("Basic", func() {
   117  			cls, more, err := c.ChangeQuery(ctx,
   118  				ChangeQueryParams{
   119  					Query: "4efbec9a685b238fced35b81b7f3444dc60150b1",
   120  				})
   121  			So(err, ShouldBeNil)
   122  			So(len(cls), ShouldEqual, 1)
   123  			So(cls[0].Owner.AccountID, ShouldEqual, 1178184)
   124  			So(more, ShouldBeFalse)
   125  		})
   126  	})
   127  
   128  	Convey("ChangeQuery returns no changes", t, func() {
   129  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   130  			w.WriteHeader(200)
   131  			w.Header().Set("Content-Type", "application/json")
   132  			fmt.Fprint(w, ")]}'\n[]\n", fakeCL2Str)
   133  		})
   134  		defer srv.Close()
   135  
   136  		Convey("Basic", func() {
   137  			cls, more, err := c.ChangeQuery(ctx,
   138  				ChangeQueryParams{
   139  					Query: "4efbec9a685b238fced35b81b7f3444dc60150b1",
   140  				})
   141  			So(err, ShouldBeNil)
   142  			So(cls, ShouldResemble, []*Change{})
   143  			So(more, ShouldBeFalse)
   144  		})
   145  	})
   146  }
   147  
   148  func TestChangeDetails(t *testing.T) {
   149  	t.Parallel()
   150  	ctx := context.Background()
   151  
   152  	Convey("Details", t, func() {
   153  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   154  			w.WriteHeader(200)
   155  			w.Header().Set("Content-Type", "application/json")
   156  			fmt.Fprintf(w, ")]}'\n%s\n", fakeCL3Str)
   157  		})
   158  		defer srv.Close()
   159  
   160  		Convey("WithOptions", func() {
   161  			options := ChangeDetailsParams{Options: []string{"CURRENT_REVISION"}}
   162  			cl, err := c.ChangeDetails(ctx, "629279", options)
   163  			So(err, ShouldBeNil)
   164  			So(cl.RevertOf, ShouldEqual, 629277)
   165  			So(cl.CurrentRevision, ShouldEqual, "1ee75012c0de")
   166  		})
   167  
   168  	})
   169  
   170  	Convey("Retry", t, func() {
   171  		var attempts int
   172  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   173  			// First attempt fails, second succeeds.
   174  			if attempts == 0 {
   175  				w.WriteHeader(500)
   176  				w.Header().Set("Content-Type", "text/plain")
   177  				fmt.Fprintf(w, "Internal server error")
   178  			} else {
   179  				w.WriteHeader(200)
   180  				w.Header().Set("Content-Type", "application/json")
   181  				fmt.Fprintf(w, ")]}'\n%s\n", fakeCL3Str)
   182  			}
   183  			attempts++
   184  		})
   185  		defer srv.Close()
   186  
   187  		cl, err := c.ChangeDetails(ctx, "629279", ChangeDetailsParams{})
   188  		So(err, ShouldBeNil)
   189  		So(cl.RevertOf, ShouldEqual, 629277)
   190  		So(cl.CurrentRevision, ShouldEqual, "1ee75012c0de")
   191  		So(attempts, ShouldEqual, 2)
   192  	})
   193  }
   194  
   195  func TestListChangeComments(t *testing.T) {
   196  	t.Parallel()
   197  	ctx := context.Background()
   198  
   199  	Convey("ListComments", t, func() {
   200  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   201  			w.WriteHeader(200)
   202  			w.Header().Set("Content-Type", "application/json")
   203  			fmt.Fprintf(w, ")]}'\n%s\n", fakeComments1Str)
   204  		})
   205  		defer srv.Close()
   206  
   207  		Convey("WithOptions", func() {
   208  			comments, err := c.ListChangeComments(ctx, "629279", "")
   209  			So(err, ShouldBeNil)
   210  			So(comments["foo"][0].Line, ShouldEqual, 3)
   211  			So(comments["foo"][0].Range.StartLine, ShouldEqual, 3)
   212  			So(comments["bar"][0].Line, ShouldEqual, 21)
   213  		})
   214  
   215  	})
   216  
   217  }
   218  
   219  func TestListRobotComments(t *testing.T) {
   220  	t.Parallel()
   221  	ctx := context.Background()
   222  
   223  	Convey("ListRobotComments", t, func() {
   224  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   225  			w.WriteHeader(200)
   226  			w.Header().Set("Content-Type", "application/json")
   227  			fmt.Fprintf(w, ")]}'\n%s\n", fakeRobotComments1Str)
   228  		})
   229  		defer srv.Close()
   230  
   231  		Convey("WithOptions", func() {
   232  			comments, err := c.ListRobotComments(ctx, "629279", "deadbeef")
   233  			So(err, ShouldBeNil)
   234  			So(comments["foo"][0].Line, ShouldEqual, 3)
   235  			So(comments["foo"][0].Range.StartLine, ShouldEqual, 3)
   236  			So(comments["foo"][0].RobotID, ShouldEqual, "somerobot")
   237  			So(comments["foo"][0].RobotRunID, ShouldEqual, "run1")
   238  			So(comments["bar"][0].Line, ShouldEqual, 21)
   239  		})
   240  	})
   241  }
   242  
   243  func TestAccountQuery(t *testing.T) {
   244  	t.Parallel()
   245  	ctx := context.Background()
   246  
   247  	Convey("Account-Query", t, func(c C) {
   248  		srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   249  			w.WriteHeader(200)
   250  			w.Header().Set("Content-Type", "application/json")
   251  			fmt.Fprintf(w, ")]}'\n%s\n", fakeAccounts1Str)
   252  		})
   253  		defer srv.Close()
   254  
   255  		Convey("WithOptions", func() {
   256  			accounts, more, err := client.AccountQuery(ctx, AccountQueryParams{Query: "email:nobody@example.com"})
   257  			So(err, ShouldBeNil)
   258  			So(more, ShouldEqual, false)
   259  			So(accounts[0].Name, ShouldEqual, "John Doe")
   260  			So(accounts[1].Name, ShouldEqual, "Jane Doe")
   261  		})
   262  	})
   263  }
   264  
   265  func TestChangesSubmittedTogether(t *testing.T) {
   266  	t.Parallel()
   267  	ctx := context.Background()
   268  
   269  	Convey("SubmittedTogether", t, func() {
   270  		var nonVisibleResp string
   271  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   272  			w.WriteHeader(200)
   273  			w.Header().Set("Content-Type", "application/json")
   274  			fmt.Fprintf(w, ")]}'\n{ \"changes\":[%s,%s]%s}\n", fakeCL1Str, fakeCL6Str, nonVisibleResp)
   275  		})
   276  		defer srv.Close()
   277  
   278  		Convey("WithCurrentRevisionOptions", func() {
   279  			nonVisibleResp = ""
   280  			options := ChangeDetailsParams{Options: []string{"CURRENT_REVISION"}}
   281  			cls, err := c.ChangesSubmittedTogether(ctx, "627036", options)
   282  			So(err, ShouldBeNil)
   283  			So(cls.Changes[0].CurrentRevision, ShouldEqual, "eb2388b592a9")
   284  			So(cls.Changes[1].CurrentRevision, ShouldEqual, "d6375c2ea5b0")
   285  		})
   286  		Convey("WithNonVisibleChangesOptions", func() {
   287  			nonVisibleResp = ",\"non_visible_changes\":1"
   288  			options := ChangeDetailsParams{Options: []string{"CURRENT_REVISION", "NON_VISIBLE_CHANGES"}}
   289  			cls, err := c.ChangesSubmittedTogether(ctx, "627036", options)
   290  			So(err, ShouldBeNil)
   291  			So(cls.Changes[0].CurrentRevision, ShouldEqual, "eb2388b592a9")
   292  			So(cls.Changes[1].CurrentRevision, ShouldEqual, "d6375c2ea5b0")
   293  			So(cls.NonVisibleChanges, ShouldEqual, 1)
   294  		})
   295  
   296  	})
   297  }
   298  
   299  func TestMergeable(t *testing.T) {
   300  	t.Parallel()
   301  	ctx := context.Background()
   302  
   303  	Convey("GetMergeable", t, func() {
   304  		var resp string
   305  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   306  			w.WriteHeader(200)
   307  			w.Header().Set("Content-Type", "application/json")
   308  			fmt.Fprintf(w, ")]}'\n%s\n", resp)
   309  		})
   310  		defer srv.Close()
   311  
   312  		Convey("yes", func() {
   313  			resp = `{
   314  				"submit_type": "REBASE_ALWAYS",
   315  				"strategy": "recursive",
   316  				"mergeable": true,
   317  				"commit_merged": false,
   318  				"content_merged": false
   319  			}`
   320  			cls, err := c.GetMergeable(ctx, "627036", "eb2388b592a9")
   321  			So(err, ShouldBeNil)
   322  			So(cls.Mergeable, ShouldEqual, true)
   323  		})
   324  
   325  		Convey("no", func() {
   326  			resp = `{
   327  				"submit_type": "REBASE_ALWAYS",
   328  				"strategy": "recursive",
   329  				"mergeable": false,
   330  				"commit_merged": false,
   331  				"content_merged": false
   332  			}`
   333  			cls, err := c.GetMergeable(ctx, "646267", "d6375c2ea5b0")
   334  			So(err, ShouldBeNil)
   335  			So(cls.Mergeable, ShouldEqual, false)
   336  		})
   337  
   338  	})
   339  }
   340  
   341  func TestChangeLabels(t *testing.T) {
   342  	t.Parallel()
   343  	ctx := context.Background()
   344  
   345  	Convey("Labels", t, func() {
   346  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   347  			w.WriteHeader(200)
   348  			w.Header().Set("Content-Type", "application/json")
   349  			fmt.Fprintf(w, ")]}'\n%s\n", fakeCL5Str)
   350  		})
   351  		defer srv.Close()
   352  
   353  		Convey("All", func() {
   354  			options := ChangeDetailsParams{Options: []string{"DETAILED_LABELS"}}
   355  			cl, err := c.ChangeDetails(ctx, "629279", options)
   356  			So(err, ShouldBeNil)
   357  			So(len(cl.Labels["Code-Review"].All), ShouldEqual, 2)
   358  			So(cl.Labels["Code-Review"].All[0].Value, ShouldEqual, -1)
   359  			So(cl.Labels["Code-Review"].All[0].Username, ShouldEqual, "jdoe")
   360  			So(cl.Labels["Code-Review"].All[1].Value, ShouldEqual, 1)
   361  			So(cl.Labels["Code-Review"].All[1].Username, ShouldEqual, "jroe")
   362  			So(len(cl.Labels["Code-Review"].Values), ShouldEqual, 5)
   363  			So(len(cl.Labels["Verified"].Values), ShouldEqual, 3)
   364  		})
   365  
   366  	})
   367  
   368  }
   369  
   370  func TestCreateChange(t *testing.T) {
   371  	t.Parallel()
   372  	ctx := context.Background()
   373  
   374  	Convey("CreateChange", t, func(c C) {
   375  		srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   376  			defer r.Body.Close()
   377  
   378  			var ci ChangeInput
   379  			err := json.NewDecoder(r.Body).Decode(&ci)
   380  			c.So(err, ShouldBeNil)
   381  
   382  			w.WriteHeader(200)
   383  			w.Header().Set("Content-Type", "application/json")
   384  			change := Change{
   385  				ID:       fmt.Sprintf("%s~%s~I8473b95934b5732ac55d26311a706c9c2bde9941", ci.Project, ci.Branch),
   386  				ChangeID: "I8473b95934b5732ac55d26311a706c9c2bde9941",
   387  				Project:  ci.Project,
   388  				Branch:   ci.Branch,
   389  				Subject:  ci.Subject,
   390  				Topic:    ci.Topic,
   391  				Status:   "NEW",
   392  				// the rest omitted for brevity...
   393  			}
   394  			var buffer bytes.Buffer
   395  			err = json.NewEncoder(&buffer).Encode(&change)
   396  			c.So(err, ShouldBeNil)
   397  			fmt.Fprintf(w, ")]}'\n%s\n", buffer.String())
   398  		})
   399  		defer srv.Close()
   400  
   401  		Convey("Basic", func() {
   402  			ci := ChangeInput{
   403  				Project: "infra/luci-go",
   404  				Branch:  "master",
   405  				Subject: "Let's make a thing. Yeah, a thing.",
   406  				Topic:   "something-something",
   407  			}
   408  			change, err := client.CreateChange(ctx, &ci)
   409  			So(err, ShouldBeNil)
   410  			So(change.Project, ShouldResemble, ci.Project)
   411  			So(change.Branch, ShouldResemble, ci.Branch)
   412  			So(change.Subject, ShouldResemble, ci.Subject)
   413  			So(change.Topic, ShouldResemble, ci.Topic)
   414  			So(change.Status, ShouldResemble, "NEW")
   415  		})
   416  
   417  	})
   418  
   419  	Convey("CreateChange but project non-existent", t, func() {
   420  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   421  			w.WriteHeader(404)
   422  			w.Header().Set("Content-Type", "text/plain")
   423  			fmt.Fprintf(w, "No such project: blah")
   424  		})
   425  		defer srv.Close()
   426  
   427  		Convey("Basic", func() {
   428  			ci := ChangeInput{
   429  				Project: "blah",
   430  				Branch:  "master",
   431  				Subject: "beep bop boop I'm a robot",
   432  				Topic:   "haha",
   433  			}
   434  			_, err := c.CreateChange(ctx, &ci)
   435  			So(err, ShouldNotBeNil)
   436  		})
   437  
   438  	})
   439  }
   440  
   441  func TestAbandonChange(t *testing.T) {
   442  	t.Parallel()
   443  	ctx := context.Background()
   444  
   445  	Convey("AbandonChange", t, func(c C) {
   446  		srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   447  			defer r.Body.Close()
   448  
   449  			var ai AbandonInput
   450  			err := json.NewDecoder(r.Body).Decode(&ai)
   451  			c.So(err, ShouldBeNil)
   452  
   453  			w.WriteHeader(200)
   454  			w.Header().Set("Content-Type", "application/json")
   455  			fmt.Fprintf(w, ")]}'\n%s\n", fakeCL4Str)
   456  		})
   457  		defer srv.Close()
   458  
   459  		Convey("Basic", func() {
   460  			change, err := client.AbandonChange(ctx, "629279", nil)
   461  			So(err, ShouldBeNil)
   462  			So(change.Status, ShouldResemble, "ABANDONED")
   463  		})
   464  
   465  		Convey("Basic with message", func() {
   466  			ai := AbandonInput{
   467  				Message: "duplicate",
   468  			}
   469  			change, err := client.AbandonChange(ctx, "629279", &ai)
   470  			So(err, ShouldBeNil)
   471  			So(change.Status, ShouldResemble, "ABANDONED")
   472  		})
   473  	})
   474  
   475  	Convey("AbandonChange but change non-existent", t, func() {
   476  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   477  			w.WriteHeader(404)
   478  			w.Header().Set("Content-Type", "text/plain")
   479  			fmt.Fprintf(w, "No such change: 629279")
   480  		})
   481  		defer srv.Close()
   482  
   483  		Convey("Basic", func() {
   484  			_, err := c.AbandonChange(ctx, "629279", nil)
   485  			So(err, ShouldNotBeNil)
   486  		})
   487  
   488  	})
   489  }
   490  
   491  func TestRebase(t *testing.T) {
   492  	t.Parallel()
   493  	ctx := context.Background()
   494  
   495  	Convey("RebaseChange", t, func(c C) {
   496  		srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   497  			defer r.Body.Close()
   498  
   499  			var ri RestoreInput
   500  			err := json.NewDecoder(r.Body).Decode(&ri)
   501  			c.So(err, ShouldBeNil)
   502  
   503  			w.WriteHeader(200)
   504  			w.Header().Set("Content-Type", "application/json")
   505  			fmt.Fprintf(w, ")]}'\n%s\n", fakeCL1Str)
   506  		})
   507  		defer srv.Close()
   508  
   509  		Convey("Basic", func() {
   510  			change, err := client.RebaseChange(ctx, "627036", nil)
   511  			So(err, ShouldBeNil)
   512  			So(change.Status, ShouldResemble, "NEW")
   513  		})
   514  
   515  		Convey("Basic with overridden base revision", func() {
   516  			ri := RebaseInput{
   517  				Base:               "abc123",
   518  				OnBehalfOfUploader: true,
   519  				AllowConflicts:     false,
   520  			}
   521  			change, err := client.RebaseChange(ctx, "627036", &ri)
   522  			So(err, ShouldBeNil)
   523  			So(change.Status, ShouldResemble, "NEW")
   524  		})
   525  	})
   526  
   527  	Convey("RebaseChange with nontrivial merge conflict", t, func(c C) {
   528  		srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   529  			defer r.Body.Close()
   530  
   531  			var ri RebaseInput
   532  			err := json.NewDecoder(r.Body).Decode(&ri)
   533  			c.So(err, ShouldBeNil)
   534  
   535  			w.WriteHeader(409)
   536  			w.Header().Set("Content-Type", "text/plain")
   537  			fmt.Fprintf(w, "change has conflicts")
   538  		})
   539  		defer srv.Close()
   540  
   541  		Convey("Basic", func() {
   542  			_, err := client.RebaseChange(ctx, "627036", nil)
   543  			So(err, ShouldNotBeNil)
   544  		})
   545  	})
   546  }
   547  
   548  func TestRestoreChange(t *testing.T) {
   549  	t.Parallel()
   550  	ctx := context.Background()
   551  
   552  	Convey("RestoreChange", t, func(c C) {
   553  		srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   554  			defer r.Body.Close()
   555  
   556  			var ri RestoreInput
   557  			err := json.NewDecoder(r.Body).Decode(&ri)
   558  			c.So(err, ShouldBeNil)
   559  
   560  			w.WriteHeader(200)
   561  			w.Header().Set("Content-Type", "application/json")
   562  			fmt.Fprintf(w, ")]}'\n%s\n", fakeCL1Str)
   563  		})
   564  		defer srv.Close()
   565  
   566  		Convey("Basic", func() {
   567  			change, err := client.RestoreChange(ctx, "627036", nil)
   568  			So(err, ShouldBeNil)
   569  			So(change.Status, ShouldResemble, "NEW")
   570  		})
   571  
   572  		Convey("Basic with message", func() {
   573  			ri := RestoreInput{
   574  				Message: "restored",
   575  			}
   576  			change, err := client.RestoreChange(ctx, "627036", &ri)
   577  			So(err, ShouldBeNil)
   578  			So(change.Status, ShouldResemble, "NEW")
   579  		})
   580  	})
   581  
   582  	Convey("RestoreChange but change not abandoned", t, func(c C) {
   583  		srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   584  			defer r.Body.Close()
   585  
   586  			var ri RestoreInput
   587  			err := json.NewDecoder(r.Body).Decode(&ri)
   588  			c.So(err, ShouldBeNil)
   589  
   590  			w.WriteHeader(409)
   591  			w.Header().Set("Content-Type", "text/plain")
   592  			fmt.Fprintf(w, "change is new")
   593  		})
   594  		defer srv.Close()
   595  
   596  		Convey("Basic", func() {
   597  			_, err := client.RestoreChange(ctx, "627036", nil)
   598  			So(err, ShouldNotBeNil)
   599  		})
   600  	})
   601  
   602  	Convey("RestoreChange but change non-existent", t, func(c C) {
   603  		srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   604  			defer r.Body.Close()
   605  
   606  			var ri RestoreInput
   607  			err := json.NewDecoder(r.Body).Decode(&ri)
   608  			c.So(err, ShouldBeNil)
   609  
   610  			w.WriteHeader(404)
   611  			w.Header().Set("Content-Type", "text/plain")
   612  			fmt.Fprintf(w, "No such change: 629279")
   613  		})
   614  		defer srv.Close()
   615  
   616  		Convey("Basic", func() {
   617  			_, err := client.RestoreChange(ctx, "629279", nil)
   618  			So(err, ShouldNotBeNil)
   619  		})
   620  	})
   621  }
   622  
   623  func TestCreateBranch(t *testing.T) {
   624  	t.Parallel()
   625  	ctx := context.Background()
   626  	bi := BranchInput{
   627  		Ref:      "branch",
   628  		Revision: "08a8326653eaa5f7aeea30348b63bf5e9595dc11",
   629  	}
   630  
   631  	Convey("CreateBranch", t, func(c C) {
   632  		srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   633  			defer r.Body.Close()
   634  
   635  			var bi BranchInput
   636  			err := json.NewDecoder(r.Body).Decode(&bi)
   637  			c.So(err, ShouldBeNil)
   638  
   639  			w.WriteHeader(200)
   640  			w.Header().Set("Content-Type", "application/json")
   641  			info := BranchInfo{
   642  				Ref:      "branch",
   643  				Revision: "08a8326653eaa5f7aeea30348b63bf5e9595dc11",
   644  			}
   645  			var buffer bytes.Buffer
   646  			err = json.NewEncoder(&buffer).Encode(&info)
   647  			c.So(err, ShouldBeNil)
   648  			fmt.Fprintf(w, ")]}'\n%s\n", buffer.String())
   649  		})
   650  		defer srv.Close()
   651  		Convey("Basic", func() {
   652  			info, err := client.CreateBranch(ctx, "project", &bi)
   653  			So(err, ShouldBeNil)
   654  			So(info.Ref, ShouldEqual, "branch")
   655  			So(info.Revision, ShouldEqual, "08a8326653eaa5f7aeea30348b63bf5e9595dc11")
   656  		})
   657  	})
   658  
   659  	Convey("Not authorized", t, func(c C) {
   660  		srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   661  			defer r.Body.Close()
   662  
   663  			var bi BranchInput
   664  			err := json.NewDecoder(r.Body).Decode(&bi)
   665  			c.So(err, ShouldBeNil)
   666  
   667  			w.WriteHeader(403)
   668  			w.Header().Set("Content-Type", "text/plain")
   669  			fmt.Fprintf(w, "Not authorized to create ref")
   670  		})
   671  		defer srv.Close()
   672  
   673  		Convey("Basic", func() {
   674  			_, err := client.CreateBranch(ctx, "project", &bi)
   675  			So(err, ShouldNotBeNil)
   676  		})
   677  	})
   678  }
   679  
   680  func TestIsPureRevert(t *testing.T) {
   681  	t.Parallel()
   682  	ctx := context.Background()
   683  
   684  	Convey("IsPureRevert", t, func() {
   685  		Convey("Bad change id", func() {
   686  			srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   687  				w.WriteHeader(404)
   688  				w.Header().Set("Content-Type", "text/plain")
   689  				fmt.Fprintf(w, "Not found: 629277")
   690  			})
   691  			defer srv.Close()
   692  
   693  			_, err := c.IsChangePureRevert(ctx, "629277")
   694  			So(err, ShouldNotBeNil)
   695  		})
   696  		Convey("Not revert", func() {
   697  			srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   698  				w.WriteHeader(400)
   699  				w.Header().Set("Content-Type", "text/plain")
   700  				fmt.Fprintf(w, "No ID was provided and change isn't a revert")
   701  			})
   702  			defer srv.Close()
   703  
   704  			r, err := c.IsChangePureRevert(ctx, "629277")
   705  			So(err, ShouldBeNil)
   706  			So(r, ShouldBeFalse)
   707  		})
   708  		Convey("Not pure revert", func() {
   709  			srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   710  				w.WriteHeader(200)
   711  				w.Header().Set("Content-Type", "application/json")
   712  				fmt.Fprintf(w, ")]}'\n%s\n", "{\"is_pure_revert\":false}")
   713  			})
   714  			defer srv.Close()
   715  
   716  			r, err := c.IsChangePureRevert(ctx, "629277")
   717  			So(err, ShouldBeNil)
   718  			So(r, ShouldBeFalse)
   719  		})
   720  		Convey("Pure revert", func() {
   721  			srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   722  				w.WriteHeader(200)
   723  				w.Header().Set("Content-Type", "application/json")
   724  				fmt.Fprintf(w, ")]}'\n%s\n", "{\"is_pure_revert\":true}")
   725  			})
   726  			defer srv.Close()
   727  
   728  			r, err := c.IsChangePureRevert(ctx, "629277")
   729  			So(err, ShouldBeNil)
   730  			So(r, ShouldBeTrue)
   731  		})
   732  	})
   733  }
   734  
   735  func TestDirectSetReview(t *testing.T) {
   736  	t.Parallel()
   737  	ctx := context.Background()
   738  
   739  	Convey("SetReview", t, func(c C) {
   740  		srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   741  			defer r.Body.Close()
   742  
   743  			var ri ReviewInput
   744  			err := json.NewDecoder(r.Body).Decode(&ri)
   745  			c.So(err, ShouldBeNil)
   746  
   747  			var rr ReviewResult
   748  			rr.Labels = ri.Labels
   749  			rr.Reviewers = make(map[string]AddReviewerResult, len(ri.Reviewers))
   750  			for _, reviewer := range ri.Reviewers {
   751  				result := AddReviewerResult{
   752  					Input: reviewer.Reviewer,
   753  				}
   754  				info := ReviewerInfo{AccountInfo: AccountInfo{AccountID: 12345}}
   755  				switch reviewer.State {
   756  				case "REVIEWER":
   757  					result.Reviewers = []ReviewerInfo{info}
   758  				case "CC":
   759  					result.CCs = []ReviewerInfo{info}
   760  				}
   761  				rr.Reviewers[reviewer.Reviewer] = result
   762  			}
   763  
   764  			w.WriteHeader(200)
   765  			w.Header().Set("Content-Type", "application/json")
   766  
   767  			var buffer bytes.Buffer
   768  			err = json.NewEncoder(&buffer).Encode(&rr)
   769  			c.So(err, ShouldBeNil)
   770  			fmt.Fprintf(w, ")]}'\n%s\n", buffer.String())
   771  		})
   772  		defer srv.Close()
   773  
   774  		Convey("Set review", func() {
   775  			_, err := client.SetReview(ctx, "629279", "current", &ReviewInput{})
   776  			So(err, ShouldBeNil)
   777  		})
   778  
   779  		Convey("Set label", func() {
   780  			ri := ReviewInput{Labels: map[string]int{"Code-Review": 1}}
   781  			result, err := client.SetReview(ctx, "629279", "current", &ri)
   782  			So(err, ShouldBeNil)
   783  			So(result.Labels, ShouldResemble, ri.Labels)
   784  		})
   785  
   786  		Convey("Set reviewers", func() {
   787  			ri := ReviewInput{
   788  				Reviewers: []ReviewerInput{
   789  					{
   790  						Reviewer: "test@example.com",
   791  						State:    "REVIEWER",
   792  					},
   793  					{
   794  						Reviewer: "test2@example.com",
   795  						State:    "CC",
   796  					},
   797  				},
   798  			}
   799  			result, err := client.SetReview(ctx, "629279", "current", &ri)
   800  			So(err, ShouldBeNil)
   801  			So(len(result.Reviewers), ShouldEqual, 2)
   802  			So(len(result.Reviewers["test@example.com"].Reviewers), ShouldEqual, 1)
   803  			So(len(result.Reviewers["test2@example.com"].CCs), ShouldEqual, 1)
   804  		})
   805  	})
   806  
   807  	Convey("SetReview but change non-existent", t, func() {
   808  		srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   809  			w.WriteHeader(404)
   810  			w.Header().Set("Content-Type", "text/plain")
   811  			fmt.Fprintf(w, "No such change: 629279")
   812  		})
   813  		defer srv.Close()
   814  
   815  		Convey("Basic", func() {
   816  			_, err := c.SetReview(ctx, "629279", "current", &ReviewInput{})
   817  			So(err, ShouldNotBeNil)
   818  		})
   819  
   820  	})
   821  }
   822  
   823  func TestSubmit(t *testing.T) {
   824  	t.Parallel()
   825  	ctx := context.Background()
   826  
   827  	Convey("Submit", t, func(c C) {
   828  		srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) {
   829  			defer r.Body.Close()
   830  			var si SubmitInput
   831  			err := json.NewDecoder(r.Body).Decode(&si)
   832  			c.So(err, ShouldBeNil)
   833  			var cr Change
   834  			w.WriteHeader(200)
   835  			w.Header().Set("Content-Type", "application/json")
   836  			var buffer bytes.Buffer
   837  			err = json.NewEncoder(&buffer).Encode(&cr)
   838  			c.So(err, ShouldBeNil)
   839  			fmt.Fprintf(w, ")]}'\n%s\n", buffer.String())
   840  		})
   841  		defer srv.Close()
   842  
   843  		Convey("Submit", func() {
   844  			_, err := client.Submit(ctx, "629279", &SubmitInput{})
   845  			So(err, ShouldBeNil)
   846  		})
   847  	})
   848  }
   849  
   850  ////////////////////////////////////////////////////////////////////////////////
   851  
   852  var (
   853  	fakeCL1Str = `{
   854  	    "id": "infra%2Fluci%2Fluci-go~master~I4c01b6686740f15844dc86aab73ee4ce00b90fe3",
   855  	    "project": "infra/luci/luci-go",
   856  	    "branch": "master",
   857  	    "hashtags": [],
   858  	    "change_id": "I4c01b6686740f15844dc86aab73ee4ce00b90fe3",
   859  	    "subject": "gitiles: Implement forward log.",
   860  	    "status": "NEW",
   861  	    "current_revision": "eb2388b592a9",
   862  	    "created": "2017-08-22 18:46:58.000000000",
   863  	    "updated": "2017-08-23 22:33:34.000000000",
   864  	    "submit_type": "REBASE_ALWAYS",
   865  	    "mergeable": true,
   866  	    "insertions": 154,
   867  	    "deletions": 23,
   868  	    "unresolved_comment_count": 3,
   869  	    "has_review_started": true,
   870  	    "_number": 627036,
   871  	    "owner": {
   872  		"_account_id": 1118104
   873  	    },
   874  	    "reviewers": {
   875  		    "CC": [
   876  			    {"_account_id": 1118110},
   877  			    {"_account_id": 1118111},
   878  			    {"_account_id": 1118112}
   879  		    ],
   880  		    "REVIEWER": [
   881  			    {"_account_id": 1118120},
   882  			    {"_account_id": 1118121},
   883  			    {"_account_id": 1118122}
   884  		    ],
   885  		    "REMOVED": [
   886  			    {"_account_id": 1118130},
   887  			    {"_account_id": 1118131},
   888  			    {"_account_id": 1118132}
   889  		    ]
   890  	    }
   891  	}`
   892  	fakeCL2Str = `{
   893  	    "id": "infra%2Finfra~master~Ia292f77ae6bd94afbd746da0b08500f738904d15",
   894  	    "project": "infra/infra",
   895  	    "branch": "master",
   896  	    "hashtags": [],
   897  	    "change_id": "Ia292f77ae6bd94afbd746da0b08500f738904d15",
   898  	    "subject": "[Findit] Add flake analyzer forced rerun instructions to makefile.",
   899  	    "status": "MERGED",
   900  	    "created": "2017-08-23 17:25:40.000000000",
   901  	    "updated": "2017-08-23 22:51:03.000000000",
   902  	    "submitted": "2017-08-23 22:51:03.000000000",
   903  	    "insertions": 4,
   904  	    "deletions": 1,
   905  	    "unresolved_comment_count": 0,
   906  	    "has_review_started": true,
   907  	    "_number": 629277,
   908  	    "owner": {
   909  		"_account_id": 1178184
   910  	    },
   911  	    "_has_more_changes": true
   912  	}`
   913  	fakeCL3Str = `{
   914  	    "id": "infra%2Finfra~master~Ia292f77ae6bd94a000046da0b08500f738904d15",
   915  	    "project": "infra/infra",
   916  	    "branch": "master",
   917  	    "hashtags": [],
   918  	    "change_id": "Ia292f77ae6bd94a000046da0b08500f738904d15",
   919  	    "subject": "Revert of [Findit] Add flake analyzer forced rerun instructions to makefile.",
   920  	    "status": "MERGED",
   921  	    "current_revision" : "1ee75012c0de",
   922  	    "revert_of": 629277,
   923  	    "created": "2017-08-23 18:25:40.000000000",
   924  	    "updated": "2017-08-23 23:51:03.000000000",
   925  	    "submitted": "2017-08-23 23:51:03.000000000",
   926  	    "insertions": 1,
   927  	    "deletions": 4,
   928  	    "unresolved_comment_count": 0,
   929  	    "has_review_started": true,
   930  	    "_number": 629279,
   931  	    "owner": {
   932  		"_account_id": 1178184
   933  	    }
   934  	}`
   935  	fakeCL4Str = `{
   936  	    "id": "infra%2Finfra~master~Ia292f77ae6bd94afbd746da0b08500f738904d15",
   937  	    "project": "infra/infra",
   938  	    "branch": "master",
   939  	    "hashtags": [],
   940  	    "change_id": "Ia292f77ae6bd94afbd746da0b08500f738904d15",
   941  	    "subject": "[Findit] Add flake analyzer forced rerun instructions to makefile.",
   942  	    "status": "ABANDONED",
   943  	    "created": "2017-08-23 17:25:40.000000000",
   944  	    "updated": "2017-08-23 22:51:03.000000000",
   945  	    "submitted": "2017-08-23 22:51:03.000000000",
   946  	    "insertions": 4,
   947  	    "deletions": 1,
   948  	    "unresolved_comment_count": 0,
   949  	    "has_review_started": true,
   950  	    "_number": 629279,
   951  	    "owner": {
   952  		"_account_id": 1178184
   953  	    },
   954  	    "_has_more_changes": true
   955  	}`
   956  	fakeCL5Str = `{
   957  	    "id": "infra%2Finfra~master~Ia292f77ae6bd94afbd746da0b08500f738904d15",
   958  	    "project": "infra/infra",
   959  	    "branch": "master",
   960  	    "hashtags": [],
   961  	    "change_id": "Ia292f77ae6bd94afbd746da0b08500f738904d15",
   962  	    "subject": "[Findit] Add flake analyzer forced rerun instructions to makefile.",
   963  	    "status": "ABANDONED",
   964  	    "created": "2017-08-23 17:25:40.000000000",
   965  	    "updated": "2017-08-23 22:51:03.000000000",
   966  	    "submitted": "2017-08-23 22:51:03.000000000",
   967  	    "insertions": 4,
   968  	    "deletions": 1,
   969  	    "unresolved_comment_count": 0,
   970  	    "has_review_started": true,
   971  	    "_number": 629279,
   972  	    "owner": {
   973  		"_account_id": 1178184
   974  	    },
   975  	    "labels": {
   976  		 "Verified": {
   977  			 "all": [{
   978  				 "value": 0,
   979  				 "_account_id": 1000096,
   980  				 "name": "John Doe",
   981  				 "email": "john.doe@example.com",
   982  				 "username": "jdoe"
   983  			 },
   984  			 {
   985  				 "value": 0,
   986  				 "_account_id": 1000097,
   987  				 "name": "Jane Roe",
   988  				 "email": "jane.roe@example.com",
   989  				 "username": "jroe"
   990  			 }],
   991  			  "values": {
   992  				  "-1": "Fails",
   993  				  " 0": "No score",
   994  				  "+1": "Verified"
   995  			  }
   996  
   997  		 },
   998  		 "Code-Review": {
   999  			 "disliked": {
  1000  				 "_account_id": 1000096,
  1001  				"name": "John Doe",
  1002  				"email": "john.doe@example.com",
  1003  				"username": "jdoe"
  1004  			 },
  1005  			 "all": [{
  1006  				"value": -1,
  1007  				"_account_id": 1000096,
  1008  				"name": "John Doe",
  1009  				"email": "john.doe@example.com",
  1010  				"username": "jdoe"
  1011  			 },
  1012  			 {
  1013  				"value": 1,
  1014  				"_account_id": 1000097,
  1015  				"name": "Jane Roe",
  1016  				"email": "jane.roe@example.com",
  1017  				"username": "jroe"
  1018  			}],
  1019  			"values": {
  1020  				"-2": "This shall not be merged",
  1021  				"-1": "I would prefer this is not merged as is",
  1022  				" 0": "No score",
  1023  				"+1": "Looks good to me, but someone else must approve",
  1024  				"+2": "Looks good to me, approved"
  1025  			}
  1026  		}
  1027  	    }
  1028  	}`
  1029  	fakeCL6Str = `{
  1030  	    "id": "infra%2Fluci%2Fluci-go~master~Id37e51c3b84bfc41bc88fa237ddf722f934f4fa4",
  1031  	    "project": "infra/luci/luci-go",
  1032  	    "branch": "master",
  1033  	    "hashtags": [],
  1034  	    "change_id": "Id37e51c3b84bfc41bc88fa237ddf722f934f4fa4",
  1035  	    "subject": "[vpython]: Re-add deprecated \"-spec\" flag.",
  1036  	    "status": "NEW",
  1037  	    "current_revision": "d6375c2ea5b0",
  1038  	    "created": "2017-08-21 18:46:58.000000000",
  1039  	    "updated": "2017-08-22 22:33:34.000000000",
  1040  	    "submit_type": "REBASE_ALWAYS",
  1041  	    "mergeable": true,
  1042  	    "insertions": 6,
  1043  	    "deletions": 0,
  1044  	    "unresolved_comment_count": 0,
  1045  	    "has_review_started": true,
  1046  	    "_number": 646267,
  1047  	    "owner": {
  1048  		"_account_id": 1118104
  1049  	    },
  1050  	    "reviewers": {
  1051  		    "CC": [
  1052  			    {"_account_id": 1118110},
  1053  			    {"_account_id": 1118111},
  1054  			    {"_account_id": 1118112}
  1055  		    ],
  1056  		    "REVIEWER": [
  1057  			    {"_account_id": 1118120},
  1058  			    {"_account_id": 1118121},
  1059  			    {"_account_id": 1118122}
  1060  		    ],
  1061  		    "REMOVED": [
  1062  			    {"_account_id": 1118130},
  1063  			    {"_account_id": 1118131},
  1064  			    {"_account_id": 1118132}
  1065  		    ]
  1066  	    }
  1067  	}`
  1068  	fakeComments1Str = `{
  1069  		"foo": [
  1070  	        {
  1071  	            "id": "61d1fbfb_63e8c695",
  1072  	            "author": {
  1073  	                "_account_id": 1002228,
  1074  	                "name": "John Doe",
  1075  	                "email": "johndoe@example.com"
  1076  	            },
  1077  	            "change_message_id": "c24215a84fdc9cec42c2d5eec4f488d172d39d7e",
  1078  	            "patch_set": 1,
  1079  	            "line": 3,
  1080  	            "range": {
  1081  	                "start_line": 3,
  1082  	                "start_character": 7,
  1083  	                "end_line": 3,
  1084  	                "end_character": 55
  1085  	            },
  1086  	            "updated": "2020-07-28 14:04:31.000000000",
  1087  	            "message": "",
  1088  	            "unresolved": true,
  1089  	            "in_reply_to": "",
  1090  	            "commit_id": "08a8326653eaa5f7aeea30348b63bf5e9595dc11"
  1091  	        }
  1092  	    ],
  1093  	    "bar": [
  1094  	        {
  1095  	            "id": "63e8c695_61d1fbfb",
  1096  	            "author": {
  1097  	                "_account_id": 1002228,
  1098  	                "name": "John Doe",
  1099  	                "email": "johndoe@example.com"
  1100  	            },
  1101  	            "change_message_id": "c24215a84fdc9cec42c2d5eec4f488d172d39d7e",
  1102  	            "patch_set": 1,
  1103  	            "line": 21,
  1104  	            "updated": "2020-07-28 14:04:31.000000000",
  1105  	            "message": "",
  1106  	            "unresolved": true,
  1107  	            "in_reply_to": "",
  1108  	            "commit_id": "08a8326653eaa5f7aeea30348b63bf5e9595dc11"
  1109  	        }
  1110  	    ]
  1111  	}`
  1112  	fakeRobotComments1Str = `{
  1113  		"foo": [
  1114  	        {
  1115  	            "id": "61d1fbfb_63e8c695",
  1116  	            "author": {
  1117  	                "_account_id": 1001234,
  1118  	                "name": "A Robot",
  1119  	                "email": "robot@example.com"
  1120  	            },
  1121  	            "change_message_id": "c24215a84fdc9cec42c2d5eec4f488d172d39d7e",
  1122  	            "patch_set": 1,
  1123  	            "line": 3,
  1124  	            "range": {
  1125  	                "start_line": 3,
  1126  	                "start_character": 7,
  1127  	                "end_line": 3,
  1128  	                "end_character": 55
  1129  	            },
  1130  	            "updated": "2020-07-28 14:04:31.000000000",
  1131  	            "message": "",
  1132  	            "in_reply_to": "",
  1133  	            "commit_id": "08a8326653eaa5f7aeea30348b63bf5e9595dc11",
  1134  				"robot_id": "somerobot",
  1135  				"robot_run_id": "run1"
  1136  	        }
  1137  	    ],
  1138  	    "bar": [
  1139  	        {
  1140  	            "id": "63e8c695_61d1fbfb",
  1141  	            "author": {
  1142  	                "_account_id": 1001234,
  1143  	                "name": "A Robot",
  1144  	                "email": "robot@example.com"
  1145  	            },
  1146  	            "change_message_id": "c24215a84fdc9cec42c2d5eec4f488d172d39d7e",
  1147  	            "patch_set": 1,
  1148  	            "line": 21,
  1149  	            "updated": "2020-07-28 14:04:31.000000000",
  1150  	            "message": "",
  1151  	            "in_reply_to": "",
  1152  	            "commit_id": "08a8326653eaa5f7aeea30348b63bf5e9595dc11",
  1153  				"robot_id": "somerobot",
  1154  				"robot_run_id": "run1"
  1155  	        }
  1156  	    ]
  1157  	}`
  1158  	fakeAccounts1Str = `[
  1159  	    {
  1160  	        "_account_id": 1002228,
  1161  	        "name": "John Doe",
  1162  	        "email": "johndoe@example.com"
  1163  	    },
  1164  	    {
  1165  	        "_account_id": 1002228,
  1166  	        "name": "Jane Doe",
  1167  	        "email": "janedoe@example.com"
  1168  	    }
  1169  	]`
  1170  )
  1171  
  1172  ////////////////////////////////////////////////////////////////////////////////
  1173  
  1174  func newMockClient(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, *Client) {
  1175  	srv := httptest.NewServer(http.HandlerFunc(handler))
  1176  	pu, _ := url.Parse(srv.URL)
  1177  	// Tests shouldn't sleep, so make sure we don't wait between request
  1178  	// attempts.
  1179  	retryStrategy := func() retry.Iterator {
  1180  		return &retry.Limited{Retries: 10, Delay: 0}
  1181  	}
  1182  	c := &Client{http.DefaultClient, *pu, retryStrategy}
  1183  	return srv, c
  1184  }