go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/gerrit/gerritfake/fake_test.go (about)

     1  // Copyright 2020 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 gerritfake
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  	"testing"
    22  	"time"
    23  
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/protobuf/types/known/timestamppb"
    26  
    27  	gerritutil "go.chromium.org/luci/common/api/gerrit"
    28  	"go.chromium.org/luci/common/clock/testclock"
    29  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    30  	"go.chromium.org/luci/grpc/grpcutil"
    31  
    32  	"go.chromium.org/luci/cv/internal/gerrit"
    33  
    34  	. "github.com/smartystreets/goconvey/convey"
    35  	. "go.chromium.org/luci/common/testing/assertions"
    36  )
    37  
    38  func TestRelationship(t *testing.T) {
    39  	t.Parallel()
    40  
    41  	Convey("Relationship works", t, func() {
    42  		ci1 := CI(1, PS(1), AllRevs())
    43  		ci2 := CI(2, PS(2), AllRevs())
    44  		ci3 := CI(3, PS(3), AllRevs())
    45  		ci4 := CI(4, PS(4), AllRevs())
    46  		f := WithCIs("host", ACLRestricted("infra"), ci1, ci2, ci3, ci4)
    47  		// Diamond using latest patchsets.
    48  		//      --<-- 2_2 --<--
    49  		//     /               \
    50  		//  1_1                 4_4
    51  		//     \               /
    52  		//      --<-- 3-3 --<--
    53  		f.SetDependsOn("host", ci4, ci3, ci2) // 2 parents.
    54  		f.SetDependsOn("host", ci3, ci1)
    55  		f.SetDependsOn("host", ci2, ci1)
    56  
    57  		// Chain made by prior patchsets.
    58  		//  2_1 --<-- 3_2 --<-- 4_3
    59  		f.SetDependsOn("host", "4_3", "3_2")
    60  		f.SetDependsOn("host", "3_2", "2_1")
    61  		ctx := context.Background()
    62  
    63  		Convey("with allowed project", func() {
    64  			gc, err := f.MakeClient(ctx, "host", "infra")
    65  			So(err, ShouldBeNil)
    66  
    67  			Convey("No relations", func() {
    68  				resp, err := gc.GetRelatedChanges(ctx, &gerritpb.GetRelatedChangesRequest{
    69  					Number:     4,
    70  					Project:    "infra/infra",
    71  					RevisionId: "1",
    72  				})
    73  				So(err, ShouldBeNil)
    74  				So(resp, ShouldResembleProto, &gerritpb.GetRelatedChangesResponse{})
    75  			})
    76  
    77  			Convey("Descendants only", func() {
    78  				resp, err := gc.GetRelatedChanges(ctx, &gerritpb.GetRelatedChangesRequest{
    79  					Number:     2,
    80  					Project:    "infra/infra",
    81  					RevisionId: "1",
    82  				})
    83  				So(err, ShouldBeNil)
    84  				sortRelated(resp)
    85  				So(resp, ShouldResembleProto, &gerritpb.GetRelatedChangesResponse{
    86  					Changes: []*gerritpb.GetRelatedChangesResponse_ChangeAndCommit{
    87  						{
    88  							Project: "infra/infra",
    89  							Commit: &gerritpb.CommitInfo{
    90  								Id:      "rev-000002-001",
    91  								Parents: []*gerritpb.CommitInfo_Parent{{Id: "fake_parent_commit"}},
    92  							},
    93  							Number:          2,
    94  							Patchset:        1,
    95  							CurrentPatchset: 2,
    96  						},
    97  						{
    98  							Project: "infra/infra",
    99  							Commit: &gerritpb.CommitInfo{
   100  								Id:      "rev-000003-002",
   101  								Parents: []*gerritpb.CommitInfo_Parent{{Id: "rev-000002-001"}},
   102  							},
   103  							Number:          3,
   104  							Patchset:        2,
   105  							CurrentPatchset: 3,
   106  						},
   107  						{
   108  							Project: "infra/infra",
   109  							Commit: &gerritpb.CommitInfo{
   110  								Id:      "rev-000004-003",
   111  								Parents: []*gerritpb.CommitInfo_Parent{{Id: "rev-000003-002"}},
   112  							},
   113  							Number:          4,
   114  							Patchset:        3,
   115  							CurrentPatchset: 4,
   116  						},
   117  					},
   118  				})
   119  			})
   120  
   121  			Convey("Diamond", func() {
   122  				resp, err := gc.GetRelatedChanges(ctx, &gerritpb.GetRelatedChangesRequest{
   123  					Number:     4,
   124  					RevisionId: "4",
   125  				})
   126  				So(err, ShouldBeNil)
   127  				sortRelated(resp)
   128  				So(resp, ShouldResembleProto, &gerritpb.GetRelatedChangesResponse{
   129  					Changes: []*gerritpb.GetRelatedChangesResponse_ChangeAndCommit{
   130  						{
   131  							Project: "infra/infra",
   132  							Commit: &gerritpb.CommitInfo{
   133  								Id:      "rev-000001-001",
   134  								Parents: []*gerritpb.CommitInfo_Parent{{Id: "fake_parent_commit"}},
   135  							},
   136  							Number:          1,
   137  							Patchset:        1,
   138  							CurrentPatchset: 1,
   139  						},
   140  						{
   141  							Project: "infra/infra",
   142  							Commit: &gerritpb.CommitInfo{
   143  								Id:      "rev-000002-002",
   144  								Parents: []*gerritpb.CommitInfo_Parent{{Id: "rev-000001-001"}},
   145  							},
   146  							Number:          2,
   147  							Patchset:        2,
   148  							CurrentPatchset: 2,
   149  						},
   150  						{
   151  							Project: "infra/infra",
   152  							Commit: &gerritpb.CommitInfo{
   153  								Id:      "rev-000003-003",
   154  								Parents: []*gerritpb.CommitInfo_Parent{{Id: "rev-000001-001"}},
   155  							},
   156  							Number:          3,
   157  							Patchset:        3,
   158  							CurrentPatchset: 3,
   159  						},
   160  						{
   161  							Project: "infra/infra",
   162  							Commit: &gerritpb.CommitInfo{
   163  								Id: "rev-000004-004",
   164  								Parents: []*gerritpb.CommitInfo_Parent{
   165  									{Id: "rev-000003-003"},
   166  									{Id: "rev-000002-002"},
   167  								},
   168  							},
   169  							Number:          4,
   170  							Patchset:        4,
   171  							CurrentPatchset: 4,
   172  						},
   173  					},
   174  				})
   175  			})
   176  
   177  			Convey("Part of Diamond", func() {
   178  				resp, err := gc.GetRelatedChanges(ctx, &gerritpb.GetRelatedChangesRequest{
   179  					Number:     3,
   180  					RevisionId: "3",
   181  				})
   182  				So(err, ShouldBeNil)
   183  				sortRelated(resp)
   184  				So(resp, ShouldResembleProto, &gerritpb.GetRelatedChangesResponse{
   185  					Changes: []*gerritpb.GetRelatedChangesResponse_ChangeAndCommit{
   186  						{
   187  							Project: "infra/infra",
   188  							Commit: &gerritpb.CommitInfo{
   189  								Id:      "rev-000001-001",
   190  								Parents: []*gerritpb.CommitInfo_Parent{{Id: "fake_parent_commit"}},
   191  							},
   192  							Number:          1,
   193  							Patchset:        1,
   194  							CurrentPatchset: 1,
   195  						},
   196  						{
   197  							Project: "infra/infra",
   198  							Commit: &gerritpb.CommitInfo{
   199  								Id:      "rev-000003-003",
   200  								Parents: []*gerritpb.CommitInfo_Parent{{Id: "rev-000001-001"}},
   201  							},
   202  							Number:          3,
   203  							Patchset:        3,
   204  							CurrentPatchset: 3,
   205  						},
   206  						{
   207  							Project: "infra/infra",
   208  							Commit: &gerritpb.CommitInfo{
   209  								Id: "rev-000004-004",
   210  								Parents: []*gerritpb.CommitInfo_Parent{
   211  									{Id: "rev-000003-003"},
   212  									{Id: "rev-000002-002"},
   213  								},
   214  							},
   215  							Number:          4,
   216  							Patchset:        4,
   217  							CurrentPatchset: 4,
   218  						},
   219  					},
   220  				})
   221  			})
   222  		})
   223  
   224  		Convey("with disallowed project", func() {
   225  			gc, err := f.MakeClient(ctx, "host", "spying-luci-project")
   226  			So(err, ShouldBeNil)
   227  			_, err = gc.GetRelatedChanges(ctx, &gerritpb.GetRelatedChangesRequest{
   228  				Number:     4,
   229  				RevisionId: "1",
   230  			})
   231  			So(err, ShouldNotBeNil)
   232  			So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
   233  		})
   234  	})
   235  }
   236  
   237  // sortRelated ensures deterministic yet ultimately abitrary order.
   238  func sortRelated(r *gerritpb.GetRelatedChangesResponse) {
   239  	key := func(i int) string {
   240  		c := r.GetChanges()[i]
   241  		return fmt.Sprintf("%40s:%020d:%020d", c.GetCommit().GetId(), c.GetNumber(), c.GetPatchset())
   242  	}
   243  	sort.Slice(r.GetChanges(), func(i, j int) bool { return key(i) < key(j) })
   244  }
   245  
   246  func TestFiles(t *testing.T) {
   247  	t.Parallel()
   248  
   249  	Convey("Files' handling works", t, func() {
   250  		sortedFiles := func(r *gerritpb.ListFilesResponse) []string {
   251  			fs := make([]string, 0, len(r.GetFiles()))
   252  			for f := range r.GetFiles() {
   253  				fs = append(fs, f)
   254  			}
   255  			sort.Strings(fs)
   256  			return fs
   257  		}
   258  		ciDefault := CI(1)
   259  		ciCustom := CI(2, Files("ps1/cus.tom", "bl.ah"), PS(2), Files("still/custom"))
   260  		ciNoFiles := CI(3, Files())
   261  		f := WithCIs("host", ACLRestricted("infra"), ciDefault, ciCustom, ciNoFiles)
   262  
   263  		ctx := context.Background()
   264  		gc, err := f.MakeClient(ctx, "host", "infra")
   265  		So(err, ShouldBeNil)
   266  
   267  		Convey("change or revision NotFound", func() {
   268  			_, err := gc.ListFiles(ctx, &gerritpb.ListFilesRequest{Number: 123213, RevisionId: "1"})
   269  			So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
   270  			_, err = gc.ListFiles(ctx, &gerritpb.ListFilesRequest{
   271  				Number:     ciDefault.GetNumber(),
   272  				RevisionId: "not existing",
   273  			})
   274  			So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
   275  		})
   276  
   277  		Convey("Default", func() {
   278  			resp, err := gc.ListFiles(ctx, &gerritpb.ListFilesRequest{
   279  				Number:     ciDefault.GetNumber(),
   280  				RevisionId: ciDefault.GetCurrentRevision(),
   281  			})
   282  			So(err, ShouldBeNil)
   283  			So(sortedFiles(resp), ShouldResemble, []string{"ps001/c.cpp", "shared/s.py"})
   284  		})
   285  
   286  		Convey("Custom", func() {
   287  			resp, err := gc.ListFiles(ctx, &gerritpb.ListFilesRequest{
   288  				Number:     ciCustom.GetNumber(),
   289  				RevisionId: "1",
   290  			})
   291  			So(err, ShouldBeNil)
   292  			So(sortedFiles(resp), ShouldResemble, []string{"bl.ah", "ps1/cus.tom"})
   293  			resp, err = gc.ListFiles(ctx, &gerritpb.ListFilesRequest{
   294  				Number:     ciCustom.GetNumber(),
   295  				RevisionId: "2",
   296  			})
   297  			So(err, ShouldBeNil)
   298  			So(sortedFiles(resp), ShouldResemble, []string{"still/custom"})
   299  		})
   300  
   301  		Convey("NoFiles", func() {
   302  			resp, err := gc.ListFiles(ctx, &gerritpb.ListFilesRequest{
   303  				Number:     ciNoFiles.GetNumber(),
   304  				RevisionId: ciNoFiles.GetCurrentRevision(),
   305  			})
   306  			So(err, ShouldBeNil)
   307  			So(resp.GetFiles(), ShouldHaveLength, 0)
   308  		})
   309  	})
   310  }
   311  
   312  func TestGetChange(t *testing.T) {
   313  	t.Parallel()
   314  
   315  	Convey("GetChange handling works", t, func() {
   316  		ci := CI(100100, PS(4), AllRevs())
   317  		So(ci.GetRevisions(), ShouldHaveLength, 4)
   318  		f := WithCIs("host", ACLRestricted("infra"), ci)
   319  
   320  		ctx := context.Background()
   321  		gc, err := f.MakeClient(ctx, "host", "infra")
   322  		So(err, ShouldBeNil)
   323  
   324  		Convey("NotFound", func() {
   325  			_, err := gc.GetChange(ctx, &gerritpb.GetChangeRequest{Number: 12321})
   326  			So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
   327  		})
   328  
   329  		Convey("Default", func() {
   330  			resp, err := gc.GetChange(ctx, &gerritpb.GetChangeRequest{Number: 100100})
   331  			So(err, ShouldBeNil)
   332  			So(resp.GetCurrentRevision(), ShouldEqual, "")
   333  			So(resp.GetRevisions(), ShouldHaveLength, 0)
   334  			So(resp.GetLabels(), ShouldHaveLength, 0)
   335  		})
   336  
   337  		Convey("CURRENT_REVISION", func() {
   338  			resp, err := gc.GetChange(ctx, &gerritpb.GetChangeRequest{
   339  				Number:  100100,
   340  				Options: []gerritpb.QueryOption{gerritpb.QueryOption_CURRENT_REVISION}})
   341  			So(err, ShouldBeNil)
   342  			So(resp.GetRevisions(), ShouldHaveLength, 1)
   343  			So(resp.GetRevisions()[resp.GetCurrentRevision()], ShouldNotBeNil)
   344  		})
   345  
   346  		Convey("Full", func() {
   347  			resp, err := gc.GetChange(ctx, &gerritpb.GetChangeRequest{
   348  				Number: 100100,
   349  				Options: []gerritpb.QueryOption{
   350  					gerritpb.QueryOption_ALL_REVISIONS,
   351  					gerritpb.QueryOption_DETAILED_ACCOUNTS,
   352  					gerritpb.QueryOption_DETAILED_LABELS,
   353  					gerritpb.QueryOption_SKIP_MERGEABLE,
   354  					gerritpb.QueryOption_MESSAGES,
   355  					gerritpb.QueryOption_SUBMITTABLE,
   356  				}})
   357  			So(err, ShouldBeNil)
   358  			So(resp, ShouldResembleProto, ci)
   359  		})
   360  	})
   361  }
   362  
   363  func TestListChanges(t *testing.T) {
   364  	t.Parallel()
   365  
   366  	Convey("ListChanges works", t, func() {
   367  		f := WithCIs("empty", ACLRestricted("empty"))
   368  		ctx := context.Background()
   369  
   370  		mustCurrentClient := func(host, luciProject string) gerrit.Client {
   371  			cl, err := f.MakeClient(ctx, host, luciProject)
   372  			So(err, ShouldBeNil)
   373  			return cl
   374  		}
   375  
   376  		listChangeIDs := func(client gerrit.Client, req *gerritpb.ListChangesRequest) []int {
   377  			out, err := client.ListChanges(ctx, req)
   378  			So(err, ShouldBeNil)
   379  			So(out.GetMoreChanges(), ShouldBeFalse)
   380  			ids := make([]int, len(out.GetChanges()))
   381  			for i, ch := range out.GetChanges() {
   382  				ids[i] = int(ch.GetNumber())
   383  				if i > 0 {
   384  					// Ensure monotonically non-decreasing update timestamps.
   385  					prior := out.GetChanges()[i-1]
   386  					So(prior.GetUpdated().AsTime().Before(ch.GetUpdated().AsTime()), ShouldBeFalse)
   387  				}
   388  			}
   389  			return ids
   390  		}
   391  
   392  		f.AddFrom(WithCIs("chrome-internal", ACLRestricted("infra-internal"),
   393  			CI(9001, Project("infra/infra-internal")),
   394  			CI(9002, Project("infra/infra-internal")),
   395  		))
   396  
   397  		Convey("ACLs enforced", func() {
   398  			So(listChangeIDs(mustCurrentClient("chrome-internal", "spy"),
   399  				&gerritpb.ListChangesRequest{}), ShouldResemble, []int{})
   400  			So(listChangeIDs(mustCurrentClient("chrome-internal", "infra-internal"),
   401  				&gerritpb.ListChangesRequest{}), ShouldResemble, []int{9002, 9001})
   402  		})
   403  
   404  		var epoch = time.Date(2011, time.February, 3, 4, 5, 6, 7, time.UTC)
   405  		u0 := Updated(epoch)
   406  		u1 := Updated(epoch.Add(time.Minute))
   407  		u2 := Updated(epoch.Add(2 * time.Minute))
   408  		f.AddFrom(WithCIs("chromium", ACLPublic(),
   409  			CI(8001, u1, Project("infra/infra"), CQ(+2)),
   410  			CI(8002, u2, Project("infra/luci/luci-go"), Vote("Commit-Queue", +1), Vote("Code-Review", -1)),
   411  			CI(8003, u0, Project("infra/luci/luci-go"), Status("MERGED"), Vote("Code-Review", +1)),
   412  		))
   413  
   414  		Convey("Order and limit", func() {
   415  			g := mustCurrentClient("chromium", "anyone")
   416  			So(listChangeIDs(g, &gerritpb.ListChangesRequest{}), ShouldResemble, []int{8002, 8001, 8003})
   417  
   418  			out, err := g.ListChanges(ctx, &gerritpb.ListChangesRequest{Limit: 2})
   419  			So(err, ShouldBeNil)
   420  			So(out.GetMoreChanges(), ShouldBeTrue)
   421  			So(out.GetChanges()[0].GetNumber(), ShouldEqual, 8002)
   422  			So(out.GetChanges()[1].GetNumber(), ShouldEqual, 8001)
   423  		})
   424  
   425  		Convey("Filtering works", func() {
   426  			query := func(q string) []int {
   427  				return listChangeIDs(mustCurrentClient("chromium", "anyone"),
   428  					&gerritpb.ListChangesRequest{Query: q})
   429  			}
   430  			Convey("before/after", func() {
   431  				So(gerritutil.FormatTime(epoch), ShouldResemble, `"2011-02-03 04:05:06.000000007"`)
   432  				So(query(`before:"2011-02-03 04:05:06.000000006"`), ShouldResemble, []int{})
   433  				// 1 ns later
   434  				So(query(`before:"2011-02-03 04:05:06.000000007"`), ShouldResemble, []int{8003})
   435  				So(query(` after:"2011-02-03 04:05:06.000000007"`), ShouldResemble, []int{8002, 8001, 8003})
   436  				// 1 minute later
   437  				So(query(` after:"2011-02-03 04:06:06.000000007"`), ShouldResemble, []int{8002, 8001})
   438  				// 1 minute later
   439  				So(query(` after:"2011-02-03 04:07:06.000000007"`), ShouldResemble, []int{8002})
   440  				// Surround middle CL:
   441  				So(query(``+
   442  					` after:"2011-02-03 04:05:30.000000000" `+
   443  					`before:"2011-02-03 04:06:30.000000000"`), ShouldResemble, []int{8001})
   444  			})
   445  			Convey("Project prefix", func() {
   446  				So(query(`projects:"inf"`), ShouldResemble, []int{8002, 8001, 8003})
   447  				So(query(`projects:"infra/"`), ShouldResemble, []int{8002, 8001, 8003})
   448  				So(query(`projects:"infra/luci"`), ShouldResemble, []int{8002, 8003})
   449  				So(query(`projects:"typo"`), ShouldResemble, []int{})
   450  			})
   451  			Convey("Project exact", func() {
   452  				So(query(`project:"infra/infra"`), ShouldResemble, []int{8001})
   453  				So(query(`project:"infra"`), ShouldResemble, []int{})
   454  				So(query(`(project:"infra/infra" OR project:"infra/luci/luci-go")`), ShouldResemble,
   455  					[]int{8002, 8001, 8003})
   456  			})
   457  			Convey("Status", func() {
   458  				So(query(`status:new`), ShouldResemble, []int{8002, 8001})
   459  				So(query(`status:abandoned`), ShouldResemble, []int{})
   460  				So(query(`status:merged`), ShouldResemble, []int{8003})
   461  			})
   462  			Convey("label", func() {
   463  				So(query(`label:Commit-Queue>0`), ShouldResemble, []int{8002, 8001})
   464  				So(query(`label:Commit-Queue>1`), ShouldResemble, []int{8001})
   465  				So(query(`label:Code-Review>-1`), ShouldResemble, []int{8003})
   466  			})
   467  			Convey("Typical CV query", func() {
   468  				So(query(`label:Commit-Queue>0 status:NEW project:"infra/infra"`),
   469  					ShouldResemble, []int{8001})
   470  				So(query(`label:Commit-Queue>0 status:NEW projects:"infra"`),
   471  					ShouldResemble, []int{8002, 8001})
   472  				So(query(`label:Commit-Queue>0 status:NEW projects:"infra"`+
   473  					` after:"2011-02-03 04:06:30.000000000" `+
   474  					`before:"2011-02-03 04:08:30.000000000"`), ShouldResemble, []int{8002})
   475  				So(query(`label:Commit-Queue>0 status:NEW `+
   476  					`(project:"infra" OR project:"infra/luci/luci-go")`+
   477  					` after:"2011-02-03 04:06:30.000000000" `+
   478  					`before:"2011-02-03 04:08:30.000000000"`), ShouldResemble, []int{8002})
   479  			})
   480  		})
   481  
   482  		Convey("Bad queries", func() {
   483  			test := func(query string) error {
   484  				client, err := f.MakeClient(ctx, "infra", "chromium")
   485  				So(err, ShouldBeNil)
   486  				_, err = client.ListChanges(ctx, &gerritpb.ListChangesRequest{Query: query})
   487  				So(grpcutil.Code(err), ShouldEqual, codes.InvalidArgument)
   488  				So(err, ShouldErrLike, `invalid query argument`)
   489  				return err
   490  			}
   491  
   492  			So(test(`"unmatched quote`), ShouldErrLike, `invalid query argument "\"unmatched quote"`)
   493  			So(test(`status:new "unmatched`), ShouldErrLike, `unrecognized token "\"unmatched`)
   494  			So(test(`project:"unmatched`), ShouldErrLike, `"project:\"unmatched": expected quoted string`)
   495  			So(test(`project:raw/not/supported`), ShouldErrLike, `expected quoted string`)
   496  			So(test(`project:"one" OR project:"two"`), ShouldErrLike, `"OR" must be inside ()`)
   497  			So(test(`project:"one" project:"two")`), ShouldErrLike, `"project:" must be inside ()`)
   498  			// This error can be better, but UX isn't essential for a fake.
   499  			So(test(`(project:"one" OR`), ShouldErrLike, `"" must be outside of ()`)
   500  
   501  			So(test(`status:rand-om`), ShouldErrLike, `unrecognized status "rand-om"`)
   502  			So(test(`status:0`), ShouldErrLike, `unrecognized status "0"`)
   503  			So(test(`label:0`), ShouldErrLike, `invalid label: 0`)
   504  			So(test(`label:Commit-Queue`), ShouldErrLike, `invalid label: Commit-Queue`)
   505  
   506  			// Note these are actually allowed in Gerrit.
   507  			So(test(`label:Commit-Queue<1`), ShouldErrLike, `invalid label: Commit-Queue<1`)
   508  			So(test(`before:2019-20-01`), ShouldErrLike, `failed to parse Gerrit timestamp "2019-20-01"`)
   509  			So(test(` after:2019-20-01`), ShouldErrLike, `failed to parse Gerrit timestamp "2019-20-01"`)
   510  			So(test(`before:"2019-20-01"`), ShouldErrLike, `failed to parse Gerrit timestamp "\"2019-20-01\""`)
   511  		})
   512  	})
   513  }
   514  
   515  func TestSetReview(t *testing.T) {
   516  	t.Parallel()
   517  
   518  	Convey("SetReview", t, func() {
   519  		ctx, tc := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC)
   520  		user := U("user-123")
   521  		accountID := user.AccountId
   522  		before := tc.Now().Add(-1 * time.Minute)
   523  		ciBefore := CI(10001, CQ(1, before, user), Updated(before))
   524  		f := WithCIs(
   525  			"example",
   526  			ACLGrant(OpReview, codes.PermissionDenied, "chromium").Or(ACLGrant(OpAlterVotesOfOthers, codes.PermissionDenied, "chromium")),
   527  			ciBefore,
   528  		)
   529  		tc.Add(2 * time.Minute)
   530  
   531  		mustWriterClient := func(host, luciProject string) gerrit.Client {
   532  			cl, err := f.MakeClient(ctx, host, luciProject)
   533  			So(err, ShouldBeNil)
   534  			return cl
   535  		}
   536  
   537  		latestCI := func() *gerritpb.ChangeInfo {
   538  			return f.GetChange("example", 10001).Info
   539  		}
   540  		Convey("ACLs enforced", func() {
   541  			client := mustWriterClient("example", "not-chromium")
   542  			res, err := client.SetReview(ctx, &gerritpb.SetReviewRequest{
   543  				Number: 11111,
   544  			})
   545  			So(res, ShouldBeNil)
   546  			So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
   547  
   548  			res, err = client.SetReview(ctx, &gerritpb.SetReviewRequest{
   549  				Number:  10001,
   550  				Message: "this is a message",
   551  			})
   552  			So(res, ShouldBeNil)
   553  			So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
   554  
   555  			res, err = client.SetReview(ctx, &gerritpb.SetReviewRequest{
   556  				Number: 10001,
   557  				Labels: map[string]int32{
   558  					"Commit-Queue": 0,
   559  				},
   560  			})
   561  			So(res, ShouldBeNil)
   562  			So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
   563  
   564  			res, err = client.SetReview(ctx, &gerritpb.SetReviewRequest{
   565  				Number: 10001,
   566  				Labels: map[string]int32{
   567  					"Commit-Queue": 0,
   568  				},
   569  				OnBehalfOf: accountID,
   570  			})
   571  			So(res, ShouldBeNil)
   572  			So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
   573  		})
   574  
   575  		Convey("Post message", func() {
   576  			client := mustWriterClient("example", "chromium")
   577  			res, err := client.SetReview(ctx, &gerritpb.SetReviewRequest{
   578  				Number:  10001,
   579  				Message: "this is a message",
   580  			})
   581  			So(err, ShouldBeNil)
   582  			So(res, ShouldResembleProto, &gerritpb.ReviewResult{})
   583  			So(latestCI().GetUpdated().AsTime(), ShouldHappenAfter, ciBefore.GetUpdated().AsTime())
   584  			So(latestCI().GetMessages(), ShouldResembleProto, []*gerritpb.ChangeMessageInfo{
   585  				{
   586  					Id:      "0",
   587  					Author:  U("chromium"),
   588  					Date:    timestamppb.New(tc.Now()),
   589  					Message: "this is a message",
   590  				},
   591  			})
   592  		})
   593  
   594  		Convey("Set vote", func() {
   595  			client := mustWriterClient("example", "chromium")
   596  			res, err := client.SetReview(ctx, &gerritpb.SetReviewRequest{
   597  				Number: 10001,
   598  				Labels: map[string]int32{
   599  					"Commit-Queue": 2,
   600  				},
   601  			})
   602  			So(err, ShouldBeNil)
   603  			So(res, ShouldResembleProto, &gerritpb.ReviewResult{
   604  				Labels: map[string]int32{
   605  					"Commit-Queue": 2,
   606  				},
   607  			})
   608  			So(latestCI().GetUpdated().AsTime(), ShouldHappenAfter, ciBefore.GetUpdated().AsTime())
   609  			So(latestCI().GetLabels()["Commit-Queue"].GetAll(), ShouldResembleProto, []*gerritpb.ApprovalInfo{
   610  				{
   611  					User:  user,
   612  					Value: 1,
   613  					Date:  timestamppb.New(before),
   614  				},
   615  				{
   616  					User:  U("chromium"),
   617  					Value: 2,
   618  					Date:  timestamppb.New(tc.Now()),
   619  				},
   620  			})
   621  		})
   622  
   623  		Convey("Set vote on behalf of", func() {
   624  			client := mustWriterClient("example", "chromium")
   625  			Convey("existing voter", func() {
   626  				res, err := client.SetReview(ctx, &gerritpb.SetReviewRequest{
   627  					Number: 10001,
   628  					Labels: map[string]int32{
   629  						"Commit-Queue": 0,
   630  					},
   631  					OnBehalfOf: 123,
   632  				})
   633  				So(err, ShouldBeNil)
   634  				So(res, ShouldResembleProto, &gerritpb.ReviewResult{
   635  					Labels: map[string]int32{
   636  						"Commit-Queue": 0,
   637  					},
   638  				})
   639  				So(latestCI().GetUpdated().AsTime(), ShouldHappenAfter, ciBefore.GetUpdated().AsTime())
   640  				So(NonZeroVotes(latestCI(), "Commit-Queue"), ShouldBeEmpty)
   641  			})
   642  
   643  			Convey("new voter", func() {
   644  				res, err := client.SetReview(ctx, &gerritpb.SetReviewRequest{
   645  					Number: 10001,
   646  					Labels: map[string]int32{
   647  						"Commit-Queue": 1,
   648  					},
   649  					OnBehalfOf: 789,
   650  				})
   651  				So(err, ShouldBeNil)
   652  				So(res, ShouldResembleProto, &gerritpb.ReviewResult{
   653  					Labels: map[string]int32{
   654  						"Commit-Queue": 1,
   655  					},
   656  				})
   657  				So(latestCI().GetUpdated().AsTime(), ShouldHappenAfter, ciBefore.GetUpdated().AsTime())
   658  				So(latestCI().GetLabels()["Commit-Queue"].GetAll(), ShouldResembleProto, []*gerritpb.ApprovalInfo{
   659  					{
   660  						User:  user,
   661  						Value: 1,
   662  						Date:  timestamppb.New(before),
   663  					},
   664  					{
   665  						User:  U("user-789"),
   666  						Value: 1,
   667  						Date:  timestamppb.New(tc.Now()),
   668  					},
   669  				})
   670  			})
   671  		})
   672  	})
   673  }
   674  
   675  func TestSubmitRevision(t *testing.T) {
   676  	t.Parallel()
   677  
   678  	Convey("SubmitRevision", t, func() {
   679  		const gHost = "example.com"
   680  		ctx, tc := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC)
   681  		var (
   682  			ciSingular  = CI(10001, Updated(tc.Now()), PS(3), AllRevs())
   683  			ciStackBase = CI(20001, Updated(tc.Now()), PS(1), AllRevs())
   684  			ciStackMid  = CI(20002, Updated(tc.Now()), PS(1), AllRevs())
   685  			ciStackTop  = CI(20003, Updated(tc.Now()), PS(1), AllRevs())
   686  		)
   687  		f := WithCIs(
   688  			"example.com",
   689  			ACLGrant(OpSubmit, codes.PermissionDenied, "chromium"),
   690  			ciSingular, ciStackBase, ciStackMid, ciStackTop,
   691  		)
   692  		f.SetDependsOn(gHost, ciStackMid, ciStackBase)
   693  		f.SetDependsOn(gHost, ciStackTop, ciStackMid)
   694  
   695  		tc.Add(2 * time.Minute)
   696  
   697  		assertStatus := func(s gerritpb.ChangeStatus, cis ...*gerritpb.ChangeInfo) {
   698  			for _, ci := range cis {
   699  				latestCI := f.GetChange(gHost, int(ci.GetNumber())).Info
   700  				So(latestCI.GetStatus(), ShouldEqual, s)
   701  			}
   702  		}
   703  
   704  		mustWriterClient := func(host, luciProject string) gerrit.Client {
   705  			cl, err := f.MakeClient(ctx, host, luciProject)
   706  			So(err, ShouldBeNil)
   707  			return cl
   708  		}
   709  
   710  		Convey("ACLs enforced", func() {
   711  			client := mustWriterClient(gHost, "not-chromium")
   712  			_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
   713  				Number:     ciSingular.GetNumber(),
   714  				RevisionId: ciSingular.GetCurrentRevision(),
   715  			})
   716  			So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
   717  			assertStatus(gerritpb.ChangeStatus_NEW, ciSingular)
   718  		})
   719  
   720  		Convey("ACLs enforced with CL stack", func() {
   721  			f.MutateChange(gHost, int(ciStackBase.GetNumber()), func(c *Change) {
   722  				c.ACLs = ACLGrant(OpSubmit, codes.PermissionDenied, "pretty-much-denied-to-everyone")
   723  			})
   724  			client := mustWriterClient(gHost, "chromium")
   725  			_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
   726  				Number:     ciStackTop.GetNumber(),
   727  				RevisionId: ciStackTop.GetCurrentRevision(),
   728  			})
   729  			So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
   730  			assertStatus(gerritpb.ChangeStatus_NEW, ciStackBase, ciStackMid, ciStackTop)
   731  		})
   732  
   733  		Convey("Non-existent revision", func() {
   734  			client := mustWriterClient(gHost, "chromium")
   735  			_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
   736  				Number:     ciSingular.GetNumber(),
   737  				RevisionId: "non-existent",
   738  			})
   739  			So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
   740  			assertStatus(gerritpb.ChangeStatus_NEW, ciSingular)
   741  		})
   742  
   743  		Convey("Old revision", func() {
   744  			client := mustWriterClient(gHost, "chromium")
   745  			var oldRev string
   746  			for rev := range ciSingular.GetRevisions() {
   747  				if rev != ciSingular.GetCurrentRevision() {
   748  					oldRev = rev
   749  					break
   750  				}
   751  			}
   752  			So(oldRev, ShouldNotBeEmpty)
   753  			_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
   754  				Number:     ciSingular.GetNumber(),
   755  				RevisionId: oldRev,
   756  			})
   757  			So(grpcutil.Code(err), ShouldEqual, codes.FailedPrecondition)
   758  			So(err, ShouldErrLike, "is not current")
   759  		})
   760  
   761  		Convey("Works", func() {
   762  			client := mustWriterClient(gHost, "chromium")
   763  			res, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
   764  				Number:     ciSingular.GetNumber(),
   765  				RevisionId: ciSingular.GetCurrentRevision(),
   766  			})
   767  			So(err, ShouldBeNil)
   768  			So(res, ShouldResembleProto, &gerritpb.SubmitInfo{
   769  				Status: gerritpb.ChangeStatus_MERGED,
   770  			})
   771  			assertStatus(gerritpb.ChangeStatus_MERGED, ciSingular)
   772  		})
   773  
   774  		Convey("Works with CL stack", func() {
   775  			client := mustWriterClient(gHost, "chromium")
   776  			res, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
   777  				Number:     ciStackTop.GetNumber(),
   778  				RevisionId: ciStackTop.GetCurrentRevision(),
   779  			})
   780  			So(err, ShouldBeNil)
   781  			So(res, ShouldResembleProto, &gerritpb.SubmitInfo{
   782  				Status: gerritpb.ChangeStatus_MERGED,
   783  			})
   784  			assertStatus(gerritpb.ChangeStatus_MERGED, ciStackBase, ciStackMid, ciStackTop)
   785  		})
   786  
   787  		Convey("Already Merged", func() {
   788  			f.MutateChange(gHost, int(ciSingular.GetNumber()), func(c *Change) {
   789  				c.Info.Status = gerritpb.ChangeStatus_MERGED
   790  			})
   791  			client := mustWriterClient(gHost, "chromium")
   792  			_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
   793  				Number:     ciSingular.GetNumber(),
   794  				RevisionId: ciSingular.GetCurrentRevision(),
   795  			})
   796  			So(grpcutil.Code(err), ShouldEqual, codes.FailedPrecondition)
   797  			So(err, ShouldErrLike, "change is merged")
   798  		})
   799  
   800  		Convey("already merged ones are skipped inside a CL Stack", func() {
   801  			verify := func() {
   802  				client := mustWriterClient(gHost, "chromium")
   803  				res, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
   804  					Number:     ciStackTop.GetNumber(),
   805  					RevisionId: ciStackTop.GetCurrentRevision(),
   806  				})
   807  				So(err, ShouldBeNil)
   808  				So(res, ShouldResembleProto, &gerritpb.SubmitInfo{
   809  					Status: gerritpb.ChangeStatus_MERGED,
   810  				})
   811  			}
   812  			Convey("base of stack", func() {
   813  				f.MutateChange(gHost, int(ciStackBase.GetNumber()), func(c *Change) {
   814  					c.Info.Status = gerritpb.ChangeStatus_MERGED
   815  				})
   816  				verify()
   817  				assertStatus(gerritpb.ChangeStatus_MERGED, ciStackBase, ciStackMid, ciStackTop)
   818  			})
   819  			Convey("mid-stack", func() {
   820  				// May happen if the mid-CL was at some point re-based on top of
   821  				// something other than ciStackBase and then submitted.
   822  				f.MutateChange(gHost, int(ciStackMid.GetNumber()), func(c *Change) {
   823  					c.Info.Status = gerritpb.ChangeStatus_MERGED
   824  					PS(int(c.Info.GetRevisions()[c.Info.GetCurrentRevision()].GetNumber() + 1))(c.Info)
   825  				})
   826  				verify()
   827  				assertStatus(gerritpb.ChangeStatus_MERGED, ciStackMid, ciStackTop)
   828  				assertStatus(gerritpb.ChangeStatus_NEW, ciStackBase)
   829  			})
   830  		})
   831  
   832  		Convey("Abandoned", func() {
   833  			f.MutateChange(gHost, int(ciSingular.GetNumber()), func(c *Change) {
   834  				c.Info.Status = gerritpb.ChangeStatus_ABANDONED
   835  			})
   836  			client := mustWriterClient(gHost, "chromium")
   837  			_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
   838  				Number:     ciSingular.GetNumber(),
   839  				RevisionId: ciSingular.GetCurrentRevision(),
   840  			})
   841  			So(grpcutil.Code(err), ShouldEqual, codes.FailedPrecondition)
   842  			So(err, ShouldErrLike, "change is abandoned")
   843  		})
   844  		Convey("Abandoned inside CL stack", func() {
   845  			f.MutateChange(gHost, int(ciStackMid.GetNumber()), func(c *Change) {
   846  				c.Info.Status = gerritpb.ChangeStatus_ABANDONED
   847  			})
   848  			client := mustWriterClient(gHost, "chromium")
   849  			_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
   850  				Number:     ciStackTop.GetNumber(),
   851  				RevisionId: ciStackTop.GetCurrentRevision(),
   852  			})
   853  			So(grpcutil.Code(err), ShouldEqual, codes.FailedPrecondition)
   854  			So(err, ShouldErrLike, "change is abandoned")
   855  		})
   856  	})
   857  }