Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions app_dart/bin/gae_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:io';
import 'dart:io' as io;

import 'package:appengine/appengine.dart';
import 'package:cocoon_server/google_auth_provider.dart';
Expand All @@ -20,6 +20,7 @@ import 'package:cocoon_service/src/service/firebase_jwt_validator.dart';
import 'package:cocoon_service/src/service/flags/dynamic_config_updater.dart';
import 'package:cocoon_service/src/service/get_files_changed.dart';
import 'package:cocoon_service/src/service/scheduler/ci_yaml_fetcher.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';

Future<void> main() async {
Expand Down Expand Up @@ -53,6 +54,7 @@ Future<void> main() async {
projectId: Config.flutterGcpProjectId,
),
initialConfig: dynamicConfig,
httpClient: MappingHttpClient(http.Client()),
);
// Start updating the config to loop forever. If this fails, it will log
// every ~1 minute.
Expand All @@ -75,12 +77,16 @@ Future<void> main() async {

final buildBucketClient = BuildBucketClient(
accessTokenService: AccessTokenService.defaultProvider(config),
httpClient: config.httpClient,
);

// Gerrit service class to communicate with GoB.
final gerritService = GerritService(
config: config,
authClient: await const GoogleAuthProvider().createClient(scopes: []),
authClient: await const GoogleAuthProvider().createClient(
baseClient: config.httpClient,
scopes: [],
),
);

/// LUCI service class to communicate with buildBucket service.
Expand Down Expand Up @@ -155,10 +161,10 @@ Future<void> main() async {
);

return runAppEngine(
(HttpRequest request) async {
(io.HttpRequest request) async {
await server(request.toRequest());
},
onAcceptingConnections: (InternetAddress address, int port) {
onAcceptingConnections: (io.InternetAddress address, int port) {
final host = address.isLoopback ? 'localhost' : address.host;
print('Serving requests at http://$host:$port/');
},
Expand Down
31 changes: 28 additions & 3 deletions app_dart/lib/src/request_handling/http_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import 'dart:io';
import 'dart:typed_data';

import 'package:cocoon_common/core_extensions.dart';
import 'package:http/http.dart' as http;

import 'http_utils.dart';
import 'http_utils.dart' as http_utils;
import 'request_handler.dart';

/// Creates a [Request] by wrapping an existing [HttpRequest].
Expand Down Expand Up @@ -74,7 +75,7 @@ final class _HttpResponse implements RequestResponse {
set statusCode(int value) => _response.statusCode = value;

@override
set contentType(MediaType? value) {
set contentType(http_utils.MediaType? value) {
if (value != null) {
_response.headers.contentType = ContentType(
value.type,
Expand All @@ -99,6 +100,30 @@ final class _HttpResponse implements RequestResponse {
@override
Future<dynamic> redirect(
Uri location, {
int status = HttpStatus.movedTemporarily,
int status = http_utils.HttpStatus.movedTemporarily,
}) => _response.redirect(location, status: status);
}

/// An [http.Client] that maps [SocketException] from dart:io to the cocoon
/// [http_utils.SocketException].
///
/// This is used to maintain platform neutrality in the core of app_dart.
class MappingHttpClient extends http.BaseClient {
MappingHttpClient(this._inner);

final http.Client _inner;

@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
try {
return await _inner.send(request);
} on SocketException catch (e) {
throw http_utils.SocketException('$e');
} on HttpException catch (e) {
throw http_utils.HttpException('$e');
}
Comment thread
jtmcdole marked this conversation as resolved.
}

@override
void close() => _inner.close();
}
22 changes: 19 additions & 3 deletions app_dart/lib/src/service/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ const String kDefaultBranchName = 'master';

interface class Config extends DynamicallyUpdatedConfig {
/// Creates and returns a [Config] instance.
Config(this._cache, this._secrets, {required super.initialConfig});
Config(
this._cache,
this._secrets, {
required super.initialConfig,
http.Client? httpClient,
}) : _httpClient = httpClient ?? http.Client();

/// When present on a pull request, instructs Cocoon to submit it
/// automatically as soon as all the required checks pass.
Expand Down Expand Up @@ -78,6 +83,10 @@ interface class Config extends DynamicallyUpdatedConfig {

final CacheService _cache;
final SecretManager _secrets;
final http.Client _httpClient;

/// The [http.Client] to use for requests.
http.Client get httpClient => _httpClient;

/// List of Github presubmit supported repos.
///
Expand Down Expand Up @@ -453,7 +462,10 @@ interface class Config extends DynamicallyUpdatedConfig {
'api.github.com',
'app/installations/$appInstallation/access_tokens',
);
final response = await http.post(githubAccessTokensUri, headers: headers);
final response = await _httpClient.post(
githubAccessTokensUri,
headers: headers,
);
final jsonBody = jsonDecode(response.body) as Map<String, dynamic>;
if (jsonBody.containsKey('token') == false) {
log.warn(response.body);
Expand All @@ -476,12 +488,16 @@ interface class Config extends DynamicallyUpdatedConfig {
}

gh.GitHub createGitHubClientWithToken(String token) {
return gh.GitHub(auth: gh.Authentication.withToken(token));
return gh.GitHub(
auth: gh.Authentication.withToken(token),
client: _httpClient,
);
}

Future<GraphQLClient> createGitHubGraphQLClient() async {
final httpLink = HttpLink(
'https://api.github.com/graphql',
httpClient: _httpClient,
defaultHeaders: <String, String>{
'Accept': 'application/vnd.github.antiope-preview+json',
},
Expand Down
73 changes: 73 additions & 0 deletions app_dart/test/request_handling/mapping_http_client_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2026 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:io' as io;

import 'package:cocoon_server_test/test_logging.dart';
import 'package:cocoon_service/src/request_handling/http_io.dart';
import 'package:cocoon_service/src/request_handling/http_utils.dart'
as cocoon_service;
import 'package:http/http.dart' as http;
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

import 'mapping_http_client_test.mocks.dart';

@GenerateMocks([http.Client])
void main() {
useTestLoggerPerTest();

group('MappingHttpClient', () {
late MockClient mockClient;
late MappingHttpClient mappingClient;

setUp(() {
mockClient = MockClient();
mappingClient = MappingHttpClient(mockClient);
});

test('maps io.SocketException to cocoon_service.SocketException', () async {
when(
mockClient.send(any),
).thenThrow(const io.SocketException('test message'));

expect(
() => mappingClient.get(Uri.parse('https://example.com')),
throwsA(
isA<cocoon_service.SocketException>().having(
(e) => e.message,
'message',
'SocketException: test message',
),
),
);
});

test('maps io.HttpException to cocoon_service.HttpException', () async {
when(
mockClient.send(any),
).thenThrow(const io.HttpException('test message'));

expect(
() => mappingClient.get(Uri.parse('https://example.com')),
throwsA(
isA<cocoon_service.HttpException>().having(
(e) => e.message,
'message',
'HttpException: test message',
),
),
);
});

test('passes through successful responses', () async {
final response = http.StreamedResponse(const Stream.empty(), 200);
when(mockClient.send(any)).thenAnswer((_) async => response);

final result = await mappingClient.get(Uri.parse('https://example.com'));
expect(result.statusCode, 200);
});
});
}
Loading
Loading