Skip to content

Commit eccb5c9

Browse files
committed
[red] cover production branch pagination and chooser limits
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent ac8dba7 commit eccb5c9

2 files changed

Lines changed: 256 additions & 31 deletions

File tree

‎app/test/unit/github_repository_client_test.dart‎

Lines changed: 154 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const String _commitSha = '1111111111111111111111111111111111111111';
2020
const String _rootTreeSha = '2222222222222222222222222222222222222222';
2121
const String _childTreeSha = '3333333333333333333333333333333333333333';
2222
const String _helloBlobSha = '4444444444444444444444444444444444444444';
23+
const int _repositoryId = 1345960947;
24+
const int _branchPageBodyLimit = 512 * 1024;
25+
const int _branchAggregateBodyLimit = 2 * 1024 * 1024;
2326

2427
RepositoryLocator _locator() =>
2528
RepositoryLocator.parse('https://github.com/PyBLE-dev/examples');
@@ -50,9 +53,26 @@ Map<String, Object?> _branch(String name) => <String, Object?>{
5053
'protected': false,
5154
};
5255

53-
String _nextBranchesLink(int page) =>
54-
'<https://api.github.com/repos/PyBLE-dev/examples/branches'
55-
'?per_page=100&page=$page>; rel="next"';
56+
Map<String, Object?> _repositoryMetadata(String defaultBranch) =>
57+
<String, Object?>{'id': _repositoryId, 'default_branch': defaultBranch};
58+
59+
String _branchesLink(
60+
int page, {
61+
String relation = 'next',
62+
int repositoryId = _repositoryId,
63+
}) =>
64+
'<https://api.github.com/repositories/$repositoryId/branches'
65+
'?per_page=100&page=$page>; rel="$relation"';
66+
67+
Uint8List _paddedJsonBytes(Object? value, int byteLength) {
68+
final List<int> encoded = utf8.encode(jsonEncode(value));
69+
if (encoded.length > byteLength) {
70+
throw StateError('JSON fixture exceeds requested byte length');
71+
}
72+
return Uint8List(byteLength)
73+
..fillRange(0, byteLength, 0x20)
74+
..setRange(0, encoded.length, encoded);
75+
}
5676

5777
List<Object?> _numberedBranches(int start, int count) => <Object?>[
5878
for (int index = start; index < start + count; index += 1)
@@ -137,10 +157,7 @@ void main() {
137157
'PyBLE-dev',
138158
'examples',
139159
]);
140-
return http.Response(
141-
jsonEncode(<String, Object?>{'default_branch': 'main'}),
142-
200,
143-
);
160+
return http.Response(jsonEncode(_repositoryMetadata('main')), 200);
144161
}
145162

