diff --git a/stronglift_replacement/workout_app/lib/services/http_server_service.dart b/stronglift_replacement/workout_app/lib/services/http_server_service.dart index d1ee043..5a2470b 100644 --- a/stronglift_replacement/workout_app/lib/services/http_server_service.dart +++ b/stronglift_replacement/workout_app/lib/services/http_server_service.dart @@ -26,6 +26,14 @@ class HttpServerService { HttpServer? _server; + /// Port [start] binds. Tests set 0 (ephemeral) so a host process that + /// already owns 8765 cannot make them fail or answer in our place. + @visibleForTesting + int port = kWorkoutServerPort; + + /// The port actually bound, or null while stopped. + int? get boundPort => _server?.port; + /// The most recent workout JSON string (updated after each finished workout). String? _latestJson; @@ -53,16 +61,13 @@ class HttpServerService { if (_server != null) return; // already running await _loadFromDisk(); try { - _server = await HttpServer.bind( - InternetAddress.anyIPv4, - kWorkoutServerPort, - ); + _server = await HttpServer.bind(InternetAddress.anyIPv4, port); unawaited(_serve()); } on SocketException catch (error) { // Port already in use or binding failed — not fatal. debugPrint( 'WorkoutApp: could not bind the workout HTTP server on port ' - '$kWorkoutServerPort ($error) — the PC cannot pull workouts over ' + '$port ($error) — the PC cannot pull workouts over ' 'HTTP; ADB and sync still work.', ); _server = null; diff --git a/stronglift_replacement/workout_app/test/services/http_server_service_test.dart b/stronglift_replacement/workout_app/test/services/http_server_service_test.dart index a0483b8..93c40ed 100644 --- a/stronglift_replacement/workout_app/test/services/http_server_service_test.dart +++ b/stronglift_replacement/workout_app/test/services/http_server_service_test.dart @@ -5,8 +5,11 @@ import 'package:workout_app/services/http_server_service.dart'; void main() { group('HttpServerService', () { + // Ephemeral port: the host may already run something on 8765. + setUp(() => HttpServerService.instance.port = 0); tearDown(() async { await HttpServerService.instance.stop(); + HttpServerService.instance.port = kWorkoutServerPort; }); test('is a singleton', () { @@ -44,22 +47,22 @@ void main() { }); test('start swallows a bind failure when the port is taken', () async { - final blocker = await ServerSocket.bind( - InternetAddress.anyIPv4, - kWorkoutServerPort, - ); + final blocker = await ServerSocket.bind(InternetAddress.anyIPv4, 0); + HttpServerService.instance.port = blocker.port; // Binding fails (SocketException) but start() must not throw. await HttpServerService.instance.start(); + expect(HttpServerService.instance.boundPort, isNull); await blocker.close(); }); test('serves the latest workout over HTTP', () async { HttpServerService.instance.resetForTesting(); await HttpServerService.instance.start(); + final port = HttpServerService.instance.boundPort!; final client = HttpClient(); Future get(String path) async { - final req = await client.get('localhost', kWorkoutServerPort, path); + final req = await client.get('localhost', port, path); final resp = await req.close(); await resp.drain(); return resp.statusCode;