blob: d82400cbf3dcb86fcf0febd115ae62523d1e824a [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:io';
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/request_handling/exceptions.dart';
import 'package:http/testing.dart' as http_test;
import 'package:http/http.dart' as http;
import 'package:github/github.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import '../src/datastore/fake_cocoon_config.dart';
import '../src/request_handling/fake_http.dart';
import '../src/request_handling/fake_logging.dart';
import '../src/request_handling/request_handler_tester.dart';
import '../src/utilities/mocks.dart';
import '../src/utilities/push_message.dart';
const String ref = 'deadbeef';
void main() {
const String authToken = '123';
const String authHeader = 'Bearer $authToken';
const String deviceLabEmail =
'flutter-devicelab@flutter-dashboard.iam.gserviceaccount.com';
LuciStatusHandler handler;
FakeConfig config;
MockGitHub mockGitHubClient;
FakeHttpRequest request;
RequestHandlerTester tester;
MockRepositoriesService mockRepositoriesService;
MockBuildBucketClient buildBucketClient;
final FakeLogging log = FakeLogging();
setUp(() {
config = FakeConfig(luciTryInfraFailureRetriesValue: 2);
buildBucketClient = MockBuildBucketClient();
handler = LuciStatusHandler(
config,
buildBucketClient,
loggingProvider: () => log,
);
request = FakeHttpRequest();
tester = RequestHandlerTester(
request: request,
httpClient: http_test.MockClient((http.BaseRequest request) async {
expect(
request.url.toString(),
'https://www.googleapis.com/oauth2/v2/tokeninfo?id_token=$authToken&alt=json',
);
return http.Response(
'''{
"issued_to": "456",
"audience": "https://flutter-dashboard.appspot.com/api/luci-status-handler",
"user_id": "789",
"expires_in": 123,
"email": "$deviceLabEmail",
"verified_email": true,
"issuer": "https://accounts.google.com",
"issued_at": 412321
}''',
200,
headers: <String, String>{
HttpHeaders.contentTypeHeader: 'application/json',
},
);
}),
);
config.luciTryBuildersValue = (json.decode('''[
{"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>>();
mockGitHubClient = MockGitHub();
mockRepositoriesService = MockRepositoriesService();
when(mockGitHubClient.repositories).thenReturn(mockRepositoriesService);
config.githubClient = mockGitHubClient;
config.deviceLabServiceAccountValue = const ServiceAccountInfo(
email: deviceLabEmail,
);
});
test('Rejects unauthorized requests', () async {
request.bodyBytes = utf8.encode(pushMessageJson('SCHEDULED')) as Uint8List;
await expectLater(
() => tester.post(handler),
throwsA(const TypeMatcher<Unauthorized>()),
);
});
group('pending', () {
List<RepositoryStatus> repositoryStatuses;
setUp(() {
when(mockRepositoriesService.listStatuses(any, ref)).thenAnswer((_) {
return Stream<RepositoryStatus>.fromIterable(repositoryStatuses);
});
});
tearDown(() {
repositoryStatuses = null;
});
test('Handles a scheduled status as pending and pending is not most recent',
() async {
repositoryStatuses = <RepositoryStatus>[
RepositoryStatus()
..context = 'Linux Coverage'
..state = 'failure',
RepositoryStatus()
..context = 'Linux Coverage'
..state = 'pending',
];
request.bodyBytes =
utf8.encode(pushMessageJson('SCHEDULED')) as Uint8List;
request.headers.add(HttpHeaders.authorizationHeader, authHeader);
await tester.post(handler);
expect(
verify(mockRepositoriesService.createStatus(
RepositorySlug('flutter', 'flutter'),
ref,
captureAny,
)).captured.single.toJson(),
jsonDecode(
'{"state":"pending","target_url":"https://ci.chromium.org/b/8905920700440101120?reload=30","description":"Flutter LUCI Build: Linux Coverage","context":"Linux Coverage"}'),
);
});
test(
'Handles a scheduled status as pending and pending is not most recent with query param',
() async {
repositoryStatuses = <RepositoryStatus>[
RepositoryStatus()
..context = 'Linux Coverage'
..state = 'failure',
RepositoryStatus()
..context = 'Linux Coverage'
..state = 'pending',
];
request.bodyBytes =
utf8.encode(pushMessageJson('SCHEDULED', urlParam: '?foo=bar'))
as Uint8List;
request.headers.add(HttpHeaders.authorizationHeader, authHeader);
await tester.post(handler);
expect(
verify(mockRepositoriesService.createStatus(
RepositorySlug('flutter', 'flutter'),
ref,
captureAny,
)).captured.single.toJson(),
jsonDecode(
'{"state":"pending","target_url":"https://ci.chromium.org/b/8905920700440101120?foo=bar&reload=30","description":"Flutter LUCI Build: Linux Coverage","context":"Linux Coverage"}'),
);
});
test('Handles a scheduled status as pending and pending already set',
() async {
repositoryStatuses = <RepositoryStatus>[
RepositoryStatus()
..context = 'Linux Coverage'
..state = 'pending'
..targetUrl = 'https://ci.chromium.org/b/8905920700440101120',
];
request.bodyBytes =
utf8.encode(pushMessageJson('SCHEDULED')) as Uint8List;
request.headers.add(HttpHeaders.authorizationHeader, authHeader);
await tester.post(handler);
verifyNever(mockRepositoriesService.createStatus(
RepositorySlug('flutter', 'flutter'),
ref,
any,
));
});
test('Handles a started status as pending and most recent is not pending',
() async {
repositoryStatuses = <RepositoryStatus>[
RepositoryStatus()
..context = 'Linux Coverage'
..state = 'failure',
RepositoryStatus()
..context = 'Linux Coverage'
..state = 'pending',
];
request.bodyBytes = utf8.encode(pushMessageJson('STARTED')) as Uint8List;
request.headers.add(HttpHeaders.authorizationHeader, authHeader);
await tester.post(handler);
expect(
verify(mockRepositoriesService.createStatus(
RepositorySlug('flutter', 'flutter'),
ref,
captureAny,
)).captured.single.toJson(),
jsonDecode(
'{"state":"pending","target_url":"https://ci.chromium.org/b/8905920700440101120?reload=30","description":"Flutter LUCI Build: Linux Coverage","context":"Linux Coverage"}'),
);
});
test('Handles a started status as pending and pending already set',
() async {
repositoryStatuses = <RepositoryStatus>[
RepositoryStatus()
..context = 'Linux Coverage'
..state = 'pending'
..targetUrl = 'https://ci.chromium.org/b/8905920700440101120',
];
request.bodyBytes = utf8.encode(pushMessageJson('STARTED')) as Uint8List;
request.headers.add(HttpHeaders.authorizationHeader, authHeader);
await tester.post(handler);
verifyNever(mockRepositoriesService.createStatus(
RepositorySlug('flutter', 'flutter'), ref, any));
});
});
test('Handles a completed/failure status/result as failure', () async {
request.bodyBytes = utf8
.encode(pushMessageJson('COMPLETED', result: 'FAILURE')) as Uint8List;
request.headers.add(HttpHeaders.authorizationHeader, authHeader);
await tester.post(handler);
expect(
verify(mockRepositoriesService.createStatus(
RepositorySlug('flutter', 'flutter'),
ref,
captureAny,
)).captured.single.toJson(),
jsonDecode(
'{"state":"failure","target_url":"https://ci.chromium.org/b/8905920700440101120","description":"Flutter LUCI Build: Linux Coverage","context":"Linux Coverage"}'),
);
});
test('Does not schedule after too many retries with infra failure', () async {
request.bodyBytes = utf8.encode(pushMessageJson('COMPLETED',
builderName: 'Linux',
result: 'FAILURE',
failureReason: 'INFRA_FAILURE',
retries: 2)) as Uint8List;
request.headers.add(HttpHeaders.authorizationHeader, authHeader);
await tester.post(handler);
verifyNever(buildBucketClient.scheduleBuild(any));
expect(
verify(mockRepositoriesService.createStatus(
RepositorySlug('flutter', 'flutter'),
ref,
captureAny,
)).captured.single.toJson(),
jsonDecode(
'{"state":"failure","target_url":"https://ci.chromium.org/b/8905920700440101120","description":"Flutter LUCI Build: Linux","context":"Linux"}'),
);
});
test('Handles a completed/canceled status/result as failure', () async {
request.bodyBytes = utf8
.encode(pushMessageJson('COMPLETED', result: 'CANCELED')) as Uint8List;
request.headers.add(HttpHeaders.authorizationHeader, authHeader);
await tester.post(handler);
expect(
verify(mockRepositoriesService.createStatus(
RepositorySlug('flutter', 'flutter'),
ref,
captureAny,
)).captured.single.toJson(),
jsonDecode(
'{"state":"failure","target_url":"https://ci.chromium.org/b/8905920700440101120","description":"Flutter LUCI Build: Linux Coverage","context":"Linux Coverage"}'),
);
});
test('Handles a completed/success status/result as sucess', () async {
request.bodyBytes = utf8
.encode(pushMessageJson('COMPLETED', result: 'SUCCESS')) as Uint8List;
request.headers.add(HttpHeaders.authorizationHeader, authHeader);
await tester.post(handler);
expect(
verify(mockRepositoriesService.createStatus(
RepositorySlug('flutter', 'flutter'),
ref,
captureAny,
)).captured.single.toJson(),
jsonDecode(
'{"state":"success","target_url":"https://ci.chromium.org/b/8905920700440101120","description":"Flutter LUCI Build: Linux Coverage","context":"Linux Coverage"}'),
);
});
test('Handles engine builder', () async {
request.bodyBytes = utf8.encode(pushMessageJson(
'COMPLETED',
result: 'SUCCESS',
builderName: 'Linux Host Engine',
)) as Uint8List;
request.headers.add(HttpHeaders.authorizationHeader, authHeader);
await tester.post(handler);
expect(
verify(mockRepositoriesService.createStatus(
RepositorySlug('flutter', 'engine'),
ref,
captureAny,
)).captured.single.toJson(),
jsonDecode(
'{"state":"success","target_url":"https://ci.chromium.org/b/8905920700440101120","description":"Flutter LUCI Build: Linux Host Engine","context":"Linux Host Engine"}'),
);
});
test('Requests without buildset skip status updates', () async {
request.bodyBytes = utf8.encode(pushMessageJsonNoBuildset(
'COMPLETED',
result: 'SUCCESS',
builderName: 'Linux Host Engine',
)) as Uint8List;
request.headers.add(HttpHeaders.authorizationHeader, authHeader);
await tester.post(handler);
verifyNever(mockRepositoriesService.createStatus(any, any, any));
});
}