blob: e87d94c0051e1734a32fea79d130d55590ffbab3 [file] [log] [blame]
// Copyright (c) 2019 The Chromium 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';
// This is based off data the Cocoon backend sends out from v1.
// It doesn't map directly to protos since the backend does
// not use protos yet.
const String jsonGetStatsResponse = '''
{
"Statuses": [
{
"Checklist": {
"Key": "iamatestkey",
"Checklist": {
"FlutterRepositoryPath": "flutter/cocoon",
"CreateTimestamp": 123456789,
"Commit": {
"Sha": "ShaShankHash",
"Author": {
"Login": "ShaSha",
"avatar_url": "https://flutter.dev"
}
}
}
},
"Stages": [
{
"Name": "devicelab",
"Status": "Succeeded",
"Tasks": [
{
"Key": "taskKey1",
"Task": {
"Attempts": 1,
"CreateTimestamp": 1569353940885,
"EndTimestamp": 1569354700642,
"Flaky": false,
"Name": "complex_layout_semantics_perf",
"Reason": "",
"RequiredCapabilities": ["linux/android"],
"ReservedForAgentID": "linux2",
"StageName": "devicelab",
"StartTimestamp": 1569354594672,
"Status": "Succeeded",
"TimeoutInMinutes": 0
}
}
]
}
]
}
],
"AgentStatuses":[
{
"AgentID":"flutter-devicelab-linux-1",
"HealthCheckTimestamp":1576876008093,
"IsHealthy":true,
"Capabilities":[
"linux/android",
"linux"
],
"HealthDetails":"ssh-connectivity: succeeded\\n Last known IP address: 192.168.1.29\\n\\nandroid-device-ZY223D6B7B: succeeded\\nhas-healthy-devices: succeeded\\n Found 1 healthy devices\\n\\ncocoon-authentication: succeeded\\ncocoon-connection: succeeded\\nable-to-perform-health-check: succeeded\\n"
},
{
"AgentID":"flutter-devicelab-mac-1",
"HealthCheckTimestamp":1576530583142,
"IsHealthy":true,
"Capabilities":[
"mac/ios",
"mac"
],
"HealthDetails":"ssh-connectivity: succeeded\\n Last known IP address: 192.168.1.233\\n\\nios-device-43ad2fda7991b34fe1acbda82f9e2fd3d6ddc9f7: succeeded\\nhas-healthy-devices: succeeded\\n Found 1 healthy devices\\n\\ncocoon-authentication: succeeded\\ncocoon-connection: succeeded\\nable-to-build-and-sign: succeeded\\nios: succeeded\\nable-to-perform-health-check: succeeded\\n"
}
]
}
''';
const String jsonBuildStatusTrueResponse = '''
{
"AnticipatedBuildStatus": "Succeeded"
}
''';
const String jsonBuildStatusFalseResponse = '''
{
"AnticipatedBuildStatus": "Failed"
}
''';
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()
..commit = (Commit()
..timestamp = Int64(123456789)
..key = (RootKey()..child = (Key()..name = 'iamatestkey'))
..sha = 'ShaShankHash'
..author = 'ShaSha'
..authorAvatarUrl = 'https://flutter.dev'
..repository = 'flutter/cocoon')
..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'));
} else {
verify(mockClient.get(
'https://flutter-dashboard.appspot.com/api/public/get-status'));
}
});
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'));
} else {
verify(mockClient.get(
'https://flutter-dashboard.appspot.com/api/public/get-status?lastCommitKey=iamatestkey'));
}
});
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 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'));
} else {
verify(mockClient.get(
'https://flutter-dashboard.appspot.com/api/public/build-status'));
}
});
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 Cocoon Service 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(
'https://flutter-dashboard.appspot.com/api/reset-devicelab-task',
headers: captureAnyNamed('headers'),
body: captureAnyNamed('body'),
));
}
});
});
group('AppEngine Cocoon Service 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\nandroid-device-ZY223D6B7B: succeeded\nhas-healthy-devices: succeeded\n Found 1 healthy devices\n\ncocoon-authentication: succeeded\ncocoon-connection: succeeded\nable-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\nios-device-43ad2fda7991b34fe1acbda82f9e2fd3d6ddc9f7: succeeded\nhas-healthy-devices: succeeded\n Found 1 healthy devices\n\ncocoon-authentication: succeeded\ncocoon-connection: succeeded\nable-to-build-and-sign: succeeded\nios: succeeded\nable-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(
'https://flutter-dashboard.appspot.com/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 Cocoon Service create agent', () {
AppEngineCocoonService service;
setUp(() {
service =
AppEngineCocoonService(client: MockClient((Request request) async {
return Response('{"Token": "abc123"}', 200);
}));
});
test('should return token if request succeeds', () async {
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 {
service =
AppEngineCocoonService(client: MockClient((Request request) async {
return 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 {
service =
AppEngineCocoonService(client: MockClient((Request request) async {
return 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 {
final Client mockClient = MockHttpClient();
when(mockClient.post(argThat(endsWith('/api/create-agent')),
headers: captureAnyNamed('headers'),
body: captureAnyNamed('body')))
.thenAnswer((_) => Future<Response>.value(Response('', 200)));
service = AppEngineCocoonService(client: mockClient);
await service.createAgent('id123', <String>['none'], 'fakeAccessToken');
if (kIsWeb) {
verify(mockClient.post(
'/api/create-agent',
headers: captureAnyNamed('headers'),
body: captureAnyNamed('body'),
));
} else {
verify(mockClient.post(
'https://flutter-dashboard.appspot.com/api/create-agent',
headers: captureAnyNamed('headers'),
body: captureAnyNamed('body'),
));
}
});
});
group('AppEngine Cocoon Service authorize agent', () {
AppEngineCocoonService service;
setUp(() {
service =
AppEngineCocoonService(client: MockClient((Request request) async {
return Response('{"Token": "abc123"}', 200);
}));
});
test('should return token if request succeeds', () async {
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 {
service =
AppEngineCocoonService(client: MockClient((Request request) async {
return 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 {
service =
AppEngineCocoonService(client: MockClient((Request request) async {
return 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 {
final Client mockClient = MockHttpClient();
when(mockClient.post(argThat(endsWith('/api/authorize-agent')),
headers: captureAnyNamed('headers'),
body: captureAnyNamed('body')))
.thenAnswer((_) => Future<Response>.value(Response('', 200)));
service = AppEngineCocoonService(client: mockClient);
await service.authorizeAgent(
Agent()..agentId = 'id123', 'fakeAccessToken');
if (kIsWeb) {
verify(mockClient.post(
'/api/authorize-agent',
headers: captureAnyNamed('headers'),
body: captureAnyNamed('body'),
));
} else {
verify(mockClient.post(
'https://flutter-dashboard.appspot.com/api/authorize-agent',
headers: captureAnyNamed('headers'),
body: captureAnyNamed('body'),
));
}
});
});
group('AppEngine Cocoon Service reserve task', () {
AppEngineCocoonService service;
setUp(() {
service =
AppEngineCocoonService(client: MockClient((Request request) async {
return Response('{"Task": "randomdata"}', 200);
}));
});
test('should not throw exception if request succeeds', () async {
await service.reserveTask(Agent()..agentId = 'id123', 'fakeAccessToken');
});
test('should throw error if request failed', () async {
service =
AppEngineCocoonService(client: MockClient((Request request) async {
return Response('', 500);
}));
expect(service.reserveTask(Agent()..agentId = 'id123', 'fakeAccessToken'),
throwsA(const TypeMatcher<Exception>()));
});
test('should throw error if task is null', () async {
service =
AppEngineCocoonService(client: MockClient((Request request) async {
return 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 {
final Client mockClient = MockHttpClient();
when(mockClient.post(argThat(endsWith('/api/reserve-task')),
headers: captureAnyNamed('headers'),
body: captureAnyNamed('body')))
.thenAnswer((_) =>
Future<Response>.value(Response('{"Task": "randomdata"}', 200)));
service = AppEngineCocoonService(client: mockClient);
await service.reserveTask(Agent()..agentId = 'id123', 'fakeAccessToken');
if (kIsWeb) {
verify(mockClient.post(
'/api/reserve-task',
headers: captureAnyNamed('headers'),
body: captureAnyNamed('body'),
));
} else {
verify(mockClient.post(
'https://flutter-dashboard.appspot.com/api/reserve-task',
headers: captureAnyNamed('headers'),
body: captureAnyNamed('body'),
));
}
});
});
}
class MockHttpClient extends Mock implements Client {}
class MockDownloader extends Mock implements Downloader {}