blob: 13406b8c73aa29bb45be775006f17b82039b5560 [file] [log] [blame]
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:typed_data';
import 'package:cocoon_service/cocoon_service.dart';
import 'package:cocoon_service/src/model/appengine/service_account_info.dart';
import 'package:cocoon_service/src/model/luci/buildbucket.dart';
import 'package:cocoon_service/src/request_handling/exceptions.dart';
import 'package:cocoon_service/src/service/buildbucket.dart';
import 'package:cocoon_service/src/service/github_status_service.dart';
import 'package:cocoon_service/src/service/luci_build_service.dart';
import 'package:crypto/crypto.dart';
import 'package:github/github.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import '../model/github/checks_test_data.dart';
import '../src/datastore/fake_cocoon_config.dart';
import '../src/request_handling/fake_http.dart';
import '../src/request_handling/request_handler_tester.dart';
import '../src/utilities/mocks.dart';
void main() {
group('githubWebhookPullRequest', () {
GithubWebhook webhook;
FakeHttpRequest request;
FakeConfig config;
MockGitHub gitHubClient;
MockIssuesService issuesService;
MockPullRequestsService pullRequestsService;
MockBuildBucketClient mockBuildBucketClient;
RequestHandlerTester tester;
MockHttpClient mockHttpClient;
const String serviceAccountEmail = 'test@test';
LuciBuildService luciBuildService;
ServiceAccountInfo serviceAccountInfo;
GithubStatusService githubStatusService;
MockGithubChecksService mockGithubChecksService;
const String keyString = 'not_a_real_key';
const String cqLabelName = 'CQ+1';
String getHmac(Uint8List list, Uint8List key) {
final Hmac hmac = Hmac(sha1, key);
return hmac.convert(list).toString();
}
setUp(() async {
serviceAccountInfo = const ServiceAccountInfo(email: serviceAccountEmail);
request = FakeHttpRequest();
config = FakeConfig(deviceLabServiceAccountValue: serviceAccountInfo);
gitHubClient = MockGitHub();
mockHttpClient = MockHttpClient();
issuesService = MockIssuesService();
pullRequestsService = MockPullRequestsService();
mockBuildBucketClient = MockBuildBucketClient();
tester = RequestHandlerTester(request: request);
serviceAccountInfo = await config.deviceLabServiceAccount;
/// LUCI service class to communicate with buildBucket service.
luciBuildService = LuciBuildService(
config,
mockBuildBucketClient,
serviceAccountInfo,
);
githubStatusService = GithubStatusService(
config,
luciBuildService,
);
mockGithubChecksService = MockGithubChecksService();
webhook = GithubWebhook(config, mockBuildBucketClient, luciBuildService,
githubStatusService, mockGithubChecksService,
skiaClient: mockHttpClient);
when(gitHubClient.issues).thenReturn(issuesService);
when(gitHubClient.pullRequests).thenReturn(pullRequestsService);
config.nonMasterPullRequestMessageValue = 'nonMasterPullRequestMessage';
config.missingTestsPullRequestMessageValue =
'missingTestPullRequestMessage';
config.goldenBreakingChangeMessageValue = 'goldenBreakingChangeMessage';
config.goldenTriageMessageValue = 'goldenTriageMessage';
config.githubOAuthTokenValue = 'githubOAuthKey';
config.webhookKeyValue = keyString;
config.githubClient = gitHubClient;
config.deviceLabServiceAccountValue =
const ServiceAccountInfo(email: serviceAccountEmail);
config.luciTryBuildersValue = (json.decode('''[
{"name": "Cocoon", "repo": "cocoon"},
{"name": "Linux", "repo": "flutter", "taskName": "linux_bot"},
{"name": "Mac", "repo": "flutter", "taskName": "mac_bot"},
{"name": "Windows", "repo": "flutter", "taskName": "windows_bot"},
{"name": "Linux Coverage", "repo": "flutter"},
{"name": "Linux Host Engine", "repo": "engine"},
{"name": "Linux Android AOT Engine", "repo": "engine"},
{"name": "Linux Android Debug Engine", "repo": "engine"},
{"name": "Mac Host Engine", "repo": "engine"},
{"name": "Mac Android AOT Engine", "repo": "engine"},
{"name": "Mac Android Debug Engine", "repo": "engine"},
{"name": "Mac iOS Engine", "repo": "engine"},
{"name": "Windows Host Engine", "repo": "engine"},
{"name": "Windows Android AOT Engine", "repo": "engine"}
]''') as List<dynamic>).cast<Map<String, dynamic>>();
config.cqLabelNameValue = cqLabelName;
});
test('Rejects non-POST methods with methodNotAllowed', () async {
expect(tester.get(webhook), throwsA(isA<MethodNotAllowed>()));
});
test('Rejects missing headers', () async {
expect(tester.post(webhook), throwsA(isA<BadRequestException>()));
});
test('Rejects invalid hmac', () async {
request.headers.set('X-GitHub-Event', 'pull_request');
request.headers.set('X-Hub-Signature', 'bar');
request.body = 'Hello, World!';
expect(tester.post(webhook), throwsA(isA<Forbidden>()));
});
test('Rejects malformed unicode', () async {
request.headers.set('X-GitHub-Event', 'pull_request');
request.bodyBytes = Uint8List.fromList(<int>[0xc3, 0x28]);
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(request.bodyBytes, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
expect(tester.post(webhook), throwsA(isA<BadRequestException>()));
});
test('Rejects non-json', () async {
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = 'Hello, World!';
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
expect(tester.post(webhook), throwsA(isA<BadRequestException>()));
});
test('Acts on opened against dev', () async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate('opened', issueNumber, 'dev');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
when(pullRequestsService.listFiles(slug, issueNumber)).thenAnswer(
(_) => Stream<PullRequestFile>.value(
PullRequestFile()..filename = 'packages/flutter/blah.dart',
),
);
when(issuesService.listCommentsByIssue(slug, issueNumber)).thenAnswer(
(_) => Stream<IssueComment>.value(
IssueComment()..body = 'some other comment',
),
);
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(skiaIgnoreTemplate()) as Uint8List);
when(mockHttpClient
.getUrl(Uri.parse('https://flutter-gold.skia.org/json/ignores')))
.thenAnswer(
(_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await tester.post(webhook);
verify(pullRequestsService.edit(
slug,
issueNumber,
base: 'master',
)).called(1);
verify(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.nonMasterPullRequestMessageValue)),
)).called(1);
});
test('Framework labels PRs, comment if no tests', () async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate('opened', issueNumber, 'master');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
when(pullRequestsService.listFiles(slug, issueNumber)).thenAnswer(
(_) => Stream<PullRequestFile>.value(
PullRequestFile()..filename = 'packages/flutter/blah.dart',
),
);
when(issuesService.listCommentsByIssue(slug, issueNumber)).thenAnswer(
(_) => Stream<IssueComment>.value(
IssueComment()..body = 'some other comment',
),
);
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(skiaIgnoreTemplate()) as Uint8List);
when(mockHttpClient
.getUrl(Uri.parse('https://flutter-gold.skia.org/json/ignores')))
.thenAnswer(
(_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await tester.post(webhook);
verify(issuesService.addLabelsToIssue(
slug,
issueNumber,
<String>['framework'],
)).called(1);
verify(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.missingTestsPullRequestMessageValue)),
)).called(1);
});
test('Framework labels PRs, no dart files', () async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate('opened', issueNumber, 'master');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
when(pullRequestsService.listFiles(slug, issueNumber)).thenAnswer(
(_) => Stream<PullRequestFile>.value(
PullRequestFile()..filename = 'packages/flutter/blah.md',
),
);
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(skiaIgnoreTemplate()) as Uint8List);
when(mockHttpClient
.getUrl(Uri.parse('https://flutter-gold.skia.org/json/ignores')))
.thenAnswer(
(_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await tester.post(webhook);
verify(issuesService.addLabelsToIssue(
slug,
issueNumber,
<String>['framework'],
)).called(1);
verifyNever(issuesService.createComment(
slug,
issueNumber,
any,
));
});
test('Framework labels PRs, no comment if tests', () async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate('opened', issueNumber, 'master');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
when(pullRequestsService.listFiles(slug, issueNumber)).thenAnswer(
(_) => Stream<PullRequestFile>.fromIterable(<PullRequestFile>[
PullRequestFile()..filename = 'packages/flutter/semantics_test.dart',
PullRequestFile()..filename = 'packages/flutter_tools/blah.dart',
PullRequestFile()..filename = 'packages/flutter_driver/blah.dart',
PullRequestFile()..filename = 'examples/flutter_gallery/blah.dart',
PullRequestFile()..filename = 'dev/blah.dart',
PullRequestFile()..filename = 'bin/internal/engine.version',
PullRequestFile()
..filename = 'packages/flutter/lib/src/cupertino/blah.dart',
PullRequestFile()
..filename = 'packages/flutter/lib/src/material/blah.dart',
PullRequestFile()
..filename = 'packages/flutter_localizations/blah.dart',
]),
);
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(skiaIgnoreTemplate()) as Uint8List);
when(mockHttpClient
.getUrl(Uri.parse('https://flutter-gold.skia.org/json/ignores')))
.thenAnswer(
(_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await tester.post(webhook);
verify(issuesService.addLabelsToIssue(
slug,
issueNumber,
<String>[
'framework',
'a: accessibility',
'tool',
'a: tests',
'd: examples',
'team',
'team: gallery',
'engine',
'f: cupertino',
'f: material design',
'a: internationalization',
],
)).called(1);
verifyNever(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.missingTestsPullRequestMessageValue)),
));
});
test('Engine labels PRs, comment if no tests', () async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate(
'opened',
issueNumber,
'master',
repoName: 'engine',
repoFullName: 'flutter/engine',
);
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'engine');
when(pullRequestsService.listFiles(slug, issueNumber)).thenAnswer(
(_) => Stream<PullRequestFile>.value(
PullRequestFile()
..filename = 'shell/platform/darwin/ios/framework/Source/boost.mm',
),
);
when(issuesService.listCommentsByIssue(slug, issueNumber)).thenAnswer(
(_) => Stream<IssueComment>.value(
IssueComment()..body = 'some other comment',
),
);
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[],
);
});
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(skiaIgnoreTemplate()) as Uint8List);
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await tester.post(webhook);
verify(issuesService.addLabelsToIssue(
slug,
issueNumber,
<String>['platform-ios'],
)).called(1);
verify(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.missingTestsPullRequestMessageValue)),
)).called(1);
});
test('Engine labels PRs, no code files', () async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate(
'opened',
issueNumber,
'master',
repoName: 'engine',
repoFullName: 'flutter/engine',
);
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'engine');
when(pullRequestsService.listFiles(slug, issueNumber)).thenAnswer(
(_) => Stream<PullRequestFile>.value(
PullRequestFile()..filename = 'DEPS',
),
);
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[],
);
});
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(skiaIgnoreTemplate()) as Uint8List);
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await tester.post(webhook);
verifyNever(issuesService.addLabelsToIssue(
slug,
issueNumber,
any,
));
verifyNever(issuesService.createComment(
slug,
issueNumber,
any,
));
});
test('Engine labels PRs, no comment if Java tests', () async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate(
'opened',
issueNumber,
'master',
repoName: 'engine',
repoFullName: 'flutter/engine',
);
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'engine');
when(pullRequestsService.listFiles(slug, issueNumber)).thenAnswer(
(_) => Stream<PullRequestFile>.fromIterable(<PullRequestFile>[
PullRequestFile()
..filename = 'shell/platform/android/io/flutter/Blah.java',
PullRequestFile()
..filename = 'shell/platform/android/test/io/flutter/BlahTest.java',
]),
);
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[],
);
});
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(skiaIgnoreTemplate()) as Uint8List);
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await tester.post(webhook);
verify(issuesService.addLabelsToIssue(
slug,
issueNumber,
<String>[
'platform-android',
],
)).called(1);
verifyNever(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.missingTestsPullRequestMessageValue)),
));
});
test('Engine labels PRs, no comment if cc tests', () async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate(
'opened',
issueNumber,
'master',
repoName: 'engine',
repoFullName: 'flutter/engine',
);
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'engine');
when(pullRequestsService.listFiles(slug, issueNumber)).thenAnswer(
(_) => Stream<PullRequestFile>.fromIterable(<PullRequestFile>[
PullRequestFile()..filename = 'fml/blah.cc',
PullRequestFile()..filename = 'fml/blah_unittests.cc',
]),
);
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[],
);
});
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(skiaIgnoreTemplate()) as Uint8List);
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await tester.post(webhook);
verifyNever(issuesService.addLabelsToIssue(
slug,
issueNumber,
any,
));
verifyNever(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.missingTestsPullRequestMessageValue)),
));
});
test('No labels when only pubspec.yaml changes', () async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate('opened', issueNumber, 'master');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
when(pullRequestsService.listFiles(slug, issueNumber)).thenAnswer(
(_) => Stream<PullRequestFile>.fromIterable(<PullRequestFile>[
PullRequestFile()..filename = 'packages/flutter/pubspec.yaml',
PullRequestFile()..filename = 'packages/flutter_tools/pubspec.yaml',
]),
);
await tester.post(webhook);
verify(issuesService.addLabelsToIssue(
slug,
issueNumber,
<String>['team'],
)).called(1);
verifyNever(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.missingTestsPullRequestMessageValue)),
));
});
test('Labels Golden changes based on Skia Gold ignore, comments to notify',
() async {
const int issueNumber = 1234;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate('opened', issueNumber, 'master');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
when(pullRequestsService.listFiles(slug, issueNumber))
.thenAnswer((_) => Stream<PullRequestFile>.value(
PullRequestFile()..filename = 'some_change.dart',
));
when(issuesService.listCommentsByIssue(slug, issueNumber)).thenAnswer(
(_) => Stream<IssueComment>.value(
IssueComment()..body = 'some other comment',
),
);
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(
skiaIgnoreTemplate(pullRequestNumber: issueNumber.toString()))
as Uint8List);
when(mockHttpClient
.getUrl(Uri.parse('https://flutter-gold.skia.org/json/ignores')))
.thenAnswer(
(_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await tester.post(webhook);
verify(issuesService.addLabelsToIssue(
slug,
issueNumber,
<String>[
'will affect goldens',
'severe: API break',
'a: tests',
],
)).called(1);
verify(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.goldenBreakingChangeMessageValue)),
)).called(1);
});
test('Golden triage comment when closed && merged from ignores', () async {
const int issueNumber = 1234;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate(
'closed',
issueNumber,
'master',
merged: true,
);
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
when(pullRequestsService.listFiles(slug, issueNumber))
.thenAnswer((_) => Stream<PullRequestFile>.value(
PullRequestFile()..filename = 'some_change.dart',
));
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(
skiaIgnoreTemplate(pullRequestNumber: issueNumber.toString()))
as Uint8List);
when(mockHttpClient
.getUrl(Uri.parse('https://flutter-gold.skia.org/json/ignores')))
.thenAnswer(
(_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await tester.post(webhook);
verify(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.goldenTriageMessageValue)),
)).called(1);
});
test('No golden triage comment when closed && !merged from labels',
() async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate('closed', issueNumber, 'master');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
when(issuesService.listLabelsByIssue(any, issueNumber)).thenAnswer((_) {
return Stream<IssueLabel>.fromIterable(<IssueLabel>[
IssueLabel()..name = 'will affect goldens',
]);
});
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[],
),
),
],
);
});
await tester.post(webhook);
verifyNever(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.goldenTriageMessageValue)),
));
});
test('No golden triage comment when closed && !merged from ignores',
() async {
const int issueNumber = 1234;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate('closed', issueNumber, 'master');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
when(pullRequestsService.listFiles(slug, issueNumber))
.thenAnswer((_) => Stream<PullRequestFile>.value(
PullRequestFile()..filename = 'some_change.dart',
));
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(
skiaIgnoreTemplate(pullRequestNumber: issueNumber.toString()))
as Uint8List);
when(mockHttpClient
.getUrl(Uri.parse('https://flutter-gold.skia.org/json/ignores')))
.thenAnswer(
(_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[],
),
),
],
);
});
await tester.post(webhook);
verifyNever(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.goldenTriageMessageValue)),
));
});
test('Cancels builds when pull request is closed without merging',
() async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate('closed', issueNumber, 'master');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
when(pullRequestsService.listFiles(slug, issueNumber))
.thenAnswer((_) => Stream<PullRequestFile>.value(
PullRequestFile()..filename = 'some_change.dart',
));
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(
skiaIgnoreTemplate(pullRequestNumber: issueNumber.toString()))
as Uint8List);
when(mockHttpClient
.getUrl(Uri.parse('https://flutter-gold.skia.org/json/ignores')))
.thenAnswer(
(_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[
Build(
id: 999,
builderId: BuilderId(
project: 'flutter',
bucket: 'prod',
builder: 'Linux',
),
status: Status.started,
),
],
),
),
],
);
});
await tester.post(webhook);
expect(
json.encode(verify(mockBuildBucketClient.batch(captureAny)).captured),
'[{"requests":[{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"createdBy":"test@test","tags":[{"key":"buildset","value":"pr/git/123"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"},{"key":"user_agent","value":"flutter-cocoon"}]}}},'
'{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"user_agent","value":"recipe"}]}}}]},{"requests":[{"cancelBuild":{"id":"999","summaryMarkdown":"Pull request closed"}}]}]',
);
});
test('Labels draft issues as work in progress, does not test pest.',
() async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate(
'opened',
issueNumber,
'master',
isDraft: true,
);
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
when(pullRequestsService.listFiles(slug, issueNumber))
.thenAnswer((_) => Stream<PullRequestFile>.value(
PullRequestFile()..filename = 'some_change.dart',
));
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(skiaIgnoreTemplate()) as Uint8List);
when(mockHttpClient
.getUrl(Uri.parse('https://flutter-gold.skia.org/json/ignores')))
.thenAnswer(
(_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await tester.post(webhook);
verify(issuesService.addLabelsToIssue(
slug,
issueNumber,
<String>['work in progress; do not review'],
)).called(1);
verifyNever(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.missingTestsPullRequestMessageValue)),
));
});
test('Will not spawn comments if they have already been made.', () async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate('opened', issueNumber, 'master');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
when(pullRequestsService.listFiles(slug, issueNumber)).thenAnswer(
(_) => Stream<PullRequestFile>.value(
PullRequestFile()..filename = 'packages/flutter/blah.dart',
),
);
when(issuesService.listCommentsByIssue(slug, issueNumber)).thenAnswer(
(_) => Stream<IssueComment>.value(
IssueComment()..body = config.missingTestsPullRequestMessageValue,
),
);
final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest();
final MockHttpClientResponse mockHttpResponse = MockHttpClientResponse(
utf8.encode(skiaIgnoreTemplate()) as Uint8List);
when(mockHttpClient
.getUrl(Uri.parse('https://flutter-gold.skia.org/json/ignores')))
.thenAnswer(
(_) => Future<MockHttpClientRequest>.value(mockHttpRequest));
when(mockHttpRequest.close()).thenAnswer(
(_) => Future<MockHttpClientResponse>.value(mockHttpResponse));
await tester.post(webhook);
verify(issuesService.addLabelsToIssue(
slug,
issueNumber,
<String>['framework'],
)).called(1);
verifyNever(issuesService.createComment(
slug,
issueNumber,
argThat(contains(config.missingTestsPullRequestMessageValue)),
));
});
test('Skips labeling or commenting on autorolls', () async {
const int issueNumber = 123;
request.headers.set('X-GitHub-Event', 'pull_request');
request.body = jsonTemplate(
'opened',
issueNumber,
'master',
login: 'engine-flutter-autoroll',
);
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
await tester.post(webhook);
final RepositorySlug slug = RepositorySlug('flutter', 'flutter');
verifyNever(gitHubClient.postJSON<List<dynamic>, List<IssueLabel>>(
'/repos/${slug.fullName}/issues/$issueNumber/labels',
body: anyNamed('body'),
convert: anyNamed('convert'),
));
verifyNever(issuesService.createComment(
slug,
issueNumber,
any,
));
});
group('BuildBucket', () {
const int issueNumber = 123;
setUp(() {
request.headers.set('X-GitHub-Event', 'pull_request');
});
test('Exception is raised when no builders available', () async {
config.luciTryBuildersValue = <Map<String, dynamic>>[];
when(issuesService.listLabelsByIssue(any, issueNumber)).thenAnswer((_) {
return Stream<IssueLabel>.fromIterable(<IssueLabel>[
IssueLabel()..name = 'Random Label',
]);
});
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[],
),
),
],
);
});
request.body = jsonTemplate('synchronize', issueNumber, 'master',
repoFullName: 'flutter/cocoon', repoName: 'cocoon');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
expect(tester.post(webhook), throwsA(isA<InternalServerError>()));
});
test('Optional repo - Not schedule build when labeled without CQ',
() async {
when(issuesService.listLabelsByIssue(any, issueNumber)).thenAnswer((_) {
return Stream<IssueLabel>.fromIterable(<IssueLabel>[
IssueLabel()..name = 'Random Label',
]);
});
request.body = jsonTemplate('labeled', issueNumber, 'master');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
await tester.post(webhook);
verifyNever(mockBuildBucketClient.searchBuilds(any));
verifyNever(mockBuildBucketClient.scheduleBuild(any));
verifyNever(mockBuildBucketClient.batch(any));
});
Future<void> _testActions(String action, {bool never = false}) async {
when(issuesService.listLabelsByIssue(any, issueNumber)).thenAnswer((_) {
return Stream<IssueLabel>.fromIterable(<IssueLabel>[
IssueLabel()..name = 'Random Label',
]);
});
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[],
),
),
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[],
),
),
],
);
});
request.body = '''
{
"action": "$action",
"number": 583,
"draft": false,
"pull_request": {
"id": 354272971,
"number": 583,
"labels": [],
"base": {
"sha": "the_base_sha",
"repo": {
"name": "cocoon",
"full_name": "flutter/cocoon"
}
},
"head": {
"sha": "the_head_sha",
"repo": {
"name": "cocoon",
"full_name": "flutter/cocoon",
"owner": {
"login": "flutter"
}
}
}
},
"repository": {
"id": 1868532,
"node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
"name": "cocoon",
"full_name": "flutter/cocoon",
"private": false,
"owner": {
"login": "flutter"
}
}
}
''';
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
config.luciTryBuildersValue = (json.decode(
'[{"name": "Cocoon", "repo": "cocoon"},{"name": "Linux", "repo": "flutter"}, {"name": "Mac", "repo": "flutter"}]')
as List<dynamic>)
.cast<Map<String, dynamic>>();
await tester.post(webhook);
if (never) {
verifyNever(mockBuildBucketClient.batch(captureAny));
return;
}
const String expectedJson = '''
[
{"requests": [
{"searchBuilds":{
"predicate":{
"builder":{
"project":"flutter",
"bucket":"try"
},
"createdBy":"test@test",
"tags":[
{
"key":"buildset",
"value":"pr/git/583"
},
{
"key":"github_link",
"value":"https://github.com/flutter/cocoon/pull/583"
},
{
"key": "user_agent",
"value":"flutter-cocoon"
}
]
}
}
},
{
"searchBuilds":{
"predicate":{
"builder":{
"project":"flutter",
"bucket":"try"
},
"tags":[
{
"key":"buildset",
"value":"pr/git/583"
},
{
"key":"user_agent",
"value":"recipe"
}
]
}
}
}]
},
{
"requests":[
{
"searchBuilds":{
"predicate":{
"builder":{
"project":"flutter",
"bucket":"try"
},
"createdBy":"test@test",
"tags":[
{
"key":"buildset",
"value":"pr/git/583"
},
{
"key":"github_link",
"value":"https://github.com/flutter/cocoon/pull/583"
},
{
"key":"user_agent",
"value":"flutter-cocoon"
}
]
}
}
},
{
"searchBuilds":{
"predicate":{
"builder":{
"project":"flutter",
"bucket":"try"
},
"tags":[
{
"key":"buildset",
"value":"pr/git/583"
},
{
"key":"user_agent",
"value":"recipe"
}
]
}
}
}
]
},
{
"requests":[
{
"scheduleBuild":{
"builder":{
"project":"flutter",
"bucket":"try",
"builder":"Cocoon"
},
"properties":{
"git_url":"https://github.com/flutter/cocoon",
"git_ref":"refs/pull/583/head"
},
"tags":[
{
"key":"buildset",
"value":"pr/git/583"
},
{
"key":"buildset",
"value":"sha/git/the_head_sha"
},
{
"key":"user_agent",
"value":"flutter-cocoon"
},
{
"key":"github_link",
"value":"https://github.com/flutter/cocoon/pull/583"
}
],
"notify":{
"pubsubTopic":"projects/flutter-dashboard/topics/luci-builds",
"userData":"eyJyZXBvX293bmVyIjoiZmx1dHRlciIsInJlcG9fbmFtZSI6ImNvY29vbiIsInVzZXJfYWdlbnQiOiJmbHV0dGVyLWNvY29vbiJ9"
}
}
}
]
},
{
"requests":[
{
"searchBuilds":{
"predicate":{
"builder":{
"project":"flutter",
"bucket":"try"
},
"createdBy":"test@test",
"tags":[
{
"key":"buildset",
"value":"pr/git/583"
},
{
"key":"github_link",
"value":"https://github.com/flutter/cocoon/pull/583"
},
{
"key":"user_agent",
"value":"flutter-cocoon"
}
]
}
}
},
{
"searchBuilds":{
"predicate":{
"builder":{
"project":"flutter",
"bucket":"try"
},
"tags":[
{
"key":"buildset",
"value":"pr/git/583"
},
{
"key":"user_agent",
"value":"recipe"
}
]
}
}
}
]
}]''';
expect(
json.encode(
verify(mockBuildBucketClient.batch(captureAny)).captured),
expectedJson.replaceAll(RegExp(r'\s|\n'), ''));
}
test('Edited Action works properly', () async {
await _testActions('edited', never: true);
});
test('Opened Action works properly', () async {
await _testActions('opened');
});
test('Ready_for_review Action works properly', () async {
await _testActions('ready_for_review');
});
test('Reopened Action works properly', () async {
await _testActions('reopened');
});
test('Labeled Action works properly', () async {
await _testActions('labeled', never: true);
});
test('Synchronize Action works properly', () async {
await _testActions('synchronize');
});
test('Schedules builds when labeled', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[],
),
),
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[],
),
),
],
);
});
request.body = jsonTemplate('labeled', issueNumber, 'master',
includeCqLabel: true);
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
await tester.post(webhook);
expect(
json.encode(verify(mockBuildBucketClient.batch(captureAny)).captured),
'[{"requests":[{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"createdBy":"test@test","tags":[{"key":"buildset","value":"pr/git/123"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"},{"key":"user_agent","value":"flutter-cocoon"}]}}},{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"user_agent","value":"recipe"}]}}}]},{"requests":[{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"createdBy":"test@test","tags":[{"key":"buildset","value":"pr/git/123"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"},{"key":"user_agent","value":"flutter-cocoon"}]}}},{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"user_agent","value":"recipe"}]}}}]},{"requests":[{"scheduleBuild":{"builder":{"project":"flutter","bucket":"try","builder":"Linux"},"properties":{"git_url":"https://github.com/flutter/flutter","git_ref":"refs/pull/123/head"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"buildset","value":"sha/git/be6ff099a4ee56e152a5fa2f37edd10f79d1269a"},{"key":"user_agent","value":"flutter-cocoon"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"}],"notify":{"pubsubTopic":"projects/flutter-dashboard/topics/luci-builds","userData":"eyJyZXBvX293bmVyIjoiZmx1dHRlciIsInJlcG9fbmFtZSI6ImZsdXR0ZXIiLCJ1c2VyX2FnZW50IjoiZmx1dHRlci1jb2Nvb24ifQ=="}}},{"scheduleBuild":{"builder":{"project":"flutter","bucket":"try","builder":"Mac"},"properties":{"git_url":"https://github.com/flutter/flutter","git_ref":"refs/pull/123/head"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"buildset","value":"sha/git/be6ff099a4ee56e152a5fa2f37edd10f79d1269a"},{"key":"user_agent","value":"flutter-cocoon"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"}],"notify":{"pubsubTopic":"projects/flutter-dashboard/topics/luci-builds","userData":"eyJyZXBvX293bmVyIjoiZmx1dHRlciIsInJlcG9fbmFtZSI6ImZsdXR0ZXIiLCJ1c2VyX2FnZW50IjoiZmx1dHRlci1jb2Nvb24ifQ=="}}},{"scheduleBuild":{"builder":{"project":"flutter","bucket":"try","builder":"Windows"},"properties":{"git_url":"https://github.com/flutter/flutter","git_ref":"refs/pull/123/head"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"buildset","value":"sha/git/be6ff099a4ee56e152a5fa2f37edd10f79d1269a"},{"key":"user_agent","value":"flutter-cocoon"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"}],"notify":{"pubsubTopic":"projects/flutter-dashboard/topics/luci-builds","userData":"eyJyZXBvX293bmVyIjoiZmx1dHRlciIsInJlcG9fbmFtZSI6ImZsdXR0ZXIiLCJ1c2VyX2FnZW50IjoiZmx1dHRlci1jb2Nvb24ifQ=="}}},{"scheduleBuild":{"builder":{"project":"flutter","bucket":"try","builder":"Linux Coverage"},"properties":{"git_url":"https://github.com/flutter/flutter","git_ref":"refs/pull/123/head"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"buildset","value":"sha/git/be6ff099a4ee56e152a5fa2f37edd10f79d1269a"},{"key":"user_agent","value":"flutter-cocoon"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"}],"notify":{"pubsubTopic":"projects/flutter-dashboard/topics/luci-builds","userData":"eyJyZXBvX293bmVyIjoiZmx1dHRlciIsInJlcG9fbmFtZSI6ImZsdXR0ZXIiLCJ1c2VyX2FnZW50IjoiZmx1dHRlci1jb2Nvb24ifQ=="}}}]},{"requests":[{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"createdBy":"test@test","tags":[{"key":"buildset","value":"pr/git/123"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"},{"key":"user_agent","value":"flutter-cocoon"}]}}},{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"user_agent","value":"recipe"}]}}}]}]',
);
});
test('Cancels builds when unlabeled', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[
Build(
id: 999,
builderId: BuilderId(
project: 'flutter',
bucket: 'prod',
builder: 'Linux',
),
status: Status.started,
)
],
),
),
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[
Build(
id: 998,
builderId: BuilderId(
project: 'flutter',
bucket: 'prod',
builder: 'Linux Host',
),
status: Status.started,
)
],
),
),
],
);
});
request.body = jsonTemplate('unlabeled', issueNumber, 'master');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
await tester.post(webhook);
expect(
json.encode(verify(mockBuildBucketClient.batch(captureAny)).captured),
'[{"requests":[{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"createdBy":"test@test","tags":[{"key":"buildset","value":"pr/git/123"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"},{"key":"user_agent","value":"flutter-cocoon"}]}}},'
'{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"user_agent","value":"recipe"}]}}}]},{"requests":[{"cancelBuild":{"id":"999","summaryMarkdown":"Tryjobs canceled (label removed)"}},{"cancelBuild":{"id":"998","summaryMarkdown":"Tryjobs canceled (label removed)"}}]}]',
);
});
test('Skips cancel build when unlabeled and build is ended', () async {
when(issuesService.listLabelsByIssue(any, issueNumber)).thenAnswer((_) {
return Stream<IssueLabel>.fromIterable(<IssueLabel>[
IssueLabel()..name = 'Random Label',
]);
});
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[
Build(
id: 999,
builderId: BuilderId(
project: 'flutter',
bucket: 'prod',
builder: 'Linux',
),
status: Status.success,
)
],
),
),
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[
Build(
id: 998,
builderId: BuilderId(
project: 'flutter',
bucket: 'prod',
builder: 'Linux',
),
status: Status.success,
)
],
),
),
],
);
});
request.body = jsonTemplate('unlabeled', issueNumber, 'master');
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
await tester.post(webhook);
expect(
json.encode(verify(mockBuildBucketClient.batch(captureAny)).captured),
'[{"requests":[{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"createdBy":"test@test","tags":[{"key":"buildset","value":"pr/git/123"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"},{"key":"user_agent","value":"flutter-cocoon"}]}}},'
'{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"user_agent","value":"recipe"}]}}}]}]',
);
});
test('When synchronized, cancels existing builds and schedules new ones',
() async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[
Build(
id: 999,
builderId: BuilderId(
project: 'flutter',
bucket: 'prod',
builder: 'Linux',
),
status: Status.ended,
)
],
),
),
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[
Build(
id: 998,
builderId: BuilderId(
project: 'flutter',
bucket: 'prod',
builder: 'Linux',
),
status: Status.ended,
)
],
),
),
],
);
});
request.body = jsonTemplate('synchronize', issueNumber, 'master',
includeCqLabel: true);
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
final MockRepositoriesService mockRepositoriesService =
MockRepositoriesService();
when(gitHubClient.repositories).thenReturn(mockRepositoriesService);
await tester.post(webhook);
expect(
json.encode(verify(mockBuildBucketClient.batch(captureAny)).captured),
'[{"requests":[{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"createdBy":"test@test","tags":[{"key":"buildset","value":"pr/git/123"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"},{"key":"user_agent","value":"flutter-cocoon"}]}}},{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"user_agent","value":"recipe"}]}}}]},{"requests":[{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"createdBy":"test@test","tags":[{"key":"buildset","value":"pr/git/123"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"},{"key":"user_agent","value":"flutter-cocoon"}]}}},{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"user_agent","value":"recipe"}]}}}]},{"requests":[{"scheduleBuild":{"builder":{"project":"flutter","bucket":"try","builder":"Linux"},"properties":{"git_url":"https://github.com/flutter/flutter","git_ref":"refs/pull/123/head"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"buildset","value":"sha/git/be6ff099a4ee56e152a5fa2f37edd10f79d1269a"},{"key":"user_agent","value":"flutter-cocoon"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"}],"notify":{"pubsubTopic":"projects/flutter-dashboard/topics/luci-builds","userData":"eyJyZXBvX293bmVyIjoiZmx1dHRlciIsInJlcG9fbmFtZSI6ImZsdXR0ZXIiLCJ1c2VyX2FnZW50IjoiZmx1dHRlci1jb2Nvb24ifQ=="}}},{"scheduleBuild":{"builder":{"project":"flutter","bucket":"try","builder":"Mac"},"properties":{"git_url":"https://github.com/flutter/flutter","git_ref":"refs/pull/123/head"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"buildset","value":"sha/git/be6ff099a4ee56e152a5fa2f37edd10f79d1269a"},{"key":"user_agent","value":"flutter-cocoon"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"}],"notify":{"pubsubTopic":"projects/flutter-dashboard/topics/luci-builds","userData":"eyJyZXBvX293bmVyIjoiZmx1dHRlciIsInJlcG9fbmFtZSI6ImZsdXR0ZXIiLCJ1c2VyX2FnZW50IjoiZmx1dHRlci1jb2Nvb24ifQ=="}}},{"scheduleBuild":{"builder":{"project":"flutter","bucket":"try","builder":"Windows"},"properties":{"git_url":"https://github.com/flutter/flutter","git_ref":"refs/pull/123/head"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"buildset","value":"sha/git/be6ff099a4ee56e152a5fa2f37edd10f79d1269a"},{"key":"user_agent","value":"flutter-cocoon"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"}],"notify":{"pubsubTopic":"projects/flutter-dashboard/topics/luci-builds","userData":"eyJyZXBvX293bmVyIjoiZmx1dHRlciIsInJlcG9fbmFtZSI6ImZsdXR0ZXIiLCJ1c2VyX2FnZW50IjoiZmx1dHRlci1jb2Nvb24ifQ=="}}},{"scheduleBuild":{"builder":{"project":"flutter","bucket":"try","builder":"Linux Coverage"},"properties":{"git_url":"https://github.com/flutter/flutter","git_ref":"refs/pull/123/head"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"buildset","value":"sha/git/be6ff099a4ee56e152a5fa2f37edd10f79d1269a"},{"key":"user_agent","value":"flutter-cocoon"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"}],"notify":{"pubsubTopic":"projects/flutter-dashboard/topics/luci-builds","userData":"eyJyZXBvX293bmVyIjoiZmx1dHRlciIsInJlcG9fbmFtZSI6ImZsdXR0ZXIiLCJ1c2VyX2FnZW50IjoiZmx1dHRlci1jb2Nvb24ifQ=="}}}]},{"requests":[{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"createdBy":"test@test","tags":[{"key":"buildset","value":"pr/git/123"},{"key":"github_link","value":"https://github.com/flutter/flutter/pull/123"},{"key":"user_agent","value":"flutter-cocoon"}]}}},{"searchBuilds":{"predicate":{"builder":{"project":"flutter","bucket":"try"},"tags":[{"key":"buildset","value":"pr/git/123"},{"key":"user_agent","value":"recipe"}]}}}]}]',
);
});
});
group('checksAPI', () {
void _generateRequest(String bodyString) {
request.body = bodyString;
final Uint8List body = utf8.encode(request.body) as Uint8List;
final Uint8List key = utf8.encode(keyString) as Uint8List;
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
}
test('CheckSuite Event is delegated to GithubChecksService', () async {
_generateRequest(checkSuiteString);
request.headers.set('X-GitHub-Event', 'check_suite');
await tester.post(webhook);
verify(mockGithubChecksService.handleCheckSuite(any, any)).called(1);
});
test('CheckRun Event is delegated to GithubChecksService', () async {
_generateRequest(checkRunString);
request.headers.set('X-GitHub-Event', 'check_run');
await tester.post(webhook);
verify(mockGithubChecksService.handleCheckRun(any, any)).called(1);
});
});
});
}
class MockGitHubClient extends Mock implements GitHub {}
class MockIssuesService extends Mock implements IssuesService {}
class MockPullRequestsService extends Mock implements PullRequestsService {}
// ignore: must_be_immutable
class MockBuildBucketClient extends Mock implements BuildBucketClient {}
String skiaIgnoreTemplate({String pullRequestNumber = '0000'}) {
return '''
[
{
"id": "7579425228619212078",
"name": "contributor@getMail.com",
"updatedBy": "contributor@getMail.com",
"expires": "2019-09-06T21:28:18.815336Z",
"query": "ext=png&name=widgets.golden_file_test",
"note": "https://github.com/flutter/flutter/pull/$pullRequestNumber"
}
]
''';
}
String jsonTemplate(String action, int number, String baseRef,
{String login = 'flutter',
bool includeCqLabel = false,
bool includeGoldLabel = false,
bool isDraft = false,
bool merged = false,
String repoFullName = 'flutter/flutter',
String repoName = 'flutter'}) =>
'''{
"action": "$action",
"number": $number,
"pull_request": {
"url": "https://api.github.com/repos/$repoFullName/pulls/$number",
"id": 294034,
"node_id": "MDExOlB1bGxSZXF1ZXN0Mjk0MDMzODQx",
"html_url": "https://github.com/$repoFullName/pull/$number",
"diff_url": "https://github.com/$repoFullName/pull/$number.diff",
"patch_url": "https://github.com/$repoFullName/pull/$number.patch",
"issue_url": "https://api.github.com/repos/$repoFullName/issues/$number",
"number": $number,
"state": "open",
"locked": false,
"title": "Defer reassemble until reload is finished",
"user": {
"login": "$login",
"id": 862741,
"node_id": "MDQ6VXNlcjg2MjA3NDE=",
"avatar_url": "https://avatars3.githubusercontent.com/u/8620741?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/flutter",
"html_url": "https://github.com/flutter",
"followers_url": "https://api.github.com/users/flutter/followers",
"following_url": "https://api.github.com/users/flutter/following{/other_user}",
"gists_url": "https://api.github.com/users/flutter/gists{/gist_id}",
"starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/flutter/subscriptions",
"organizations_url": "https://api.github.com/users/flutter/orgs",
"repos_url": "https://api.github.com/users/flutter/repos",
"events_url": "https://api.github.com/users/flutter/events{/privacy}",
"received_events_url": "https://api.github.com/users/flutter/received_events",
"type": "User",
"site_admin": false
},
"draft" : "$isDraft",
"body": "The body",
"created_at": "2019-07-03T07:14:35Z",
"updated_at": "2019-07-03T16:34:53Z",
"closed_at": null,
"merged_at": null,
"merge_commit_sha": "d22ab7ced21d3b2a5be00cf576d383eb5ffddb8a",
"assignee": null,
"assignees": [],
"requested_reviewers": [],
"requested_teams": [],
"labels": [
{
"id": 487496476,
"node_id": "MDU6TGFiZWw0ODc0OTY0NzY=",
"url": "https://api.github.com/repos/$repoFullName/labels/cla:%20yes",
"name": "cla: yes",
"color": "ffffff",
"default": false
},
{
"id": 284437560,
"node_id": "MDU6TGFiZWwyODQ0Mzc1NjA=",
"url": "https://api.github.com/repos/$repoFullName/labels/framework",
"name": "framework",
"color": "207de5",
"default": false
},
${includeGoldLabel ? '''
{
"id": 283480100,
"node_id": "MDU6TGFiZWwyODM0ODAxMDA=",
"url": "https://api.github.com/repos/$repoFullName/labels/tool",
"name": "will affect goldens",
"color": "5319e7",
"default": false
},''' : ''}
${includeCqLabel ? '''
{
"id": 283480100,
"node_id": "MDU6TGFiZWwyODM0ODAxMDA=",
"url": "https://api.github.com/repos/$repoFullName/labels/tool",
"name": "CQ+1",
"color": "5319e7",
"default": false
},''' : ''}
{
"id": 283480100,
"node_id": "MDU6TGFiZWwyODM0ODAxMDA=",
"url": "https://api.github.com/repos/$repoFullName/labels/tool",
"name": "tool",
"color": "5319e7",
"default": false
}
],
"milestone": null,
"commits_url": "https://api.github.com/repos/$repoFullName/pulls/$number/commits",
"review_comments_url": "https://api.github.com/repos/$repoFullName/pulls/$number/comments",
"review_comment_url": "https://api.github.com/repos/$repoFullName/pulls/comments{/number}",
"comments_url": "https://api.github.com/repos/$repoFullName/issues/$number/comments",
"statuses_url": "https://api.github.com/repos/$repoFullName/statuses/be6ff099a4ee56e152a5fa2f37edd10f79d1269a",
"head": {
"label": "$login:wait_for_reassemble",
"ref": "wait_for_reassemble",
"sha": "be6ff099a4ee56e152a5fa2f37edd10f79d1269a",
"user": {
"login": "$login",
"id": 8620741,
"node_id": "MDQ6VXNlcjg2MjA3NDE=",
"avatar_url": "https://avatars3.githubusercontent.com/u/8620741?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/flutter",
"html_url": "https://github.com/flutter",
"followers_url": "https://api.github.com/users/flutter/followers",
"following_url": "https://api.github.com/users/flutter/following{/other_user}",
"gists_url": "https://api.github.com/users/flutter/gists{/gist_id}",
"starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/flutter/subscriptions",
"organizations_url": "https://api.github.com/users/flutter/orgs",
"repos_url": "https://api.github.com/users/flutter/repos",
"events_url": "https://api.github.com/users/flutter/events{/privacy}",
"received_events_url": "https://api.github.com/users/flutter/received_events",
"type": "User",
"site_admin": false
},
"repo": {
"id": 131232406,
"node_id": "MDEwOlJlcG9zaXRvcnkxMzEyMzI0MDY=",
"name": "$repoName",
"full_name": "$repoFullName",
"private": false,
"owner": {
"login": "flutter",
"id": 8620741,
"node_id": "MDQ6VXNlcjg2MjA3NDE=",
"avatar_url": "https://avatars3.githubusercontent.com/u/8620741?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/flutter",
"html_url": "https://github.com/flutter",
"followers_url": "https://api.github.com/users/flutter/followers",
"following_url": "https://api.github.com/users/flutter/following{/other_user}",
"gists_url": "https://api.github.com/users/flutter/gists{/gist_id}",
"starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/flutter/subscriptions",
"organizations_url": "https://api.github.com/users/flutter/orgs",
"repos_url": "https://api.github.com/users/flutter/repos",
"events_url": "https://api.github.com/users/flutter/events{/privacy}",
"received_events_url": "https://api.github.com/users/flutter/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/$repoFullName",
"description": "Flutter makes it easy and fast to build beautiful mobile apps.",
"fork": true,
"url": "https://api.github.com/repos/$repoFullName",
"forks_url": "https://api.github.com/repos/$repoFullName/forks",
"keys_url": "https://api.github.com/repos/$repoFullName/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/$repoFullName/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/$repoFullName/teams",
"hooks_url": "https://api.github.com/repos/$repoFullName/hooks",
"issue_events_url": "https://api.github.com/repos/$repoFullName/issues/events{/number}",
"events_url": "https://api.github.com/repos/$repoFullName/events",
"assignees_url": "https://api.github.com/repos/$repoFullName/assignees{/user}",
"branches_url": "https://api.github.com/repos/$repoFullName/branches{/branch}",
"tags_url": "https://api.github.com/repos/$repoFullName/tags",
"blobs_url": "https://api.github.com/repos/$repoFullName/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/$repoFullName/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/$repoFullName/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/$repoFullName/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/$repoFullName/statuses/{sha}",
"languages_url": "https://api.github.com/repos/$repoFullName/languages",
"stargazers_url": "https://api.github.com/repos/$repoFullName/stargazers",
"contributors_url": "https://api.github.com/repos/$repoFullName/contributors",
"subscribers_url": "https://api.github.com/repos/$repoFullName/subscribers",
"subscription_url": "https://api.github.com/repos/$repoFullName/subscription",
"commits_url": "https://api.github.com/repos/$repoFullName/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/$repoFullName/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/$repoFullName/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/$repoFullName/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/$repoFullName/contents/{+path}",
"compare_url": "https://api.github.com/repos/$repoFullName/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/$repoFullName/merges",
"archive_url": "https://api.github.com/repos/$repoFullName/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/$repoFullName/downloads",
"issues_url": "https://api.github.com/repos/$repoFullName/issues{/number}",
"pulls_url": "https://api.github.com/repos/$repoFullName/pulls{/number}",
"milestones_url": "https://api.github.com/repos/$repoFullName/milestones{/number}",
"notifications_url": "https://api.github.com/repos/$repoFullName/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/$repoFullName/labels{/name}",
"releases_url": "https://api.github.com/repos/$repoFullName/releases{/id}",
"deployments_url": "https://api.github.com/repos/$repoFullName/deployments",
"created_at": "2018-04-27T02:03:08Z",
"updated_at": "2019-06-27T06:56:59Z",
"pushed_at": "2019-07-03T19:40:11Z",
"git_url": "git://github.com/$repoFullName.git",
"ssh_url": "git@github.com:$repoFullName.git",
"clone_url": "https://github.com/$repoFullName.git",
"svn_url": "https://github.com/$repoFullName",
"homepage": "https://flutter.io",
"size": 94508,
"stargazers_count": 1,
"watchers_count": 1,
"language": "Dart",
"has_issues": false,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": {
"key": "other",
"name": "Other",
"spdx_id": "NOASSERTION",
"url": null,
"node_id": "MDc6TGljZW5zZTA="
},
"forks": 0,
"open_issues": 0,
"watchers": 1,
"default_branch": "master"
}
},
"base": {
"label": "flutter:$baseRef",
"ref": "$baseRef",
"sha": "4cd12fc8b7d4cc2d8609182e1c4dea5cddc86890",
"user": {
"login": "flutter",
"id": 14101776,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE0MTAxNzc2",
"avatar_url": "https://avatars3.githubblahblahblah",
"gravatar_id": "",
"url": "https://api.github.com/users/flutter",
"html_url": "https://github.com/flutter",
"followers_url": "https://api.github.com/users/flutter/followers",
"following_url": "https://api.github.com/users/flutter/following{/other_user}",
"gists_url": "https://api.github.com/users/flutter/gists{/gist_id}",
"starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/flutter/subscriptions",
"organizations_url": "https://api.github.com/users/flutter/orgs",
"repos_url": "https://api.github.com/users/flutter/repos",
"events_url": "https://api.github.com/users/flutter/events{/privacy}",
"received_events_url": "https://api.github.com/users/flutter/received_events",
"type": "Organization",
"site_admin": false
},
"repo": {
"id": 31792824,
"node_id": "MDEwOlJlcG9zaXRvcnkzMTc5MjgyNA==",
"name": "$repoName",
"full_name": "$repoFullName",
"private": false,
"owner": {
"login": "flutter",
"id": 14101776,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE0MTAxNzc2",
"avatar_url": "https://avatars3.githubblahblahblah",
"gravatar_id": "",
"url": "https://api.github.com/users/flutter",
"html_url": "https://github.com/flutter",
"followers_url": "https://api.github.com/users/flutter/followers",
"following_url": "https://api.github.com/users/flutter/following{/other_user}",
"gists_url": "https://api.github.com/users/flutter/gists{/gist_id}",
"starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/flutter/subscriptions",
"organizations_url": "https://api.github.com/users/flutter/orgs",
"repos_url": "https://api.github.com/users/flutter/repos",
"events_url": "https://api.github.com/users/flutter/events{/privacy}",
"received_events_url": "https://api.github.com/users/flutter/received_events",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/$repoFullName",
"description": "Flutter makes it easy and fast to build beautiful mobile apps.",
"fork": false,
"url": "https://api.github.com/repos/$repoFullName",
"forks_url": "https://api.github.com/repos/$repoFullName/forks",
"keys_url": "https://api.github.com/repos/$repoFullName/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/$repoFullName/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/$repoFullName/teams",
"hooks_url": "https://api.github.com/repos/$repoFullName/hooks",
"issue_events_url": "https://api.github.com/repos/$repoFullName/issues/events{/number}",
"events_url": "https://api.github.com/repos/$repoFullName/events",
"assignees_url": "https://api.github.com/repos/$repoFullName/assignees{/user}",
"branches_url": "https://api.github.com/repos/$repoFullName/branches{/branch}",
"tags_url": "https://api.github.com/repos/$repoFullName/tags",
"blobs_url": "https://api.github.com/repos/$repoFullName/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/$repoFullName/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/$repoFullName/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/$repoFullName/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/$repoFullName/statuses/{sha}",
"languages_url": "https://api.github.com/repos/$repoFullName/languages",
"stargazers_url": "https://api.github.com/repos/$repoFullName/stargazers",
"contributors_url": "https://api.github.com/repos/$repoFullName/contributors",
"subscribers_url": "https://api.github.com/repos/$repoFullName/subscribers",
"subscription_url": "https://api.github.com/repos/$repoFullName/subscription",
"commits_url": "https://api.github.com/repos/$repoFullName/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/$repoFullName/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/$repoFullName/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/$repoFullName/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/$repoFullName/contents/{+path}",
"compare_url": "https://api.github.com/repos/$repoFullName/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/$repoFullName/merges",
"archive_url": "https://api.github.com/repos/$repoFullName/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/$repoFullName/downloads",
"issues_url": "https://api.github.com/repos/$repoFullName/issues{/number}",
"pulls_url": "https://api.github.com/repos/$repoFullName/pulls{/number}",
"milestones_url": "https://api.github.com/repos/$repoFullName/milestones{/number}",
"notifications_url": "https://api.github.com/repos/$repoFullName/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/$repoFullName/labels{/name}",
"releases_url": "https://api.github.com/repos/$repoFullName/releases{/id}",
"deployments_url": "https://api.github.com/repos/$repoFullName/deployments",
"created_at": "2015-03-06T22:54:58Z",
"updated_at": "2019-07-04T02:08:44Z",
"pushed_at": "2019-07-04T02:03:04Z",
"git_url": "git://github.com/$repoFullName.git",
"ssh_url": "git@github.com:$repoFullName.git",
"clone_url": "https://github.com/$repoFullName.git",
"svn_url": "https://github.com/$repoFullName",
"homepage": "https://flutter.dev",
"size": 65507,
"stargazers_count": 68944,
"watchers_count": 68944,
"language": "Dart",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 7987,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 6536,
"license": {
"key": "other",
"name": "Other",
"spdx_id": "NOASSERTION",
"url": null,
"node_id": "MDc6TGljZW5zZTA="
},
"forks": 7987,
"open_issues": 6536,
"watchers": 68944,
"default_branch": "master"
}
},
"_links": {
"self": {
"href": "https://api.github.com/repos/$repoFullName/pulls/$number"
},
"html": {
"href": "https://github.com/$repoFullName/pull/$number"
},
"issue": {
"href": "https://api.github.com/repos/$repoFullName/issues/$number"
},
"comments": {
"href": "https://api.github.com/repos/$repoFullName/issues/$number/comments"
},
"review_comments": {
"href": "https://api.github.com/repos/$repoFullName/pulls/$number/comments"
},
"review_comment": {
"href": "https://api.github.com/repos/$repoFullName/pulls/comments{/number}"
},
"commits": {
"href": "https://api.github.com/repos/$repoFullName/pulls/$number/commits"
},
"statuses": {
"href": "https://api.github.com/repos/$repoFullName/statuses/deadbeef"
}
},
"author_association": "MEMBER",
"draft" : $isDraft,
"merged": $merged,
"mergeable": null,
"rebaseable": true,
"mergeable_state": "draft",
"merged_by": null,
"comments": 1,
"review_comments": 0,
"maintainer_can_modify": true,
"commits": 5,
"additions": 55,
"deletions": 36,
"changed_files": 5
},
"repository": {
"id": 1868532,
"node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
"name": "$repoName",
"full_name": "$repoFullName",
"private": false,
"owner": {
"login": "flutter",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/flutter",
"html_url": "https://github.com/flutter",
"followers_url": "https://api.github.com/users/flutter/followers",
"following_url": "https://api.github.com/users/flutter/following{/other_user}",
"gists_url": "https://api.github.com/users/flutter/gists{/gist_id}",
"starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/flutter/subscriptions",
"organizations_url": "https://api.github.com/users/flutter/orgs",
"repos_url": "https://api.github.com/users/flutter/repos",
"events_url": "https://api.github.com/users/flutter/events{/privacy}",
"received_events_url": "https://api.github.com/users/flutter/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/$repoFullName",
"description": null,
"fork": false,
"url": "https://api.github.com/repos/$repoFullName",
"forks_url": "https://api.github.com/repos/$repoFullName/forks",
"keys_url": "https://api.github.com/repos/$repoFullName/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/$repoFullName/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/$repoFullName/teams",
"hooks_url": "https://api.github.com/repos/$repoFullName/hooks",
"issue_events_url": "https://api.github.com/repos/$repoFullName/issues/events{/number}",
"events_url": "https://api.github.com/repos/$repoFullName/events",
"assignees_url": "https://api.github.com/repos/$repoFullName/assignees{/user}",
"branches_url": "https://api.github.com/repos/$repoFullName/branches{/branch}",
"tags_url": "https://api.github.com/repos/$repoFullName/tags",
"blobs_url": "https://api.github.com/repos/$repoFullName/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/$repoFullName/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/$repoFullName/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/$repoFullName/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/$repoFullName/statuses/{sha}",
"languages_url": "https://api.github.com/repos/$repoFullName/languages",
"stargazers_url": "https://api.github.com/repos/$repoFullName/stargazers",
"contributors_url": "https://api.github.com/repos/$repoFullName/contributors",
"subscribers_url": "https://api.github.com/repos/$repoFullName/subscribers",
"subscription_url": "https://api.github.com/repos/$repoFullName/subscription",
"commits_url": "https://api.github.com/repos/$repoFullName/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/$repoFullName/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/$repoFullName/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/$repoFullName/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/$repoFullName/contents/{+path}",
"compare_url": "https://api.github.com/repos/$repoFullName/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/$repoFullName/merges",
"archive_url": "https://api.github.com/repos/$repoFullName/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/$repoFullName/downloads",
"issues_url": "https://api.github.com/repos/$repoFullName/issues{/number}",
"pulls_url": "https://api.github.com/repos/$repoFullName/pulls{/number}",
"milestones_url": "https://api.github.com/repos/$repoFullName/milestones{/number}",
"notifications_url": "https://api.github.com/repos/$repoFullName/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/$repoFullName/labels{/name}",
"releases_url": "https://api.github.com/repos/$repoFullName/releases{/id}",
"deployments_url": "https://api.github.com/repos/$repoFullName/deployments",
"created_at": "2019-05-15T15:19:25Z",
"updated_at": "2019-05-15T15:19:27Z",
"pushed_at": "2019-05-15T15:20:32Z",
"git_url": "git://github.com/$repoFullName.git",
"ssh_url": "git@github.com:$repoFullName.git",
"clone_url": "https://github.com/$repoFullName.git",
"svn_url": "https://github.com/$repoFullName",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 2,
"license": null,
"forks": 0,
"open_issues": 2,
"watchers": 0,
"default_branch": "master"
},
"sender": {
"login": "$login",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/flutter",
"html_url": "https://github.com/flutter",
"followers_url": "https://api.github.com/users/flutter/followers",
"following_url": "https://api.github.com/users/flutter/following{/other_user}",
"gists_url": "https://api.github.com/users/flutter/gists{/gist_id}",
"starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/flutter/subscriptions",
"organizations_url": "https://api.github.com/users/flutter/orgs",
"repos_url": "https://api.github.com/users/flutter/repos",
"events_url": "https://api.github.com/users/flutter/events{/privacy}",
"received_events_url": "https://api.github.com/users/flutter/received_events",
"type": "User",
"site_admin": false
}
}''';