From cec1bf5b6e9c980751385dbf046eb8d119c94ae2 Mon Sep 17 00:00:00 2001 From: Bryan Oltman Date: Tue, 15 Oct 2024 14:49:53 -0400 Subject: [PATCH] feat: add URL reachability checks to shorebird doctor (#2537) --- packages/shorebird_cli/bin/shorebird.dart | 2 + .../lib/src/commands/doctor_command.dart | 6 ++ .../lib/src/network_checker.dart | 37 +++++++++ .../src/commands/doctor_command_test.dart | 14 +++- packages/shorebird_cli/test/src/mocks.dart | 3 + .../test/src/network_checker_test.dart | 79 +++++++++++++++++++ 6 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 packages/shorebird_cli/lib/src/network_checker.dart create mode 100644 packages/shorebird_cli/test/src/network_checker_test.dart diff --git a/packages/shorebird_cli/bin/shorebird.dart b/packages/shorebird_cli/bin/shorebird.dart index f59f1fa3..da71007a 100644 --- a/packages/shorebird_cli/bin/shorebird.dart +++ b/packages/shorebird_cli/bin/shorebird.dart @@ -15,6 +15,7 @@ import 'package:shorebird_cli/src/engine_config.dart'; import 'package:shorebird_cli/src/executables/executables.dart'; import 'package:shorebird_cli/src/http_client/http_client.dart'; import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/network_checker.dart'; import 'package:shorebird_cli/src/os/os.dart'; import 'package:shorebird_cli/src/patch_diff_checker.dart'; import 'package:shorebird_cli/src/platform.dart'; @@ -63,6 +64,7 @@ Future main(List args) async { iosRef, javaRef, loggerRef, + networkCheckerRef, osInterfaceRef, patchExecutableRef, patchDiffCheckerRef, diff --git a/packages/shorebird_cli/lib/src/commands/doctor_command.dart b/packages/shorebird_cli/lib/src/commands/doctor_command.dart index f1855bc3..d8076f43 100644 --- a/packages/shorebird_cli/lib/src/commands/doctor_command.dart +++ b/packages/shorebird_cli/lib/src/commands/doctor_command.dart @@ -6,6 +6,7 @@ import 'package:shorebird_cli/src/android_studio.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/executables/executables.dart'; import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/network_checker.dart'; import 'package:shorebird_cli/src/shorebird_command.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; import 'package:shorebird_cli/src/shorebird_flutter.dart'; @@ -94,6 +95,11 @@ Android Toolchain logger.info(output.toString()); + // ignore: cascade_invocations + logger.info('URL reachability:'); + await networkChecker.checkReachability(); + logger.info(''); + await doctor.runValidators(doctor.generalValidators, applyFixes: shouldFix); return ExitCode.success.code; diff --git a/packages/shorebird_cli/lib/src/network_checker.dart b/packages/shorebird_cli/lib/src/network_checker.dart new file mode 100644 index 00000000..ea8e9804 --- /dev/null +++ b/packages/shorebird_cli/lib/src/network_checker.dart @@ -0,0 +1,37 @@ +import 'package:scoped_deps/scoped_deps.dart'; +import 'package:shorebird_cli/src/http_client/http_client.dart'; +import 'package:shorebird_cli/src/logger.dart'; + +/// A reference to a [NetworkChecker] instance. +final networkCheckerRef = create(NetworkChecker.new); + +/// The [NetworkChecker] instance available in the current zone. +NetworkChecker get networkChecker => read(networkCheckerRef); + +/// {@template network_checker} +/// Checks reachability of various Shorebird-related endpoints and logs the +/// results. +/// {@endtemplate} +class NetworkChecker { + /// The URLs to check for network reachability. + static final urlsToCheck = [ + 'https://api.shorebird.dev', + 'https://console.shorebird.dev', + 'https://oauth2.googleapis.com', + ].map(Uri.parse).toList(); + + /// Verify that each of [urlsToCheck] responds to an HTTP GET request. + Future checkReachability() async { + for (final url in urlsToCheck) { + final progress = logger.progress('Checking reachability of $url'); + + try { + await httpClient.get(url); + progress.complete('$url OK'); + } catch (e) { + progress.fail('$url unreachable'); + logger.detail('Failed to reach $url: $e'); + } + } + } +} diff --git a/packages/shorebird_cli/test/src/commands/doctor_command_test.dart b/packages/shorebird_cli/test/src/commands/doctor_command_test.dart index f03614e7..e570c83f 100644 --- a/packages/shorebird_cli/test/src/commands/doctor_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/doctor_command_test.dart @@ -10,6 +10,7 @@ import 'package:shorebird_cli/src/commands/commands.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/executables/executables.dart'; import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/network_checker.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; import 'package:shorebird_cli/src/shorebird_flutter.dart'; import 'package:shorebird_cli/src/validators/validators.dart'; @@ -30,6 +31,7 @@ void main() { late Doctor doctor; late Gradlew gradlew; late Java java; + late NetworkChecker networkChecker; late ShorebirdLogger logger; late ShorebirdEnv shorebirdEnv; late ShorebirdFlutter shorebirdFlutter; @@ -46,6 +48,7 @@ void main() { gradlewRef.overrideWith(() => gradlew), javaRef.overrideWith(() => java), loggerRef.overrideWith(() => logger), + networkCheckerRef.overrideWith(() => networkChecker), shorebirdEnvRef.overrideWith(() => shorebirdEnv), shorebirdFlutterRef.overrideWith(() => shorebirdFlutter), }, @@ -61,6 +64,7 @@ void main() { logsDirectory = Directory.systemTemp.createTempSync('shorebird_logs'); java = MockJava(); logger = MockShorebirdLogger(); + networkChecker = MockNetworkChecker(); shorebirdEnv = MockShorebirdEnv(); shorebirdFlutter = MockShorebirdFlutter(); validator = MockValidator(); @@ -72,6 +76,9 @@ void main() { when(() => androidSdk.adbPath).thenReturn(null); when(() => gradlew.exists(any())).thenReturn(false); when(() => java.home).thenReturn(null); + when( + () => networkChecker.checkReachability(), + ).thenAnswer((_) async => {}); when( () => shorebirdEnv.shorebirdEngineRevision, ).thenReturn(shorebirdEngineRevision); @@ -104,8 +111,8 @@ Engine • revision $shorebirdEngineRevision }); test( - 'prints shorebird version, flutter revision, ' - 'flutter version, and engine revision', () async { + '''prints shorebird version, flutter revision, flutter version, and engine revision''', + () async { const flutterVersion = '1.2.3'; when( () => shorebirdFlutter.getVersionString(), @@ -120,6 +127,7 @@ Flutter $flutterVersion • revision ${shorebirdEnv.flutterRevision} Engine • revision $shorebirdEngineRevision '''), ).called(1); + verify(() => networkChecker.checkReachability()).called(1); }); group('--verbose', () { @@ -197,6 +205,8 @@ Android Toolchain ''', ), ); + + verify(() => networkChecker.checkReachability()).called(1); }); group('when a gradlew executable exists', () { diff --git a/packages/shorebird_cli/test/src/mocks.dart b/packages/shorebird_cli/test/src/mocks.dart index 522a2c8c..0732903c 100644 --- a/packages/shorebird_cli/test/src/mocks.dart +++ b/packages/shorebird_cli/test/src/mocks.dart @@ -26,6 +26,7 @@ import 'package:shorebird_cli/src/engine_config.dart'; import 'package:shorebird_cli/src/executables/devicectl/apple_device.dart'; import 'package:shorebird_cli/src/executables/executables.dart'; import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/network_checker.dart'; import 'package:shorebird_cli/src/os/os.dart'; import 'package:shorebird_cli/src/patch_diff_checker.dart'; import 'package:shorebird_cli/src/platform/platform.dart'; @@ -112,6 +113,8 @@ class MockJwtHeader extends Mock implements JwtHeader {} class MockJwtPayload extends Mock implements JwtPayload {} +class MockNetworkChecker extends Mock implements NetworkChecker {} + class MockOperatingSystemInterface extends Mock implements OperatingSystemInterface {} diff --git a/packages/shorebird_cli/test/src/network_checker_test.dart b/packages/shorebird_cli/test/src/network_checker_test.dart new file mode 100644 index 00000000..19a4a093 --- /dev/null +++ b/packages/shorebird_cli/test/src/network_checker_test.dart @@ -0,0 +1,79 @@ +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:scoped_deps/scoped_deps.dart'; +import 'package:shorebird_cli/src/http_client/http_client.dart'; +import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/network_checker.dart'; +import 'package:test/test.dart'; + +import 'fakes.dart'; +import 'mocks.dart'; + +void main() { + group(NetworkChecker, () { + late http.Client httpClient; + late ShorebirdLogger logger; + late Progress progress; + late NetworkChecker networkChecker; + + R runWithOverrides(R Function() body) { + return runScoped( + () => body(), + values: { + httpClientRef.overrideWith(() => httpClient), + loggerRef.overrideWith(() => logger), + }, + ); + } + + setUpAll(() { + registerFallbackValue(FakeBaseRequest()); + registerFallbackValue(Uri()); + }); + + setUp(() { + httpClient = MockHttpClient(); + logger = MockShorebirdLogger(); + progress = MockProgress(); + + when(() => logger.progress(any())).thenReturn(progress); + + networkChecker = NetworkChecker(); + }); + + group('checkReachability', () { + group('when endpoints are reachable', () { + setUp(() { + when(() => httpClient.get(any())).thenAnswer( + (_) async => http.Response('', HttpStatus.ok), + ); + }); + + test('logs reachability for each checked url', () async { + await runWithOverrides(networkChecker.checkReachability); + + verify( + () => progress.complete(any(that: contains('OK'))), + ).called(NetworkChecker.urlsToCheck.length); + }); + }); + + group('when endpoints are not reachable', () { + setUp(() { + when(() => httpClient.send(any())).thenThrow(Exception('oops')); + }); + + test('logs reachability for each checked url', () async { + await runWithOverrides(networkChecker.checkReachability); + + verify( + () => progress.fail(any(that: contains('unreachable'))), + ).called(NetworkChecker.urlsToCheck.length); + }); + }); + }); + }); +}