Skip to content

Commit f91175f

Browse files
committed
test(e2e): cover gateway List API
1 parent 418983a commit f91175f

4 files changed

Lines changed: 184 additions & 0 deletions

File tree

service/submitqueue/gateway/server/queues.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
queues:
77
- name: test-queue
88
- name: e2e-test-queue
9+
- name: e2e-list-queue
910
- name: e2e-cancel-queue
1011
# Routes to an analyzer that always errors (conflictfake.FailAlways) so e2e can
1112
# exercise the conflict-analysis error path. See newQueueRegistry in the

service/submitqueue/orchestrator/server/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,7 @@ func newQueueRegistry(logger *zap.Logger, scope tally.Scope, resolver changeset.
941941
byQueue: map[string]queueExtensions{
942942
"test-queue": testQueue,
943943
"e2e-test-queue": e2eQueue,
944+
"e2e-list-queue": e2eQueue,
944945
"e2e-conflict-error-queue": conflictErrQueue,
945946
"file-overlap-queue": fileOverlapQueue,
946947
},

test/e2e/submitqueue/harness_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,65 @@ func (s *E2EIntegrationSuite) land(queue string, uris ...string) string {
5151
return resp.Sqid
5252
}
5353

54+
// list calls the gateway List RPC and fails the test on unexpected errors.
55+
func (s *E2EIntegrationSuite) list(req *gatewaypb.ListRequest) *gatewaypb.ListResponse {
56+
t := s.T()
57+
resp, err := s.gatewayClient.List(s.ctx, req)
58+
require.NoError(t, err, "List failed for queue %s", req.Queue)
59+
return resp
60+
}
61+
62+
// awaitListContains polls List until all expected sqids are visible in one
63+
// response page. The gateway updates request summaries synchronously for Land
64+
// and asynchronously from the log topic for later statuses, so callers use the
65+
// same bounded polling style as Status.
66+
func (s *E2EIntegrationSuite) awaitListContains(req *gatewaypb.ListRequest, want ...string) *gatewaypb.ListResponse {
67+
t := s.T()
68+
var resp *gatewaypb.ListResponse
69+
require.Eventually(t, func() bool {
70+
var err error
71+
resp, err = s.gatewayClient.List(s.ctx, req)
72+
if err != nil {
73+
s.log.Logf("List(%s) not ready yet: %v", req.Queue, err)
74+
return false
75+
}
76+
got := summarySQIDs(resp.Requests)
77+
s.log.Logf("List(%s) = %v (want %v)", req.Queue, got, want)
78+
return containsAll(got, want)
79+
}, persistTimeout, persistPollInterval,
80+
"List(%s) should contain sqids %v", req.Queue, want)
81+
return resp
82+
}
83+
84+
func summarySQIDs(summaries []*gatewaypb.RequestSummary) []string {
85+
out := make([]string, len(summaries))
86+
for i, summary := range summaries {
87+
out[i] = summary.Sqid
88+
}
89+
return out
90+
}
91+
92+
func containsAll(got []string, want []string) bool {
93+
seen := make(map[string]struct{}, len(got))
94+
for _, sqid := range got {
95+
seen[sqid] = struct{}{}
96+
}
97+
for _, sqid := range want {
98+
if _, ok := seen[sqid]; !ok {
99+
return false
100+
}
101+
}
102+
return true
103+
}
104+
105+
func mapFromStrings(values []string) map[string]struct{} {
106+
out := make(map[string]struct{}, len(values))
107+
for _, value := range values {
108+
out[value] = struct{}{}
109+
}
110+
return out
111+
}
112+
54113
// currentStatus reads the request's current customer-facing status via the
55114
// Status RPC. A transport error is returned so callers can keep polling.
56115
func (s *E2EIntegrationSuite) currentStatus(sqid string) (entity.RequestStatus, error) {

test/e2e/submitqueue/suite_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,129 @@ func (s *E2EIntegrationSuite) TestLand_HappyPath_ReachesLanded() {
218218
"operating store should show request %s in terminal state landed", sqid)
219219
}
220220

221+
// TestList_ReturnsFilteredPagedSummaries verifies the customer-facing List RPC
222+
// against the full stack. Land writes the initial summary through the gateway,
223+
// later status updates arrive through the request-log topic, and List reads the
224+
// gateway-owned summary read model through the public RPC surface.
225+
func (s *E2EIntegrationSuite) TestList_ReturnsFilteredPagedSummaries() {
226+
t := s.T()
227+
228+
beforeWindow := s.land("e2e-list-queue", "github://uber/e2e-list/pull/100/abcdef0123456789abcdef0123456789abcdef00")
229+
startTimeMs := time.Now().UnixMilli() + 1
230+
windowTimer := time.NewTimer(time.Until(time.UnixMilli(startTimeMs)))
231+
<-windowTimer.C
232+
firstURI := "github://uber/e2e-list/pull/101/abcdef0123456789abcdef0123456789abcdef01"
233+
secondURI := "github://uber/e2e-list/pull/102/abcdef0123456789abcdef0123456789abcdef02"
234+
thirdURI := "github://uber/e2e-list/pull/103/abcdef0123456789abcdef0123456789abcdef03"
235+
otherURI := "github://uber/e2e-list-other/pull/201/abcdef0123456789abcdef0123456789abcdef03"
236+
237+
first := s.land("e2e-list-queue", firstURI)
238+
second := s.land("e2e-list-queue", secondURI)
239+
third := s.land("e2e-list-queue", thirdURI)
240+
otherQueue := s.land("e2e-cancel-queue", otherURI)
241+
endTimeMs := time.Now().Add(time.Minute).UnixMilli()
242+
243+
resp := s.awaitListContains(&gatewaypb.ListRequest{
244+
Queue: "e2e-list-queue",
245+
StartTimeMs: startTimeMs,
246+
EndTimeMs: endTimeMs,
247+
PageSize: 10,
248+
}, first, second, third)
249+
250+
assert.NotContains(t, summarySQIDs(resp.Requests), otherQueue,
251+
"List should not return requests from a different queue")
252+
assert.NotContains(t, summarySQIDs(resp.Requests), beforeWindow,
253+
"List should not return requests admitted before the time window")
254+
255+
bySQID := make(map[string]*gatewaypb.RequestSummary, len(resp.Requests))
256+
for _, summary := range resp.Requests {
257+
bySQID[summary.Sqid] = summary
258+
}
259+
260+
firstSummary := bySQID[first]
261+
require.NotNil(t, firstSummary, "List response should include %s", first)
262+
assert.Equal(t, "e2e-list-queue", firstSummary.Queue)
263+
assert.Equal(t, []string{firstURI}, firstSummary.ChangeUris)
264+
assert.NotEmpty(t, firstSummary.Status)
265+
assert.GreaterOrEqual(t, firstSummary.StartedAtMs, startTimeMs)
266+
assert.Less(t, firstSummary.StartedAtMs, endTimeMs)
267+
assert.GreaterOrEqual(t, firstSummary.UpdatedAtMs, firstSummary.StartedAtMs)
268+
269+
s.awaitStatus(first, entity.RequestStatusLanded)
270+
s.awaitStatus(second, entity.RequestStatusLanded)
271+
s.awaitStatus(third, entity.RequestStatusLanded)
272+
273+
landedResp := s.awaitListContains(&gatewaypb.ListRequest{
274+
Queue: "e2e-list-queue",
275+
StartTimeMs: startTimeMs,
276+
EndTimeMs: endTimeMs,
277+
Statuses: []string{string(entity.RequestStatusLanded)},
278+
PageSize: 10,
279+
}, first, second, third)
280+
for _, summary := range landedResp.Requests {
281+
if summary.Sqid != first && summary.Sqid != second && summary.Sqid != third {
282+
continue
283+
}
284+
assert.Equal(t, string(entity.RequestStatusLanded), summary.Status, "landed summary %s should report landed", summary.Sqid)
285+
assert.Greater(t, summary.CompletedAtMs, int64(0), "landed summary %s should have completion time", summary.Sqid)
286+
}
287+
288+
var pagedSQIDs []string
289+
var pageToken string
290+
for {
291+
page := s.list(&gatewaypb.ListRequest{
292+
Queue: "e2e-list-queue",
293+
StartTimeMs: startTimeMs,
294+
EndTimeMs: endTimeMs,
295+
PageSize: 1,
296+
PageToken: pageToken,
297+
Sort: gatewaypb.ListSort_ADMITTED_DESC,
298+
})
299+
require.LessOrEqual(t, len(page.Requests), 1)
300+
if len(page.Requests) == 1 {
301+
pagedSQIDs = append(pagedSQIDs, page.Requests[0].Sqid)
302+
}
303+
if page.NextPageToken == "" {
304+
break
305+
}
306+
pageToken = page.NextPageToken
307+
}
308+
assert.ElementsMatch(t, []string{first, second, third}, pagedSQIDs)
309+
assert.Len(t, mapFromStrings(pagedSQIDs), len(pagedSQIDs), "descending pagination should not return duplicates")
310+
}
311+
312+
// TestList_ShowsCancelIntent verifies the List summary read model observes the
313+
// gateway-synchronous cancelling log written by Cancel. Terminal cancellation is
314+
// intentionally not asserted here because the e2e stack does not yet have a
315+
// deterministic pipeline pause; the terminal outcome can race with landing.
316+
func (s *E2EIntegrationSuite) TestList_ShowsCancelIntent() {
317+
t := s.T()
318+
319+
startTimeMs := time.Now().Add(-time.Second).UnixMilli()
320+
sqid := s.land("e2e-cancel-queue", "github://uber/e2e-list-cancel/pull/301/abcdef0123456789abcdef0123456789abcdef04")
321+
endTimeMs := time.Now().Add(time.Minute).UnixMilli()
322+
323+
_, err := s.gatewayClient.Cancel(s.ctx, &gatewaypb.CancelRequest{Sqid: sqid, Reason: "e2e list cancel test"})
324+
require.NoError(t, err, "Cancel failed")
325+
326+
resp := s.list(&gatewaypb.ListRequest{
327+
Queue: "e2e-cancel-queue",
328+
StartTimeMs: startTimeMs,
329+
EndTimeMs: endTimeMs,
330+
Statuses: []string{string(entity.RequestStatusCancelling)},
331+
PageSize: 10,
332+
})
333+
var summary *gatewaypb.RequestSummary
334+
for _, candidate := range resp.Requests {
335+
if candidate.Sqid == sqid {
336+
summary = candidate
337+
break
338+
}
339+
}
340+
require.NotNil(t, summary, "List should include %s with cancelling status immediately after Cancel", sqid)
341+
assert.Equal(t, string(entity.RequestStatusCancelling), summary.Status)
342+
}
343+
221344
// TestCancelRequest_InvalidSqid verifies the gateway rejects an empty sqid
222345
// synchronously before publishing anything to the cancel queue.
223346
func (s *E2EIntegrationSuite) TestCancelRequest_InvalidSqid() {

0 commit comments

Comments
 (0)