diff --git a/.gitignore b/.gitignore index 94d5769..dd85a37 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,9 @@ Thumbs.db # Conversation exports (local only) conversation_export/ +uploads/ +objectbox/ +objectbox.dll # AI .claude diff --git a/lib/src/handlers/auth_handlers.dart b/lib/src/handlers/auth_handlers.dart index 226d0ed..2ffd7fe 100644 --- a/lib/src/handlers/auth_handlers.dart +++ b/lib/src/handlers/auth_handlers.dart @@ -397,10 +397,14 @@ String? normalizeHomeServer(String homeServerOrHost) { return null; } - return uri - .replace(path: '', query: null, fragment: null) - .toString() - .replaceAll(RegExp(r'/$'), ''); + final normalized = Uri( + scheme: uri.scheme, + userInfo: uri.userInfo, + host: uri.host, + port: uri.hasPort ? uri.port : null, + ).toString(); + + return normalized.replaceAll(RegExp(r'/$'), ''); } String defaultSchemeForHost(String host) { diff --git a/pubspec.lock b/pubspec.lock index 39d8412..95b39fa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -137,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -169,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" crypto: dependency: "direct main" description: @@ -341,10 +357,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" meta: dependency: transitive description: @@ -361,6 +377,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" objectbox: dependency: "direct main" description: @@ -457,6 +481,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -473,6 +513,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: @@ -521,14 +577,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + url: "https://pub.dev" + source: hosted + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + url: "https://pub.dev" + source: hosted + version: "0.6.12" timing: dependency: transitive description: @@ -585,6 +657,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3a219a9..06743a8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: hotreloader: dev_dependencies: + test: objectbox_generator: build_runner: lints: diff --git a/test/auth_helpers_test.dart b/test/auth_helpers_test.dart new file mode 100644 index 0000000..3047955 --- /dev/null +++ b/test/auth_helpers_test.dart @@ -0,0 +1,89 @@ +import 'package:chat_server/src/handlers/auth_handlers.dart'; +import 'package:test/test.dart'; + +void main() { + group('parseUserId', () { + test('parses valid userId', () { + final parsed = parseUserId('alice@example.com'); + expect(parsed, isNotNull); + expect(parsed!.username, 'alice'); + expect(parsed.host, 'example.com'); + }); + + test('returns null for invalid userId', () { + expect(parseUserId('alice'), isNull); + expect(parseUserId('alice@'), isNull); + expect(parseUserId('@example.com'), isNull); + expect(parseUserId('alice@example.com@x'), isNull); + }); + }); + + group('defaultSchemeForHost', () { + test('returns http for localhost-style hosts', () { + expect(defaultSchemeForHost('localhost:9870'), 'http'); + expect(defaultSchemeForHost('127.0.0.1:9870'), 'http'); + expect(defaultSchemeForHost('[::1]:9870'), 'http'); + }); + + test('returns https for non-local hosts', () { + expect(defaultSchemeForHost('chat.example.com'), 'https'); + }); + }); + + group('normalizeHomeServer', () { + test('normalizes host without scheme', () { + expect( + normalizeHomeServer('chat.example.com'), + 'https://chat.example.com', + ); + expect( + normalizeHomeServer('localhost:9870'), + 'http://localhost:9870', + ); + }); + + test('strips trailing slash/path/query/fragment', () { + expect( + normalizeHomeServer('https://example.com/a/b?x=1#y'), + 'https://example.com', + ); + expect( + normalizeHomeServer('https://example.com/'), + 'https://example.com', + ); + }); + + test('returns null for invalid input', () { + expect(normalizeHomeServer(''), isNull); + expect(normalizeHomeServer('://bad'), isNull); + }); + }); + + group('homeServerMatchesUserIdHost', () { + test('matches host and explicit port', () { + expect( + homeServerMatchesUserIdHost( + 'https://chat.example.com:8443', 'chat.example.com:8443'), + isTrue, + ); + expect( + homeServerMatchesUserIdHost( + 'https://chat.example.com:8443', 'chat.example.com'), + isFalse, + ); + }); + + test('matches default ports when omitted', () { + expect( + homeServerMatchesUserIdHost( + 'https://chat.example.com', 'chat.example.com:443'), + isTrue, + ); + expect( + homeServerMatchesUserIdHost( + 'http://chat.example.com', 'chat.example.com:80'), + isTrue, + ); + }); + }); +} diff --git a/test/crypto_service_test.dart b/test/crypto_service_test.dart new file mode 100644 index 0000000..1f74643 --- /dev/null +++ b/test/crypto_service_test.dart @@ -0,0 +1,42 @@ +import 'package:chat_server/src/auth/crypto_service.dart'; +import 'package:test/test.dart'; + +void main() { + group('AuthService password hashing', () { + test('hash + verify works', () { + const password = 'pass1234'; + final hash = AuthService.hashPassword(password); + + expect(hash, isNotEmpty); + expect(AuthService.verifyPassword(password, hash), isTrue); + expect(AuthService.verifyPassword('wrong-password', hash), isFalse); + }); + }); + + group('CryptoService signing', () { + test('signatures verify with matching keypair', () { + final keyPair = CryptoService.generateKeyPair(); + const payload = 'alice@localhost:9870:1700000000'; + + final signature = CryptoService.sign(payload, keyPair.privateKey); + final ok = CryptoService.verify(payload, signature, keyPair.publicKey); + + expect(ok, isTrue); + }); + + test('signature fails for modified payload', () { + final keyPair = CryptoService.generateKeyPair(); + const payload = 'payload-a'; + const tamperedPayload = 'payload-b'; + + final signature = CryptoService.sign(payload, keyPair.privateKey); + final ok = CryptoService.verify( + tamperedPayload, + signature, + keyPair.publicKey, + ); + + expect(ok, isFalse); + }); + }); +}