import 'package:flutter_test/flutter_test.dart'; import 'package:marianum_mobile/push/push_registration.dart'; import 'package:marianum_mobile/push/push_registration_store.dart'; import 'package:marianum_mobile/push/push_registration_type.dart'; import 'package:marianum_mobile/push/push_secure_storage.dart'; class _MemoryStorage implements FlutterSecureStorageLike { final Map values = {}; @override Future read({required String key}) async => values[key]; @override Future write({required String key, required String? value}) async { if (value == null) { values.remove(key); } else { values[key] = value; } } @override Future delete({required String key}) async => values.remove(key); } void main() { group('talk user agents', () { // Quoted from nextcloud/server lib/public/IRequest.php. final uaTalkAndroid = RegExp( r'^Mozilla/5\.0 \(Android\) Nextcloud-Talk v([^ ]*).*$', ); final uaTalkIos = RegExp( r'^Mozilla/5\.0 \(iOS\) Nextcloud-Talk v([^ ]*).*$', ); test('android UA matches USER_AGENT_TALK_ANDROID', () { expect( uaTalkAndroid.hasMatch(PushRegistration.talkUserAgentAndroid), isTrue, ); expect( uaTalkIos.hasMatch(PushRegistration.talkUserAgentAndroid), isFalse, ); }); test('ios UA matches USER_AGENT_TALK_IOS', () { expect(uaTalkIos.hasMatch(PushRegistration.talkUserAgentIos), isTrue); expect( uaTalkAndroid.hasMatch(PushRegistration.talkUserAgentIos), isFalse, ); }); }); group('pushTokenVariant', () { test('general uses the raw token, talk appends the suffix', () { expect(pushTokenVariant('tok', PushRegistrationType.general), 'tok'); expect(pushTokenVariant('tok', PushRegistrationType.talk), 'tok#talk'); }); test('variants always differ (NC would delete same-hash siblings)', () { const token = 'any-token'; expect( pushTokenVariant(token, PushRegistrationType.general), isNot(pushTokenVariant(token, PushRegistrationType.talk)), ); }); }); group('PushRegistrationStore', () { test('pre-dual entries are read as the general registration', () async { final storage = _MemoryStorage(); // State written by a pre-dual app version (no type suffixes). storage.values.addAll({ 'push_device_identifier': 'legacy-device', 'push_server_public_key_pem': 'legacy-key', 'push_registered_fcm_token': 'legacy-token', 'push_registered_proxy_server': 'https://old/push-proxy/', 'push_registered_nc_base_url': 'https://cloud', 'push_last_registration_at': '2026-07-01T10:00:00.000', 'push_last_registration_error': '', }); final store = PushRegistrationStore(storage); expect( await store.deviceIdentifier(PushRegistrationType.general), 'legacy-device', ); expect(await store.isRegistered(PushRegistrationType.general), isTrue); expect( await store.registeredProxyServer(PushRegistrationType.general), 'https://old/push-proxy/', ); expect( await store.lastRegistrationAt(PushRegistrationType.general), DateTime(2026, 7, 1, 10), ); // The talk registration is genuinely absent — the self-heal adds it. expect(await store.deviceIdentifier(PushRegistrationType.talk), isNull); expect(await store.isRegistered(PushRegistrationType.talk), isFalse); }); test('per-type values stay independent', () async { final storage = _MemoryStorage(); final store = PushRegistrationStore(storage); await store.save( type: PushRegistrationType.general, deviceIdentifier: 'dev-general', serverPublicKeyPem: 'server-key', fcmToken: 'token', proxyServer: 'https://a/push-proxy/', ncBaseUrl: 'https://cloud', ); await store.save( type: PushRegistrationType.talk, deviceIdentifier: 'dev-talk', serverPublicKeyPem: 'server-key', fcmToken: 'token', proxyServer: 'https://a/push-proxy/', ncBaseUrl: 'https://cloud', ); await store.saveLastRegistrationAttempt( type: PushRegistrationType.talk, at: DateTime(2026, 7, 5, 12), error: 'HTTP 404', ); expect( await store.deviceIdentifier(PushRegistrationType.general), 'dev-general', ); expect( await store.deviceIdentifier(PushRegistrationType.talk), 'dev-talk', ); // Shared server key: last write wins, both read the same value. expect(await store.serverPublicKeyPem(), 'server-key'); expect( await store.lastRegistrationError(PushRegistrationType.general), isNull, ); expect( await store.lastRegistrationError(PushRegistrationType.talk), 'HTTP 404', ); }); test('clear removes both registrations and shared keys', () async { final storage = _MemoryStorage(); final store = PushRegistrationStore(storage); for (final type in PushRegistrationType.values) { await store.save( type: type, deviceIdentifier: 'dev', serverPublicKeyPem: 'key', fcmToken: 'token', proxyServer: 'https://a/', ncBaseUrl: 'https://cloud', ); await store.saveLastRegistrationAttempt( type: type, at: DateTime(2026), error: 'x', ); } await store.saveNativeAuthContext( username: 'user', baseUrl: 'https://cloud', ); await store.clear(); expect(storage.values, isEmpty); }); }); }