diff --git a/lib/desktop/wrapper_server.dart b/lib/desktop/wrapper_server.dart index 09537e8..817110a 100644 --- a/lib/desktop/wrapper_server.dart +++ b/lib/desktop/wrapper_server.dart @@ -28,7 +28,20 @@ class WrapperServer { required this.webRoot, required this.backlogPath, required this.logPath, - }); + 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; @@ -98,14 +111,12 @@ class WrapperServer { /// daemons already use, so there is one source of truth per machine rather /// than a per-app copy. Future _syncAccount(HttpRequest request) async { - if ((Platform.environment[kSyncAccountEnvVar] ?? '').isEmpty) { + if (!serveSyncAccount) { request.response.statusCode = HttpStatus.notFound; return; } - final home = Platform.environment['HOME'] ?? ''; - final configDir = p.join(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/test/wrapper_server_test.dart b/test/wrapper_server_test.dart index 08f8e7c..7b0931e 100644 --- a/test/wrapper_server_test.dart +++ b/test/wrapper_server_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:crdt_sync/crdt_sync.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; @@ -149,4 +150,66 @@ void main() { expect(kind('a.bin'), 'application/octet-stream'); expect(kind('a.unknown'), 'application/octet-stream'); }); + + group('sync-account provisioning', () { + /// Starts a second server with provisioning enabled against [files]. + Future enabledOrigin(Map files) async { + final configDir = Directory(p.join(root.path, 'crdt-sync')) + ..createSync(recursive: true); + files.forEach((name, contents) { + File(p.join(configDir.path, name)).writeAsStringSync(contents); + }); + final enabled = WrapperServer( + webRoot: p.join(root.path, 'web'), + backlogPath: p.join(root.path, 'todo', 'BACKLOG.md'), + logPath: p.join(root.path, 'state', 'todo_notes.json'), + serveSyncAccount: true, + syncConfigDir: configDir.path, + ); + await enabled.start(0); + addTearDown(enabled.stop); + return 'http://localhost:${enabled.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 app is running. + final response = await http.get(Uri.parse('$base$kSyncAccountPath')); + + expect(response.statusCode, HttpStatus.notFound); + }); + + test('serves the account when enabled', () async { + final origin = await enabledOrigin({ + '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 enabledOrigin({}); + + 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 enabledOrigin({ + 'firebase.json': '{"apiKey":"x"}', + 'password': 'pw', + }); + + final response = await http.get(Uri.parse('$origin$kSyncAccountPath')); + + expect(response.statusCode, HttpStatus.notFound); + }); + }); }