@@ -20,6 +20,9 @@ const String _commitSha = '1111111111111111111111111111111111111111';
2020const String _rootTreeSha = '2222222222222222222222222222222222222222' ;
2121const String _childTreeSha = '3333333333333333333333333333333333333333' ;
2222const String _helloBlobSha = '4444444444444444444444444444444444444444' ;
23+ const int _repositoryId = 1345960947 ;
24+ const int _branchPageBodyLimit = 512 * 1024 ;
25+ const int _branchAggregateBodyLimit = 2 * 1024 * 1024 ;
2326
2427RepositoryLocator _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
5777List <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