146163
expect(request.url.pathSegments, <String>[
@@ -256,7 +273,7 @@ void main() {
256273
]);
257274
expect(request.url.hasQuery, isFalse);
258275
return http.Response(
259-
jsonEncode(<String, Object?>{'default_branch': 'main'}),
276+
jsonEncode(_repositoryMetadata('main')),
260277
200,
261278
);
262279
}
@@ -313,7 +330,7 @@ void main() {
313330
_expectGitHubRequest(request);
314331
if (request.url.pathSegments.last != 'branches') {
315332
return http.Response(
316-
jsonEncode(<String, Object?>{'default_branch': 'main'}),
333+
jsonEncode(_repositoryMetadata('main')),
317334
200,
318335
);
319336
}
@@ -327,9 +344,8 @@ void main() {
327344
200,
328345
headers: <String, String>{
329346
'link':
330-
'${_nextBranchesLink(2)}, '
331-
'<https://api.github.com/repos/PyBLE-dev/examples/'
332-
'branches?per_page=100&page=2>; rel="last"',
347+
'${_branchesLink(2)}, '
348+
'${_branchesLink(2, relation: 'last')}',
333349
},
334350
);
335351
}
@@ -358,19 +374,18 @@ void main() {
358374
test(
359375
'rejects a wrong-host or skipped-page next Link without following it',
360376
() async {
361-
const List<({String label, String link})> cases =
377+
final List<({String label, String link})> cases =
362378
<({String label, String link})>[
363379
(
364380
label: 'wrong host',
365381
link:
366-
'<https://evil.example/repos/PyBLE-dev/examples/branches'
382+
'<https://evil.example/repositories/$_repositoryId/branches'
367383
'?per_page=100&page=2>; rel="next"',
368384
),
385+
(label: 'skipped page', link: _branchesLink(3)),
369386
(
370-
label: 'skipped page',
371-
link:
372-
'<https://api.github.com/repos/PyBLE-dev/examples/branches'
373-
'?per_page=100&page=3>; rel="next"',
387+
label: 'mismatched repository id',
388+
link: _branchesLink(2, repositoryId: _repositoryId + 1),
374389
),
375390
];
376391

@@ -382,7 +397,7 @@ void main() {
382397
_expectGitHubRequest(request);
383398
if (request.url.pathSegments.last != 'branches') {
384399
return http.Response(
385-
jsonEncode(<String, Object?>{'default_branch': 'main'}),
400+
jsonEncode(_repositoryMetadata('main')),
386401
200,
387402
);
388403
}
@@ -404,13 +419,125 @@ void main() {
404419
},
405420
);
406421

422+
test('accepts a branch page body at exactly 512 KiB', () async {
423+
final GithubApi api = GithubRepositoryClient(
424+
httpClient: MockClient((http.Request request) async {
425+
if (request.url.pathSegments.last != 'branches') {
426+
return http.Response(jsonEncode(_repositoryMetadata('main')), 200);
427+
}
428+
return http.Response.bytes(
429+
_paddedJsonBytes(<Object?>[_branch('main')], _branchPageBodyLimit),
430+
200,
431+
);
432+
}),
433+
);
434+
435+
final GithubBranchCatalog catalog = await api.listBranches(_locator());
436+
437+
expect(catalog.branches, <String>['main']);
438+
});
439+
440+
test('rejects a branch page body one byte above 512 KiB', () async {
441+
int requestCount = 0;
442+
final GithubApi api = GithubRepositoryClient(
443+
httpClient: MockClient((http.Request request) async {
444+
requestCount += 1;
445+
if (request.url.pathSegments.last != 'branches') {
446+
return http.Response(jsonEncode(_repositoryMetadata('main')), 200);
447+
}
448+
return http.Response.bytes(
449+
_paddedJsonBytes(<Object?>[
450+
_branch('main'),
451+
], _branchPageBodyLimit + 1),
452+
200,
453+
);
454+
}),
455+
);
456+
457+
await expectLater(
458+
api.listBranches(_locator()),
459+
throwsA(_githubFailure(GithubFailureKind.malformedResponse)),
460+
);
461+
expect(requestCount, 2);
462+
});
463+
464+
test('accepts aggregate branch bodies at exactly 2 MiB', () async {
465+
final List<int> requestedPages = <int>[];
466+
final GithubApi api = GithubRepositoryClient(
467+
httpClient: MockClient((http.Request request) async {
468+
if (request.url.pathSegments.last != 'branches') {
469+
return http.Response(jsonEncode(_repositoryMetadata('main')), 200);
470+
}
471+
472+
final int page = int.parse(request.url.queryParameters['page']!);
473+
requestedPages.add(page);
474+
return http.Response.bytes(
475+
_paddedJsonBytes(<Object?>[
476+
_branch(page == 1 ? 'main' : 'branch-$page'),
477+
], _branchPageBodyLimit),
478+
200,
479+
headers: page < 4
480+
? <String, String>{'link': _branchesLink(page + 1)}
481+
: const <String, String>{},
482+
);
483+
}),
484+
);
485+
486+
final GithubBranchCatalog catalog = await api.listBranches(_locator());
487+
488+
expect(requestedPages, <int>[1, 2, 3, 4]);
489+
expect(catalog.branches, <String>[
490+
'main',
491+
'branch-2',
492+
'branch-3',
493+
'branch-4',
494+
]);
495+
});
496+
497+
test('rejects aggregate branch bodies one byte above 2 MiB', () async {
498+
final List<int> requestedPages = <int>[];
499+
final GithubApi api = GithubRepositoryClient(
500+
httpClient: MockClient((http.Request request) async {
501+
if (request.url.pathSegments.last != 'branches') {
502+
return http.Response(jsonEncode(_repositoryMetadata('main')), 200);
503+
}
504+
505+
final int page = int.parse(request.url.queryParameters['page']!);
506+
requestedPages.add(page);
507+
final int bodyLength = switch (page) {
508+
< 4 => _branchPageBodyLimit,
509+
4 => _branchAggregateBodyLimit - (3 * _branchPageBodyLimit) - 1,
510+
_ => 2,
511+
};
512+
return http.Response.bytes(
513+
_paddedJsonBytes(
514+
page == 5
515+
? const <Object?>[]
516+
: <Object?>[_branch(page == 1 ? 'main' : 'branch-$page')],
517+
bodyLength,
518+
),
519+
200,
520+
headers: page < 5
521+
? <String, String>{'link': _branchesLink(page + 1)}
522+
: const <String, String>{},
523+
);
524+
}),
525+
);
526+
527+
await expectLater(
528+
api.listBranches(_locator()),
529+
throwsA(_githubFailure(GithubFailureKind.malformedResponse)),
530+
);
531+
expect(requestedPages, <int>[1, 2, 3, 4, 5]);
532+
});
533+
407534
test('accepts a complete catalog at the 512-branch boundary', () async {
408535
final List<int> requestedPages = <int>[];
409536
final GithubApi api = GithubRepositoryClient(
410537
httpClient: MockClient((http.Request request) async {
411538
if (request.url.pathSegments.last != 'branches') {
412539
return http.Response(
413-
jsonEncode(<String, Object?>{'default_branch': 'branch-000'}),
540+
jsonEncode(_repositoryMetadata('branch-000')),
414541
200,
415542
);
416543
}
@@ -423,7 +550,7 @@ void main() {
423550
jsonEncode(_numberedBranches(start, count)),
424551
200,
425552
headers: page < 6
426-
? <String, String>{'link': _nextBranchesLink(page + 1)}
553+
? <String, String>{'link': _branchesLink(page + 1)}
427554
: const <String, String>{},
428555
);
429556
}),
@@ -445,7 +572,7 @@ void main() {
445572
httpClient: MockClient((http.Request request) async {
446573
if (request.url.pathSegments.last != 'branches') {
447574
return http.Response(
448-
jsonEncode(<String, Object?>{'default_branch': 'branch-000'}),
575+
jsonEncode(_repositoryMetadata('branch-000')),
449576
200,
450577
);
451578
}
@@ -458,7 +585,7 @@ void main() {
458585
jsonEncode(_numberedBranches(start, count)),
459586
200,
460587
headers: page < 6
461-
? <String, String>{'link': _nextBranchesLink(page + 1)}
588+
? <String, String>{'link': _branchesLink(page + 1)}
462589
: const <String, String>{},
463590
);
464591
}),
@@ -478,7 +605,7 @@ void main() {
478605
httpClient: MockClient((http.Request request) async {
479606
if (request.url.pathSegments.last != 'branches') {
480607
return http.Response(
481-
jsonEncode(<String, Object?>{'default_branch': 'branch-000'}),
608+
jsonEncode(_repositoryMetadata('branch-000')),
482609
200,
483610
);
484611
}
@@ -490,7 +617,7 @@ void main() {
490617
return http.Response(
491618
jsonEncode(_numberedBranches(start, count)),
492619
200,
493-
headers: <String, String>{'link': _nextBranchesLink(page + 1)},
620+
headers: <String, String>{'link': _branchesLink(page + 1)},
494621
);
495622
}),
496623
);
@@ -525,7 +652,7 @@ void main() {
525652
httpClient: MockClient((http.Request request) async {
526653
if (request.url.pathSegments.last != 'branches') {
527654
return http.Response(
528-
jsonEncode(<String, Object?>{'default_branch': 'main'}),
655+
jsonEncode(_repositoryMetadata('main')),
529656
200,
530657
);
531658
}
@@ -548,10 +675,7 @@ void main() {
548675
requests.add(request);
549676
_expectGitHubRequest(request);
550677
if (request.url.pathSegments.last == 'examples') {
551-
return http.Response(
552-
jsonEncode(<String, Object?>{'default_branch': 'main'}),
553-
200,
554-
);
678+
return http.Response(jsonEncode(_repositoryMetadata('main')), 200);
555679
}
556680
if (request.url.pathSegments.last == 'branches') {
557681
return http.Response(

0 commit comments

Comments
 (0)