blob: 7bd870d8b4cc2c6a6ee887633bdb1c4e8c4d2e53 [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 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:http/http.dart' show Client, Request, Response;
import 'package:http/testing.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:cocoon_service/protos.dart' show Agent, Commit, CommitStatus, Key, RootKey, Stage, Task;
import 'package:app_flutter/service/appengine_cocoon.dart';
import 'package:app_flutter/service/downloader.dart';
import 'package:app_flutter/service/cocoon.dart';
import '../utils/appengine_cocoon_test_data.dart';
void main() {
group('AppEngine CocoonService fetchCommitStatus', () {
AppEngineCocoonService service;
setUp(() async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
return Response(jsonGetStatsResponse, 200);
}));
});
test('should return CocoonResponse<List<CommitStatus>>', () {
expect(service.fetchCommitStatuses(), const TypeMatcher<Future<CocoonResponse<List<CommitStatus>>>>());
});
test('should return expected List<CommitStatus>', () async {
final CocoonResponse<List<CommitStatus>> statuses = await service.fetchCommitStatuses();
final CommitStatus expectedStatus = CommitStatus()
..branch = 'master'
..commit = (Commit()
..timestamp = Int64(123456789)
..key = (RootKey()..child = (Key()..name = 'iamatestkey'))
..sha = 'ShaShankHash'
..author = 'ShaSha'
..authorAvatarUrl = 'https://flutter.dev'
..repository = 'flutter/cocoon'
..branch = 'master')
..stages.add(Stage()
..name = 'devicelab'
..taskStatus = 'Succeeded'
..tasks.add(Task()
..key = (RootKey()..child = (Key()..name = 'taskKey1'))
..createTimestamp = Int64(1569353940885)
..startTimestamp = Int64(1569354594672)
..endTimestamp = Int64(1569354700642)
..name = 'complex_layout_semantics_perf'
..attempts = 1
..isFlaky = false
..timeoutInMinutes = 0
..reason = ''
..requiredCapabilities.add('[linux/android]')
..reservedForAgentId = 'linux2'
..stageName = 'devicelab'
..status = 'Succeeded'));
expect(statuses.data.length, 1);
expect(statuses.data.first, expectedStatus);
});
/// This requires a separate test run on the web platform.
test('should query correct endpoint whether web or mobile', () async {
final Client mockClient = MockHttpClient();
when(mockClient.get(any)).thenAnswer((_) => Future<Response>.value(Response('', 200)));
service = AppEngineCocoonService(client: mockClient);
await service.fetchCommitStatuses();
if (kIsWeb) {
verify(mockClient.get('/api/public/get-status?branch=master'));
} else {
verify(mockClient.get('$baseApiUrl/api/public/get-status?branch=master'));
}
});
/// This requires a separate test run on the web platform.
test('should query correct endpoint when given a specific branch', () async {
final Client mockClient = MockHttpClient();
when(mockClient.get(any)).thenAnswer((_) => Future<Response>.value(Response('', 200)));
service = AppEngineCocoonService(client: mockClient);
await service.fetchCommitStatuses(branch: 'stable');
if (kIsWeb) {
verify(mockClient.get('/api/public/get-status?branch=stable'));
} else {
verify(mockClient.get('$baseApiUrl/api/public/get-status?branch=stable'));
}
});
test('given last commit status should query correct endpoint whether web or mobile', () async {
final Client mockClient = MockHttpClient();
when(mockClient.get(any)).thenAnswer((_) => Future<Response>.value(Response('', 200)));
service = AppEngineCocoonService(client: mockClient);
final CommitStatus status = CommitStatus()
..commit = (Commit()..key = (RootKey()..child = (Key()..name = 'iamatestkey')));
await service.fetchCommitStatuses(lastCommitStatus: status);
if (kIsWeb) {
verify(mockClient.get('/api/public/get-status?lastCommitKey=iamatestkey&branch=master'));
} else {
verify(mockClient.get('$baseApiUrl/api/public/get-status?lastCommitKey=iamatestkey&branch=master'));
}
});
test('should have error if given non-200 response', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async => Response('', 404)));
final CocoonResponse<List<CommitStatus>> response = await service.fetchCommitStatuses();
expect(response.error, isNotNull);
});
test('should have error if given bad response', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async => Response('bad', 200)));
final CocoonResponse<List<CommitStatus>> response = await service.fetchCommitStatuses();
expect(response.error, isNotNull);
});
});
group('AppEngine CocoonService fetchCommitStatus - luci', () {
AppEngineCocoonService service;
setUp(() async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
return Response(luciJsonGetStatsResponse, 200);
}));
});
test('should return expected List<CommitStatus> - luci', () async {
final CocoonResponse<List<CommitStatus>> statuses = await service.fetchCommitStatuses();
final CommitStatus expectedStatus = CommitStatus()
..branch = 'master'
..commit = (Commit()
..timestamp = Int64(123456789)
..key = (RootKey()..child = (Key()..name = 'iamatestkey'))
..sha = 'ShaShankHash'
..author = 'ShaSha'
..authorAvatarUrl = 'https://flutter.dev'
..repository = 'flutter/cocoon'
..branch = 'master')
..stages.add(Stage()
..name = 'chromebot'
..taskStatus = 'Succeeded'
..tasks.add(Task()
..key = (RootKey()..child = (Key()..name = 'taskKey1'))
..createTimestamp = Int64(1569353940885)
..startTimestamp = Int64(1569354594672)
..endTimestamp = Int64(1569354700642)
..name = 'linux'
..attempts = 1
..isFlaky = false
..timeoutInMinutes = 0
..reason = ''
..requiredCapabilities.add('[linux]')
..reservedForAgentId = ''
..stageName = 'chromebot'
..status = 'Succeeded'
..buildNumberList = '123'
..builderName = 'Linux'
..luciBucket = 'luci.flutter.try'));
expect(statuses.data.length, 1);
expect(statuses.data.first, expectedStatus);
});
});
group('AppEngine CocoonService fetchTreeBuildStatus', () {
AppEngineCocoonService service;
setUp(() async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
return Response(jsonBuildStatusTrueResponse, 200);
}));
});
test('should return CocoonResponse<bool>', () {
expect(service.fetchTreeBuildStatus(), const TypeMatcher<Future<CocoonResponse<bool>>>());
});
test('data should be true when given Succeeded', () async {
final CocoonResponse<bool> treeBuildStatus = await service.fetchTreeBuildStatus();
expect(treeBuildStatus.data, true);
});
test('data should be false when given Failed', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
return Response(jsonBuildStatusFalseResponse, 200);
}));
final CocoonResponse<bool> treeBuildStatus = await service.fetchTreeBuildStatus();
expect(treeBuildStatus.data, false);
});
/// This requires a separate test run on the web platform.
test('should query correct endpoint whether web or mobile', () async {
final Client mockClient = MockHttpClient();
when(mockClient.get(any)).thenAnswer((_) => Future<Response>.value(Response('', 200)));
service = AppEngineCocoonService(client: mockClient);
await service.fetchTreeBuildStatus();
if (kIsWeb) {
verify(mockClient.get('/api/public/build-status?branch=master'));
} else {
verify(mockClient.get('$baseApiUrl/api/public/build-status?branch=master'));
}
});
/// This requires a separate test run on the web platform.
test('should query correct endpoint when given a specific branch', () async {
final Client mockClient = MockHttpClient();
when(mockClient.get(any)).thenAnswer((_) => Future<Response>.value(Response('', 200)));
service = AppEngineCocoonService(client: mockClient);
await service.fetchTreeBuildStatus(branch: 'stable');
if (kIsWeb) {
verify(mockClient.get('/api/public/build-status?branch=stable'));
} else {
verify(mockClient.get('$baseApiUrl/api/public/build-status?branch=stable'));
}
});
test('should have error if given non-200 response', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async => Response('', 404)));
final CocoonResponse<bool> response = await service.fetchTreeBuildStatus();
expect(response.error, isNotNull);
});
test('should have error if given bad response', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async => Response('bad', 200)));
final CocoonResponse<bool> response = await service.fetchTreeBuildStatus();
expect(response.error, isNotNull);
});
});
group('AppEngine CocoonService rerun task', () {
AppEngineCocoonService service;
setUp(() {
service = AppEngineCocoonService(client: MockClient((Request request) async {
return Response('', 200);
}));
});
test('should return true if request succeeds', () async {
expect(await service.rerunTask(Task()..key = RootKey(), 'fakeAccessToken'), true);
});
test('should return false if request failed', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
return Response('', 500);
}));
expect(await service.rerunTask(Task()..key = RootKey(), 'fakeAccessToken'), false);
});
test('should return false if task key is null', () async {
expect(service.rerunTask(Task(), null), throwsA(const TypeMatcher<AssertionError>()));
});
/// This requires a separate test run on the web platform.
test('should query correct endpoint whether web or mobile', () async {
final Client mockClient = MockHttpClient();
when(mockClient.post(argThat(endsWith('/api/reset-devicelab-task')),
headers: captureAnyNamed('headers'), body: captureAnyNamed('body')))
.thenAnswer((_) => Future<Response>.value(Response('', 200)));
service = AppEngineCocoonService(client: mockClient);
await service.rerunTask(Task(), '');
if (kIsWeb) {
verify(mockClient.post(
'/api/reset-devicelab-task',
headers: captureAnyNamed('headers'),
body: captureAnyNamed('body'),
));
} else {
verify(mockClient.post(
'$baseApiUrl/api/reset-devicelab-task',
headers: captureAnyNamed('headers'),
body: captureAnyNamed('body'),
));
}
});
});
group('AppEngine CocoonService download log', () {
AppEngineCocoonService service;
setUp(() {
service = AppEngineCocoonService(client: MockClient((Request request) async {
return Response('', 200);
}));
});
test('should throw assertion error if task is null', () async {
expect(service.downloadLog(null, 'abc123', 'shashank'), throwsA(const TypeMatcher<AssertionError>()));
});
test('should throw assertion error if id token is null', () async {
expect(
service.downloadLog(Task()..key = RootKey(), null, 'shashank'), throwsA(const TypeMatcher<AssertionError>()));
});
test('should send correct request to downloader service', () async {
final Downloader mockDownloader = MockDownloader();
when(mockDownloader.download(argThat(contains('/api/get-log?ownerKey')), 'test_task_shashan_1.log',
idToken: 'abc123'))
.thenAnswer((_) => Future<bool>.value(true));
service = AppEngineCocoonService(
client: MockClient((Request request) async {
return Response('', 200);
}),
downloader: mockDownloader);
expect(
await service.downloadLog(
Task()
..name = 'test_task'
..attempts = 1,
'abc123',
'shashankabcdefghijklmno'),
isTrue);
});
});
group('AppEngine CocoonService fetchAgentStatus ', () {
AppEngineCocoonService service;
setUp(() async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
return Response(jsonGetStatsResponse, 200);
}));
});
test('should return CocoonResponse<List<Agent>>', () {
expect(service.fetchAgentStatuses(), const TypeMatcher<Future<CocoonResponse<List<Agent>>>>());
});
test('should return expected List<Agent>', () async {
final CocoonResponse<List<Agent>> agents = await service.fetchAgentStatuses();
final List<Agent> expectedAgents = <Agent>[
Agent()
..agentId = 'flutter-devicelab-linux-1'
..healthCheckTimestamp = Int64.parseInt('1576876008093')
..isHealthy = true
..capabilities.addAll(<String>['linux/android', 'linux'])
..healthDetails = 'ssh-connectivity: succeeded\n'
' Last known IP address: 192.168.1.29\n'
'\n'
'android-device-ZY223D6B7B: succeeded\n'
'has-healthy-devices: succeeded\n'
' Found 1 healthy devices\n'
'\n'
'cocoon-authentication: succeeded\n'
'cocoon-connection: succeeded\n'
'able-to-perform-health-check: succeeded\n',
Agent()
..agentId = 'flutter-devicelab-mac-1'
..healthCheckTimestamp = Int64.parseInt('1576530583142')
..isHealthy = true
..capabilities.addAll(<String>['mac/ios', 'mac'])
..healthDetails = 'ssh-connectivity: succeeded\n'
' Last known IP address: 192.168.1.233\n'
'\n'
'ios-device-43ad2fda7991b34fe1acbda82f9e2fd3d6ddc9f7: succeeded\n'
'has-healthy-devices: succeeded\n'
' Found 1 healthy devices\n'
'\n'
'cocoon-authentication: succeeded\n'
'cocoon-connection: succeeded\n'
'able-to-build-and-sign: succeeded\n'
'ios: succeeded\n'
'able-to-perform-health-check: succeeded\n'
];
expect(agents.data, expectedAgents);
expect(agents.error, isNull);
});
/// This requires a separate test run on the web platform.
test('should query correct endpoint whether web or mobile', () async {
final Client mockClient = MockHttpClient();
when(mockClient.get(any)).thenAnswer((_) => Future<Response>.value(Response('', 200)));
service = AppEngineCocoonService(client: mockClient);
await service.fetchAgentStatuses();
if (kIsWeb) {
verify(mockClient.get('/api/public/get-status'));
} else {
verify(mockClient.get('$baseApiUrl/api/public/get-status'));
}
});
test('should have error if given non-200 response', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async => Response('', 404)));
final CocoonResponse<List<Agent>> response = await service.fetchAgentStatuses();
expect(response.error, isNotNull);
});
test('should have error if given bad response', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async => Response('bad', 200)));
final CocoonResponse<List<Agent>> response = await service.fetchAgentStatuses();
expect(response.error, isNotNull);
});
});
group('AppEngine CocoonService fetchFlutterBranches', () {
AppEngineCocoonService service;
setUp(() async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
return Response(jsonGetBranchesResponse, 200);
}));
});
test('should return CocoonResponse<List<String>>', () {
expect(service.fetchFlutterBranches(), const TypeMatcher<Future<CocoonResponse<List<String>>>>());
});
test('data should be expected list of branches', () async {
final CocoonResponse<List<String>> branches = await service.fetchFlutterBranches();
expect(branches.data, <String>[
'master',
'flutter-0.0-candidate.1',
]);
});
/// This requires a separate test run on the web platform.
test('should query correct endpoint whether web or mobile', () async {
final Client mockClient = MockHttpClient();
when(mockClient.get(any)).thenAnswer((_) => Future<Response>.value(Response('', 200)));
service = AppEngineCocoonService(client: mockClient);
await service.fetchFlutterBranches();
if (kIsWeb) {
verify(mockClient.get('/api/public/get-branches'));
} else {
verify(mockClient.get('$baseApiUrl/api/public/get-branches'));
}
});
test('should have error if given non-200 response', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async => Response('', 404)));
final CocoonResponse<List<String>> response = await service.fetchFlutterBranches();
expect(response.error, isNotNull);
});
test('should have error if given bad response', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async => Response('bad', 200)));
final CocoonResponse<List<String>> response = await service.fetchFlutterBranches();
expect(response.error, isNotNull);
});
});
group('AppEngine CocoonService create agent', () {
AppEngineCocoonService service;
Response fakeResponse;
setUp(() {
service = AppEngineCocoonService(client: MockClient((Request request) async => fakeResponse));
});
test('should return token if request succeeds', () async {
fakeResponse = Response('{"Token": "abc123"}', 200);
final CocoonResponse<String> response = await service.createAgent(
'id123',
<String>['im', 'capable'],
'fakeAccessToken',
);
expect(response.data, 'abc123');
expect(response.error, isNull);
});
test('should return error if request failed', () async {
fakeResponse = Response('', 500);
expect((await service.createAgent('id123', <String>['im', 'not', 'capable'], 'fakeAccessToken')).error,
'/api/create-agent did not respond with 200');
});
test('should return error if token is null', () async {
fakeResponse = Response('', 200);
expect((await service.createAgent('id123', <String>['im', 'capable'], 'fakeAccessToken')).error,
'/api/create-agent returned unexpected response');
});
/// This requires a separate test run on the web platform.
test('should query correct endpoint whether web or mobile', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
expect(request.url.toString(), kIsWeb ? '/api/create-agent' : '$baseApiUrl/api/create-agent');
return Response('', 200);
}));
await service.createAgent('id123', <String>['none'], 'fakeAccessToken');
});
test('should send correct headers and body', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
expect(request.headers, <String, String>{
'X-Flutter-IdToken': 'fakeAccessToken',
'content-type': 'text/plain; charset=utf-8',
});
expect(request.body, '{"AgentID":"id123","Capabilities":["none"]}');
return Response('', 200);
}));
await service.createAgent('id123', <String>['none'], 'fakeAccessToken');
});
});
group('AppEngine CocoonService authorize agent', () {
AppEngineCocoonService service;
Response fakeResponse;
setUp(() {
service = AppEngineCocoonService(client: MockClient((Request request) async => fakeResponse));
});
test('should return token if request succeeds', () async {
fakeResponse = Response('{"Token": "abc123"}', 200);
final CocoonResponse<String> response = await service.authorizeAgent(
Agent()..agentId = 'id123',
'fakeAccessToken',
);
expect(response.data, 'abc123');
expect(response.error, isNull);
});
test('should return error if request failed', () async {
fakeResponse = Response('', 500);
expect((await service.authorizeAgent(Agent()..agentId = 'id123', 'fakeAccessToken')).error,
'/api/authorize-agent did not respond with 200');
});
test('should return error if token is null', () async {
fakeResponse = Response('', 200);
expect((await service.authorizeAgent(Agent()..agentId = 'id123', 'fakeAccessToken')).error,
'/api/authorize-agent returned unexpected response');
});
/// This requires a separate test run on the web platform.
test('should query correct endpoint whether web or mobile', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
expect(request.url.toString(), kIsWeb ? '/api/authorize-agent' : '$baseApiUrl/api/authorize-agent');
return Response('', 200);
}));
await service.authorizeAgent(Agent()..agentId = 'id123', 'fakeAccessToken');
});
test('should send correct headers and body', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
expect(request.headers, <String, String>{
'X-Flutter-IdToken': 'fakeAccessToken',
'content-type': 'text/plain; charset=utf-8',
});
expect(request.body, '{"AgentID":"id123"}');
return Response('', 200);
}));
await service.authorizeAgent(Agent()..agentId = 'id123', 'fakeAccessToken');
});
});
group('AppEngine CocoonService reserve task', () {
AppEngineCocoonService service;
Response fakeResponse;
setUp(() {
service = AppEngineCocoonService(client: MockClient((Request request) async => fakeResponse));
});
test('should not throw exception if request succeeds', () async {
fakeResponse = Response('{"Task": "randomdata"}', 200);
await service.reserveTask(Agent()..agentId = 'id123', 'fakeAccessToken');
});
test('should throw error if request failed', () async {
fakeResponse = Response('', 500);
expect(
service.reserveTask(Agent()..agentId = 'id123', 'fakeAccessToken'), throwsA(const TypeMatcher<Exception>()));
});
test('should throw error if task is null', () async {
fakeResponse = Response('', 200);
expect(
service.reserveTask(Agent()..agentId = 'id123', 'fakeAccessToken'), throwsA(const TypeMatcher<Exception>()));
});
/// This requires a separate test run on the web platform.
test('should query correct endpoint whether web or mobile', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
expect(request.url.toString(), kIsWeb ? '/api/reserve-task' : '$baseApiUrl/api/reserve-task');
return Response('{"Task": "randomdata"}', 200);
}));
service.reserveTask(Agent()..agentId = 'id123', 'fakeAccessToken');
});
test('should send correct headers and body', () async {
service = AppEngineCocoonService(client: MockClient((Request request) async {
expect(request.headers, <String, String>{
'X-Flutter-IdToken': 'fakeAccessToken',
'content-type': 'text/plain; charset=utf-8',
});
expect(request.body, '{"AgentID":"id123"}');
return Response('{"Task": "randomdata"}', 200);
}));
service.reserveTask(Agent()..agentId = 'id123', 'fakeAccessToken');
});
});
group('AppEngine CocoonService apiEndpoint', () {
AppEngineCocoonService service;
setUp(() {
service = AppEngineCocoonService(client: MockClient((Request request) async {
return Response('{"Token": "abc123"}', 200);
}));
});
test('handles url suffix', () {
expect(service.apiEndpoint('/test'), '$baseApiUrl/test');
});
test('single query parameter', () {
expect(service.apiEndpoint('/test', queryParameters: <String, String>{'key': 'value'}),
'$baseApiUrl/test?key=value');
});
test('multiple query parameters', () {
expect(service.apiEndpoint('/test', queryParameters: <String, String>{'key': 'value', 'another': 'test'}),
'$baseApiUrl/test?key=value&another=test');
});
test('query parameter with null value', () {
expect(service.apiEndpoint('/test', queryParameters: <String, String>{'key': null}), '$baseApiUrl/test?key');
});
});
}
class MockHttpClient extends Mock implements Client {}
class MockDownloader extends Mock implements Downloader {}