diff --git a/app/lib/desktop/wrapper_server.dart b/app/lib/desktop/wrapper_server.dart index 220f26f..25c9ddb 100644 --- a/app/lib/desktop/wrapper_server.dart +++ b/app/lib/desktop/wrapper_server.dart @@ -35,7 +35,20 @@ class WrapperServer { required this.webRoot, required this.dataDir, required this.gitHubProxy, - }); + bool? serveSyncAccount, + String? syncConfigDir, + }) : serveSyncAccount = + serveSyncAccount ?? + (Platform.environment[kSyncAccountEnvVar] ?? '').isNotEmpty, + syncConfigDir = + syncConfigDir ?? + p.join(Platform.environment['HOME'] ?? '', '.config', 'crdt-sync'); + + /// Whether the sync-account route answers; off unless explicitly enabled. + final bool serveSyncAccount; + + /// Directory holding `firebase.json` and `password`. + final String syncConfigDir; /// Directory holding the built Flutter web assets. final String webRoot; @@ -114,17 +127,12 @@ class WrapperServer { /// the same reasoning that keeps the GitHub token in [gitHubProxy] instead /// of the browser. Future _syncAccount(HttpRequest request) async { - if ((Platform.environment[kSyncAccountEnvVar] ?? '').isEmpty) { + if (!serveSyncAccount) { request.response.statusCode = HttpStatus.notFound; return; } - final configDir = p.join( - Platform.environment['HOME'] ?? '', - '.config', - 'crdt-sync', - ); - final configFile = File(p.join(configDir, 'firebase.json')); - final passwordFile = File(p.join(configDir, 'password')); + final configFile = File(p.join(syncConfigDir, 'firebase.json')); + final passwordFile = File(p.join(syncConfigDir, 'password')); if (!configFile.existsSync() || !passwordFile.existsSync()) { stderr.writeln( 'Sync account requested but ~/.config/crdt-sync/ is incomplete — ' diff --git a/app/test/desktop/wrapper_server_test.dart b/app/test/desktop/wrapper_server_test.dart index b2039bf..a083ca7 100644 --- a/app/test/desktop/wrapper_server_test.dart +++ b/app/test/desktop/wrapper_server_test.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'package:diet_guard_app/desktop/github_proxy.dart'; import 'package:diet_guard_app/desktop/wrapper_server.dart'; +import 'package:crdt_sync/crdt_sync.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; @@ -20,7 +21,10 @@ void main() { late List outbound; late Map canned; - Future startServer() async { + Future startServer({ + bool serveSyncAccount = false, + String? syncConfigDir, + }) async { outbound = []; canned = {}; final proxy = GitHubProxy( @@ -36,6 +40,8 @@ void main() { webRoot: webRoot, dataDir: dataDir, gitHubProxy: proxy, + serveSyncAccount: serveSyncAccount, + syncConfigDir: syncConfigDir, ); await server.start(0); } @@ -285,4 +291,59 @@ void main() { expect(typeOf('data.bin').mimeType, 'application/octet-stream'); }); }); + + group('sync-account provisioning', () { + /// Writes [files] into a temp config dir and restarts with the route on. + Future enableWith(Map files) async { + final configDir = Directory(p.join(tempDir.path, 'crdt-sync')) + ..createSync(recursive: true); + files.forEach((name, contents) { + File(p.join(configDir.path, name)).writeAsStringSync(contents); + }); + await server.stop(); + await startServer(serveSyncAccount: true, syncConfigDir: configDir.path); + return 'http://localhost:${server.port}'; + } + + test('is 404 when not enabled', () async { + // The default, and the whole security posture: a credential route must + // not be reachable just because the wrapper is running. + final response = await http.get(url(kSyncAccountPath)); + + expect(response.statusCode, HttpStatus.notFound); + }); + + test('serves the account when enabled', () async { + final origin = await enableWith({ + 'firebase.json': '{"email":"a@b.c"}', + 'password': 'pw\n', + }); + + final response = await http.get(Uri.parse('$origin$kSyncAccountPath')); + final account = FirebaseAccount.tryParse(response.body); + + expect(response.statusCode, HttpStatus.ok); + expect(account?.email, 'a@b.c'); + expect(account?.password, 'pw'); + }); + + test('is 404 when the config files are absent', () async { + final origin = await enableWith({}); + + final response = await http.get(Uri.parse('$origin$kSyncAccountPath')); + + expect(response.statusCode, HttpStatus.notFound); + }); + + test('is 404 when firebase.json has no usable email', () async { + final origin = await enableWith({ + 'firebase.json': '{"apiKey":"x"}', + 'password': 'pw', + }); + + final response = await http.get(Uri.parse('$origin$kSyncAccountPath')); + + expect(response.statusCode, HttpStatus.notFound); + }); + }); }