Skip to content

Commit f1a291e

Browse files
committed
Keep batch write tests with orchestration
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d7dc302d-e6f2-41e9-a2c8-ed598de47067
1 parent 40b258a commit f1a291e

1 file changed

Lines changed: 207 additions & 0 deletions

File tree

pkg/github/projects_batch_test.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"math"
89
"net/http"
910
"strings"
@@ -57,6 +58,55 @@ func projectIDMatcher(owner string, projectNumber int, projectNodeID string) git
5758
)
5859
}
5960

61+
// mutationAwareTransport routes GraphQL requests to a fixed query-matcher
62+
// transport (e.g. githubv4mock.NewMockedHTTPClient's Transport) for ordinary
63+
// queries/lookups, and to a sequenced, call-counted responder for mutation
64+
// requests, so end-to-end tests can assert on aliased-mutation call counts and
65+
// per-call variables without needing to hand-construct the exact minified
66+
// mutation query text that reflect.StructOf produces.
67+
type mutationAwareTransport struct {
68+
t *testing.T
69+
queries http.RoundTripper
70+
mutationRespond func(callIndex int, req capturedGraphQLRequest) (status int, body string)
71+
queryCalls []capturedGraphQLRequest
72+
mutationCalls []capturedGraphQLRequest
73+
}
74+
75+
func (m *mutationAwareTransport) RoundTrip(req *http.Request) (*http.Response, error) {
76+
raw, err := io.ReadAll(req.Body)
77+
if err != nil {
78+
return nil, err
79+
}
80+
_ = req.Body.Close()
81+
82+
var parsed struct {
83+
Query string `json:"query"`
84+
Variables map[string]any `json:"variables"`
85+
}
86+
if err := json.Unmarshal(raw, &parsed); err != nil {
87+
return nil, err
88+
}
89+
90+
if !strings.HasPrefix(strings.TrimSpace(parsed.Query), "mutation") {
91+
m.queryCalls = append(m.queryCalls, capturedGraphQLRequest{Query: parsed.Query, Variables: parsed.Variables})
92+
req.Body = io.NopCloser(strings.NewReader(string(raw)))
93+
return m.queries.RoundTrip(req)
94+
}
95+
96+
captured := capturedGraphQLRequest{Query: parsed.Query, Variables: parsed.Variables}
97+
idx := len(m.mutationCalls)
98+
m.mutationCalls = append(m.mutationCalls, captured)
99+
if m.mutationRespond == nil {
100+
m.t.Fatalf("unexpected mutation call #%d (query: %s)", idx, parsed.Query)
101+
}
102+
status, body := m.mutationRespond(idx, captured)
103+
return &http.Response{
104+
StatusCode: status,
105+
Body: io.NopCloser(strings.NewReader(body)),
106+
Header: make(http.Header),
107+
}, nil
108+
}
109+
60110
func Test_UpdateProjectItemsBatch_TopLevelGuards(t *testing.T) {
61111
tooMany := make([]any, maxProjectItemsPerBatch+1)
62112
validItem := map[string]any{"node_id": "PVTI_item1"}
@@ -1076,3 +1126,160 @@ func Test_ResolveItemNodeIDsByNumericID_DeduplicatesOrgAndUserLookups(t *testing
10761126
})
10771127
}
10781128
}
1129+
1130+
func Test_ExecuteBatchWrites_AllAliasGraphQLErrorContinues(t *testing.T) {
1131+
transport := &sequencedGraphQLTransport{
1132+
t: t,
1133+
responses: []func(capturedGraphQLRequest) (int, string){
1134+
func(_ capturedGraphQLRequest) (int, string) {
1135+
return http.StatusOK, mutationErrorResponse(t, map[string]any{}, "all aliases failed")
1136+
},
1137+
func(_ capturedGraphQLRequest) (int, string) {
1138+
return http.StatusOK, mutationDataResponse(t, map[int]struct{ NodeID, FullDatabaseID string }{
1139+
0: {NodeID: "PVTI_item20", FullDatabaseID: "1020"},
1140+
})
1141+
},
1142+
},
1143+
}
1144+
items, results := batchItemsOfSize(21)
1145+
1146+
executeTestBatchWrites(t.Context(), newTestGQLClient(transport), items, results)
1147+
1148+
assert.Len(t, transport.calls, 2)
1149+
for i := range 20 {
1150+
assert.Equal(t, batchItemUnknown, results[i].Status)
1151+
}
1152+
assert.Equal(t, batchItemSucceeded, results[20].Status)
1153+
}
1154+
1155+
func Test_ExecuteBatchWrites_PartialGraphQLErrorPreservesSuccess(t *testing.T) {
1156+
transport := &sequencedGraphQLTransport{
1157+
t: t,
1158+
responses: []func(capturedGraphQLRequest) (int, string){
1159+
func(_ capturedGraphQLRequest) (int, string) {
1160+
return http.StatusOK, mutationErrorResponse(t, map[string]any{
1161+
"item0": map[string]any{
1162+
"projectV2Item": map[string]any{"id": "PVTI_item0", "fullDatabaseId": "1000"},
1163+
},
1164+
"item1": nil,
1165+
}, "item1 failed")
1166+
},
1167+
},
1168+
}
1169+
items, results := batchItemsOfSize(2)
1170+
1171+
executeTestBatchWrites(t.Context(), newTestGQLClient(transport), items, results)
1172+
1173+
assert.Equal(t, batchItemSucceeded, results[0].Status)
1174+
assert.Equal(t, items[0].ref, results[0].Ref)
1175+
assert.Equal(t, batchItemUnknown, results[1].Status)
1176+
assert.Equal(t, items[1].ref, results[1].Ref)
1177+
}
1178+
1179+
func Test_ExecuteBatchWrites_AmbiguousSuccessResponseAborts(t *testing.T) {
1180+
tests := []struct {
1181+
name string
1182+
body string
1183+
confirmedSuccesses int
1184+
}{
1185+
{
1186+
name: "null data",
1187+
body: `{"data":null}`,
1188+
},
1189+
{
1190+
name: "missing data",
1191+
body: `{}`,
1192+
},
1193+
{
1194+
name: "partial data without errors",
1195+
body: mutationDataResponse(t, map[int]struct{ NodeID, FullDatabaseID string }{
1196+
0: {NodeID: "PVTI_item0", FullDatabaseID: "1000"},
1197+
}),
1198+
confirmedSuccesses: 1,
1199+
},
1200+
}
1201+
1202+
for _, tt := range tests {
1203+
t.Run(tt.name, func(t *testing.T) {
1204+
transport := &sequencedGraphQLTransport{
1205+
t: t,
1206+
responses: []func(capturedGraphQLRequest) (int, string){
1207+
func(_ capturedGraphQLRequest) (int, string) {
1208+
return http.StatusOK, tt.body
1209+
},
1210+
func(_ capturedGraphQLRequest) (int, string) {
1211+
return http.StatusOK, mutationDataResponse(t, map[int]struct{ NodeID, FullDatabaseID string }{
1212+
0: {NodeID: "PVTI_item20", FullDatabaseID: "1020"},
1213+
})
1214+
},
1215+
},
1216+
}
1217+
items, results := batchItemsOfSize(21)
1218+
1219+
executeTestBatchWrites(t.Context(), newTestGQLClient(transport), items, results)
1220+
1221+
assert.Len(t, transport.calls, 1)
1222+
for i, result := range results {
1223+
if i < tt.confirmedSuccesses {
1224+
assert.Equal(t, batchItemSucceeded, result.Status)
1225+
continue
1226+
}
1227+
assert.Equal(t, batchItemUnknown, result.Status)
1228+
}
1229+
})
1230+
}
1231+
}
1232+
1233+
func Test_ExecuteBatchWrites_TransportTimeoutAborts(t *testing.T) {
1234+
transport := &errorGraphQLTransport{err: context.DeadlineExceeded}
1235+
items, results := batchItemsOfSize(21)
1236+
1237+
executeTestBatchWrites(t.Context(), newTestGQLClient(transport), items, results)
1238+
1239+
assert.Equal(t, 1, transport.calls)
1240+
for _, result := range results {
1241+
assert.Equal(t, batchItemUnknown, result.Status)
1242+
}
1243+
}
1244+
1245+
func Test_ExecuteBatchWrites_CanceledContextSkipsWrites(t *testing.T) {
1246+
ctx, cancel := context.WithCancel(t.Context())
1247+
cancel()
1248+
transport := &sequencedGraphQLTransport{t: t}
1249+
items, results := batchItemsOfSize(21)
1250+
1251+
executeTestBatchWrites(ctx, newTestGQLClient(transport), items, results)
1252+
1253+
assert.Empty(t, transport.calls)
1254+
for _, result := range results {
1255+
assert.Equal(t, batchItemUnknown, result.Status)
1256+
}
1257+
}
1258+
1259+
func executeTestBatchWrites(ctx context.Context, gqlClient *githubv4.Client, items []resolvedBatchItem, results []batchItemResult) {
1260+
executeBatchWrites(
1261+
ctx,
1262+
batchWriteOperation{
1263+
gqlClient: gqlClient,
1264+
kind: batchMutationUpdate,
1265+
projectID: githubv4.ID("PVT_project"),
1266+
fieldID: githubv4.ID("PVTF_field"),
1267+
value: githubv4.ProjectV2FieldValue{Text: githubv4.NewString("value")},
1268+
},
1269+
items,
1270+
results,
1271+
)
1272+
}
1273+
1274+
func batchItemsOfSize(n int) ([]resolvedBatchItem, []batchItemResult) {
1275+
items := make([]resolvedBatchItem, n)
1276+
for i := range n {
1277+
nodeID := fmt.Sprintf("PVTI_item%d", i)
1278+
items[i] = resolvedBatchItem{
1279+
index: i,
1280+
ref: map[string]any{"node_id": nodeID},
1281+
nodeID: nodeID,
1282+
}
1283+
}
1284+
return items, make([]batchItemResult, n)
1285+
}

0 commit comments

Comments
 (0)