From 4d72c86d3494fe13d52f17e62b487bedecaa6007 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Mon, 23 Sep 2024 14:31:56 +0200 Subject: [PATCH] fixup! feat(notifications_push_repository): Init --- .../lib/notifications_push_repository.dart | 1 - .../lib/src/models/push_notification.dart | 2 +- .../lib/src/models/push_subscription.dart | 2 +- .../src/notifications_push_repository.dart | 123 ++--- .../lib/src/notifications_push_storage.dart | 26 +- .../pubspec.yaml | 2 +- .../notifications_push_repository_test.dart | 496 +++++++++++++++--- .../test/notifications_push_storage_test.dart | 15 +- 8 files changed, 510 insertions(+), 157 deletions(-) diff --git a/packages/neon_framework/packages/notifications_push_repository/lib/notifications_push_repository.dart b/packages/neon_framework/packages/notifications_push_repository/lib/notifications_push_repository.dart index 2e6b1a4760a..642fbb9d6f6 100644 --- a/packages/neon_framework/packages/notifications_push_repository/lib/notifications_push_repository.dart +++ b/packages/neon_framework/packages/notifications_push_repository/lib/notifications_push_repository.dart @@ -1,4 +1,3 @@ export 'src/models/models.dart' show PushNotification; export 'src/notifications_push_repository.dart'; -export 'src/notifications_push_storage.dart'; export 'src/utils/encryption.dart' show parseEncryptedPushNotifications; diff --git a/packages/neon_framework/packages/notifications_push_repository/lib/src/models/push_notification.dart b/packages/neon_framework/packages/notifications_push_repository/lib/src/models/push_notification.dart index a70018b4431..e8d155eff65 100644 --- a/packages/neon_framework/packages/notifications_push_repository/lib/src/models/push_notification.dart +++ b/packages/neon_framework/packages/notifications_push_repository/lib/src/models/push_notification.dart @@ -57,7 +57,7 @@ abstract class PushNotification implements Built>? _accountsListener; late final RSAPrivateKey _privateKey; - late BuiltList _accounts; String? _selectedDistributor; - String? _previousDistributor; /// Returns all available distributors. Future> get distributors async { @@ -61,7 +61,7 @@ final class NotificationsPushRepository { } _selectedDistributor = distributor; - await _updateSubscriptions(); + await _updateSubscriptions(distributorChanged: true); } /// Initializes the repository. @@ -70,20 +70,17 @@ final class NotificationsPushRepository { return; } - _accounts = (await _accountRepository.accounts.first).accounts; _accountsListener = _accountRepository.accounts.skip(1).map((e) => e.accounts).listen((accounts) async { - _accounts = accounts; await _updateSubscriptions(); }); _selectedDistributor = await UnifiedPush.getDistributor(); - _previousDistributor = _selectedDistributor; _privateKey = await getDevicePrivateKey(_storage); await UnifiedPush.initialize( onNewEndpoint: (endpoint, accountID) async { - final account = _accounts.firstWhereOrNull((account) => account.id == accountID); + final account = _accountRepository.accountByID(accountID); if (account == null) { _log.finer('Account $accountID not found'); // coverage:ignore-line return; @@ -97,12 +94,25 @@ final class NotificationsPushRepository { } subscription = subscription.rebuild((b) => b.endpoint = endpoint); - subscription = await _unregisterNextcloud(accountID, account, subscription); - subscription = await _registerNextcloud(account, endpoint, subscription); - await _saveUpdatedSubscription(subscriptions, account.id, subscription); + var pushDevice = subscription.pushDevice; + if (pushDevice != null) { + await _unregisterNextcloud(accountID, account, pushDevice); + subscription = subscription.rebuild((b) => b.pushDevice = null); + } + + pushDevice = await _registerNextcloud(account, endpoint); + subscription = subscription.rebuild((b) { + if (pushDevice == null) { + b.pushDevice = null; + } else { + b.pushDevice.replace(pushDevice); + } + }); + + await _storage.updateSubscription(account.id, subscription); }, onUnregistered: (accountID) async { - final account = _accounts.firstWhereOrNull((account) => account.id == accountID); + final account = _accountRepository.accountByID(accountID); if (account == null) { _log.finer('Account $accountID not found'); // coverage:ignore-line return; @@ -116,9 +126,13 @@ final class NotificationsPushRepository { } subscription = subscription.rebuild((b) => b.endpoint = null); - subscription = await _unregisterNextcloud(accountID, account, subscription); + final pushDevice = subscription.pushDevice; + if (pushDevice != null) { + await _unregisterNextcloud(accountID, account, pushDevice); + subscription = subscription.rebuild((b) => b.pushDevice = null); + } - await _saveUpdatedSubscription(subscriptions, accountID, subscription); + await _storage.updateSubscription(accountID, subscription); }, onMessage: _onMessage, ); @@ -128,7 +142,7 @@ final class NotificationsPushRepository { _initialized = true; } - Future _updateSubscriptions() async { + Future _updateSubscriptions({bool distributorChanged = false}) async { if (_selectedDistributor == null) { _log.fine('Push notifications disabled, removing all subscriptions'); @@ -136,8 +150,7 @@ final class NotificationsPushRepository { return; } - if (_selectedDistributor != _previousDistributor) { - _previousDistributor = _selectedDistributor; + if (distributorChanged) { _log.finer('UnifiedPush distributor changed to $_selectedDistributor'); await _unregisterUnifiedPush(); @@ -149,7 +162,8 @@ final class NotificationsPushRepository { Future _registerUnifiedPush() async { // Notifications will only work on accounts with app password - for (final account in _accounts.where((a) => a.credentials.appPassword != null)) { + final accounts = (await _accountRepository.accounts.first).accounts; + for (final account in accounts.where((a) => a.credentials.appPassword != null)) { _log.finer('Registering ${account.id} for UnifiedPush'); await UnifiedPush.registerApp(account.id); @@ -160,24 +174,28 @@ final class NotificationsPushRepository { final subscriptions = await _storage.readSubscriptions(); for (final entry in subscriptions.entries) { final accountID = entry.key; - final account = _accounts.firstWhereOrNull((account) => account.id == accountID); + final account = _accountRepository.accountByID(accountID); var subscription = entry.value; - subscription = await _unregisterNextcloud(accountID, account, subscription); + final pushDevice = subscription.pushDevice; + if (pushDevice != null) { + await _unregisterNextcloud(accountID, account, pushDevice); + subscription = subscription.rebuild((b) => b.pushDevice = null); + } + if (subscription.endpoint != null) { _log.finer('Unregistering $accountID from UnifiedPush'); - subscription = subscription.rebuild((b) => b.endpoint = null); - await _saveUpdatedSubscription(subscriptions, accountID, subscription); - await UnifiedPush.unregister(accountID); - } else { - await _saveUpdatedSubscription(subscriptions, accountID, subscription); + + subscription = subscription.rebuild((b) => b.endpoint = null); } + + await _storage.updateSubscription(accountID, subscription); } } - Future _registerNextcloud(Account account, String endpoint, PushSubscription subscription) async { + Future _registerNextcloud(Account account, String endpoint) async { _log.finer('Registering ${account.id} at Nextcloud'); try { @@ -190,54 +208,25 @@ final class NotificationsPushRepository { ), ); - return subscription.rebuild( - (b) => b - ..endpoint = endpoint - ..pushDevice.replace(response.body.ocs.data), - ); - } on DynamiteApiException catch (error) { + return response.body.ocs.data; + } on http.ClientException catch (error) { _log.warning('Failed to register ${account.id} at Nextcloud', error); } - return subscription; + return null; } - Future _unregisterNextcloud( + Future _unregisterNextcloud( String accountID, Account? account, - PushSubscription subscription, + notifications.PushDevice pushDevice, ) async { - if (subscription.pushDevice != null) { - _log.finer('Unregistering $accountID from Nextcloud'); + _log.finer('Unregistering $accountID from Nextcloud'); - try { - await account?.client.notifications.push.removeDevice(); - } on DynamiteApiException catch (error) { - _log.warning('Failed to unregister $accountID at Nextcloud', error); - } + try { + await account?.client.notifications.push.removeDevice(); + } on http.ClientException catch (error) { + _log.warning('Failed to unregister $accountID at Nextcloud', error); } - - // Remove the push device either way, as we need to register for a new one later - return subscription.rebuild((b) => b.pushDevice = null); - } - - Future _saveUpdatedSubscription( - BuiltMap subscriptions, - String accountID, - PushSubscription subscription, - ) async { - await _storage.saveSubscriptions( - subscriptions.rebuild( - (b) { - if (subscription.endpoint == null && subscription.pushDevice == null) { - _log.finer('Removing subscription for $accountID'); - b.remove(accountID); - } else { - _log.finer('Saving subscription for $accountID'); - b[accountID] = subscription; - } - }, - ), - ); } } diff --git a/packages/neon_framework/packages/notifications_push_repository/lib/src/notifications_push_storage.dart b/packages/neon_framework/packages/notifications_push_repository/lib/src/notifications_push_storage.dart index 795d561fa89..4600edcb4cb 100644 --- a/packages/neon_framework/packages/notifications_push_repository/lib/src/notifications_push_storage.dart +++ b/packages/neon_framework/packages/notifications_push_repository/lib/src/notifications_push_storage.dart @@ -1,9 +1,4 @@ -import 'dart:convert'; - -import 'package:built_collection/built_collection.dart'; -import 'package:crypton/crypton.dart'; -import 'package:neon_framework/storage.dart'; -import 'package:notifications_push_repository/src/models/models.dart'; +part of 'notifications_push_repository.dart'; /// A storage for push subscriptions and the device private key. class NotificationsPushStorage { @@ -45,19 +40,14 @@ class NotificationsPushStorage { return builder.build(); } - /// Saves the updated [subscriptions]. + /// Updates a [subscription]. /// - /// Removes all stored subscriptions that are no longer present in the new [subscriptions]. - Future saveSubscriptions(BuiltMap subscriptions) async { - for (final entry in subscriptions.entries) { - _pushSubscriptionsPersistence.setValue(entry.key, json.encode(entry.value.toJson())); - } - - final keys = await _pushSubscriptionsPersistence.keys(); - for (final key in keys) { - if (!subscriptions.containsKey(key)) { - await _pushSubscriptionsPersistence.remove(key); - } + /// If [PushSubscription.endpoint] and [PushSubscription.pushDevice] are `null` the subscription will be removed. + Future updateSubscription(String key, PushSubscription subscription) async { + if (subscription.endpoint == null && subscription.pushDevice == null) { + _pushSubscriptionsPersistence.remove(key); + } else { + _pushSubscriptionsPersistence.setValue(key, json.encode(subscription.toJson())); } } } diff --git a/packages/neon_framework/packages/notifications_push_repository/pubspec.yaml b/packages/neon_framework/packages/notifications_push_repository/pubspec.yaml index c2b457b747c..b53e824b056 100644 --- a/packages/neon_framework/packages/notifications_push_repository/pubspec.yaml +++ b/packages/neon_framework/packages/notifications_push_repository/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: built_value: ^8.9.2 collection: ^1.0.0 crypton: ^2.0.0 + http: ^1.0.0 logging: ^1.0.0 meta: ^1.0.0 neon_framework: @@ -34,7 +35,6 @@ dev_dependencies: # https://github.com/invertase/melos/issues/755 flutter: sdk: flutter - http: ^1.2.2 mocktail: ^1.0.4 neon_lints: git: diff --git a/packages/neon_framework/packages/notifications_push_repository/test/notifications_push_repository_test.dart b/packages/neon_framework/packages/notifications_push_repository/test/notifications_push_repository_test.dart index b93a9e4aa0c..21cc9c7fe1b 100644 --- a/packages/neon_framework/packages/notifications_push_repository/test/notifications_push_repository_test.dart +++ b/packages/neon_framework/packages/notifications_push_repository/test/notifications_push_repository_test.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:account_repository/account_repository.dart'; import 'package:built_collection/built_collection.dart'; +import 'package:collection/collection.dart'; import 'package:crypton/crypton.dart'; import 'package:http/http.dart' as http; import 'package:mocktail/mocktail.dart'; @@ -63,7 +64,7 @@ void main() { setUpAll(() { registerFallbackValue(RSAPrivateKey(BigInt.zero, BigInt.zero, BigInt.zero, BigInt.zero)); - registerFallbackValue(BuiltMap()); + registerFallbackValue(PushSubscription()); registerFallbackValue(Uri()); registerFallbackValue(BuiltMap()); registerFallbackValue(Uint8List(0)); @@ -75,6 +76,12 @@ void main() { accountsSubject = BehaviorSubject(); accountRepository = _AccountRepositoryMock(); when(() => accountRepository.accounts).thenAnswer((_) => accountsSubject.map((e) => (active: null, accounts: e))); + when(() => accountRepository.accountByID(any())).thenAnswer( + (invocation) { + final accountID = invocation.positionalArguments[0] as String; + return accountsSubject.value.singleWhereOrNull((account) => account.id == accountID); + }, + ); storage = _StorageMock(); @@ -223,8 +230,18 @@ void main() { ); var subscriptions = BuiltMap({account.id: subscription}); when(() => storage.readSubscriptions()).thenAnswer((_) async => subscriptions); - when(() => storage.saveSubscriptions(any())).thenAnswer((invocation) async { - subscriptions = invocation.positionalArguments[0] as BuiltMap; + when(() => storage.updateSubscription(any(), any())).thenAnswer((invocation) async { + subscriptions = subscriptions.rebuild( + (b) { + final key = invocation.positionalArguments[0] as String; + final subscription = invocation.positionalArguments[1] as PushSubscription; + if (subscription.endpoint == null && subscription.pushDevice == null) { + b.remove(key); + } else { + b[key] = subscription; + } + }, + ); }); accountsSubject.add(BuiltList([account])); @@ -292,8 +309,8 @@ void main() { ), ).called(1); verifyNever(() => httpRequest(any(), any(), any(), any())); - verify(() => storage.saveSubscriptions(BuiltMap())).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription(account.id, PushSubscription())).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); test('Failure at Nextcloud', () async { @@ -336,8 +353,8 @@ void main() { ), ).called(1); verifyNever(() => httpRequest(any(), any(), any(), any())); - verify(() => storage.saveSubscriptions(BuiltMap())).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription(account.id, PushSubscription())).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); }); @@ -349,7 +366,7 @@ void main() { when(() => storage.saveDevicePrivateKey(any())).thenAnswer((_) async {}); final subscription = PushSubscription(); when(() => storage.readSubscriptions()).thenAnswer((_) async => BuiltMap({'1': subscription})); - when(() => storage.saveSubscriptions(any())).thenAnswer((_) async {}); + when(() => storage.updateSubscription(any(), any())).thenAnswer((_) async {}); accountsSubject.add(BuiltList([])); @@ -361,8 +378,8 @@ void main() { await repository.initialize(); verifyNever(() => unifiedPushPlatform.unregister(any())); - verify(() => storage.saveSubscriptions(BuiltMap())).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription('1', PushSubscription())).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); test('With endpoint', () async { @@ -374,7 +391,7 @@ void main() { (b) => b..endpoint = 'endpoint', ); when(() => storage.readSubscriptions()).thenAnswer((_) async => BuiltMap({'1': subscription})); - when(() => storage.saveSubscriptions(any())).thenAnswer((_) async {}); + when(() => storage.updateSubscription(any(), any())).thenAnswer((_) async {}); accountsSubject.add(BuiltList([])); @@ -387,8 +404,8 @@ void main() { verify(() => unifiedPushPlatform.unregister('1')).called(1); verifyNever(() => unifiedPushPlatform.unregister(any())); - verify(() => storage.saveSubscriptions(BuiltMap())).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription('1', PushSubscription())).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); test('With pushDevice', () async { @@ -406,7 +423,7 @@ void main() { ), ); when(() => storage.readSubscriptions()).thenAnswer((_) async => BuiltMap({'1': subscription})); - when(() => storage.saveSubscriptions(any())).thenAnswer((_) async {}); + when(() => storage.updateSubscription(any(), any())).thenAnswer((_) async {}); accountsSubject.add(BuiltList([])); @@ -418,8 +435,8 @@ void main() { await repository.initialize(); verifyNever(() => unifiedPushPlatform.unregister(any())); - verify(() => storage.saveSubscriptions(BuiltMap())).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription('1', PushSubscription())).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); test('With endpoint and pushDevice', () async { @@ -438,7 +455,7 @@ void main() { ), ); when(() => storage.readSubscriptions()).thenAnswer((_) async => BuiltMap({'1': subscription})); - when(() => storage.saveSubscriptions(any())).thenAnswer((_) async {}); + when(() => storage.updateSubscription(any(), any())).thenAnswer((_) async {}); accountsSubject.add(BuiltList([])); @@ -451,8 +468,8 @@ void main() { verify(() => unifiedPushPlatform.unregister('1')).called(1); verifyNever(() => unifiedPushPlatform.unregister(any())); - verify(() => storage.saveSubscriptions(BuiltMap())).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription('1', PushSubscription())).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); }); @@ -496,7 +513,7 @@ void main() { group('Existing accounts', () { setUp(() { when(() => storage.readSubscriptions()).thenAnswer((_) async => BuiltMap()); - when(() => storage.saveSubscriptions(any())).thenAnswer((invocation) async {}); + when(() => storage.updateSubscription(any(), any())).thenAnswer((_) async {}); }); test('Success at Nextcloud', () async { @@ -584,8 +601,8 @@ void main() { ..signature = 'signature', ), ); - verify(() => storage.saveSubscriptions(BuiltMap({account.id: expectedSubscription}))).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription(account.id, expectedSubscription)).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); test('Failure at Nextcloud', () async { @@ -640,8 +657,8 @@ void main() { final expectedSubscription = PushSubscription( (b) => b..endpoint = 'https://cloud.example.com:8443/nextcloud/unifiedpush', ); - verify(() => storage.saveSubscriptions(BuiltMap({account.id: expectedSubscription}))).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription(account.id, expectedSubscription)).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); }); @@ -671,8 +688,18 @@ void main() { ); var subscriptions = BuiltMap({account.id: subscription}); when(() => storage.readSubscriptions()).thenAnswer((_) async => subscriptions); - when(() => storage.saveSubscriptions(any())).thenAnswer((invocation) async { - subscriptions = invocation.positionalArguments[0] as BuiltMap; + when(() => storage.updateSubscription(any(), any())).thenAnswer((invocation) async { + subscriptions = subscriptions.rebuild( + (b) { + final key = invocation.positionalArguments[0] as String; + final subscription = invocation.positionalArguments[1] as PushSubscription; + if (subscription.endpoint == null && subscription.pushDevice == null) { + b.remove(key); + } else { + b[key] = subscription; + } + }, + ); }); }); @@ -764,15 +791,8 @@ void main() { ..signature = 'new-signature', ), ); - verify( - () => storage.saveSubscriptions( - BuiltMap({ - account.id: subscription, - newAccount.id: expectedSubscription, - }), - ), - ).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription(newAccount.id, expectedSubscription)).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); test('Failure at Nextcloud', () async { @@ -830,15 +850,8 @@ void main() { final expectedSubscription = PushSubscription( (b) => b..endpoint = 'https://cloud.example.com:8443/nextcloud/unifiedpush', ); - verify( - () => storage.saveSubscriptions( - BuiltMap({ - account.id: subscription, - newAccount.id: expectedSubscription, - }), - ), - ).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription(newAccount.id, expectedSubscription)).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); }); }); @@ -857,8 +870,18 @@ void main() { ); var subscriptions = BuiltMap({account.id: subscription}); when(() => storage.readSubscriptions()).thenAnswer((_) async => subscriptions); - when(() => storage.saveSubscriptions(any())).thenAnswer((invocation) async { - subscriptions = invocation.positionalArguments[0] as BuiltMap; + when(() => storage.updateSubscription(any(), any())).thenAnswer((invocation) async { + subscriptions = subscriptions.rebuild( + (b) { + final key = invocation.positionalArguments[0] as String; + final subscription = invocation.positionalArguments[1] as PushSubscription; + if (subscription.endpoint == null && subscription.pushDevice == null) { + b.remove(key); + } else { + b[key] = subscription; + } + }, + ); }); }); @@ -925,8 +948,8 @@ void main() { ), ).called(1); verifyNever(() => httpRequest(any(), any(), any(), any())); - verify(() => storage.saveSubscriptions(BuiltMap())).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription(account.id, PushSubscription())).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); test('Failure at Nextcloud', () async { @@ -969,8 +992,8 @@ void main() { ), ).called(1); verifyNever(() => httpRequest(any(), any(), any(), any())); - verify(() => storage.saveSubscriptions(BuiltMap())).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription(account.id, PushSubscription())).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); }); @@ -1075,7 +1098,7 @@ void main() { Uint8List(0), ), ).called(1); - verify(() => storage.saveSubscriptions(BuiltMap())).called(1); + verify(() => storage.updateSubscription(account.id, PushSubscription())).called(1); verify(() => unifiedPushPlatform.registerApp(account.id, [])).called(1); verifyNever(() => unifiedPushPlatform.registerApp(any(), any())); @@ -1108,7 +1131,7 @@ void main() { verifyNever(() => httpRequest(any(), any(), any(), any())); final expectedSubscription = PushSubscription( (b) => b - ..endpoint = 'https://cloud.example.com:8443/nextcloud/new-unifiedpush' + ..endpoint = endpoint.toString() ..pushDevice.update( (b) => b ..publicKey = 'new-publicKey' @@ -1116,8 +1139,8 @@ void main() { ..signature = 'new-signature', ), ); - verify(() => storage.saveSubscriptions(BuiltMap({account.id: expectedSubscription}))).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription(account.id, expectedSubscription)).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); group('Failure at Nextcloud', () { @@ -1199,7 +1222,7 @@ void main() { Uint8List(0), ), ).called(1); - verify(() => storage.saveSubscriptions(BuiltMap())).called(1); + verify(() => storage.updateSubscription(account.id, PushSubscription())).called(1); verify(() => unifiedPushPlatform.registerApp(account.id, [])).called(1); verifyNever(() => unifiedPushPlatform.registerApp(any(), any())); @@ -1232,7 +1255,7 @@ void main() { verifyNever(() => httpRequest(any(), any(), any(), any())); final expectedSubscription = PushSubscription( (b) => b - ..endpoint = 'https://cloud.example.com:8443/nextcloud/new-unifiedpush' + ..endpoint = endpoint.toString() ..pushDevice.update( (b) => b ..publicKey = 'new-publicKey' @@ -1240,8 +1263,8 @@ void main() { ..signature = 'new-signature', ), ); - verify(() => storage.saveSubscriptions(BuiltMap({account.id: expectedSubscription}))).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription(account.id, expectedSubscription)).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); test('Register', () async { @@ -1318,7 +1341,7 @@ void main() { Uint8List(0), ), ).called(1); - verify(() => storage.saveSubscriptions(BuiltMap())).called(1); + verify(() => storage.updateSubscription(account.id, PushSubscription())).called(1); verify(() => unifiedPushPlatform.registerApp(account.id, [])).called(1); verifyNever(() => unifiedPushPlatform.registerApp(any(), any())); @@ -1350,10 +1373,359 @@ void main() { ).called(1); verifyNever(() => httpRequest(any(), any(), any(), any())); final expectedSubscription = PushSubscription( - (b) => b..endpoint = 'https://cloud.example.com:8443/nextcloud/new-unifiedpush', + (b) => b.endpoint = endpoint.toString(), + ); + verify(() => storage.updateSubscription(account.id, expectedSubscription)).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); + }); + }); + }); + + group('Updates subscriptions when endpoint changes', () { + test('Success at Nextcloud', () async { + when( + () => httpRequest( + 'DELETE', + Uri.parse('https://cloud.example.com:8443/nextcloud/ocs/v2.php/apps/notifications/api/v2/push'), + any(), + any(), + ), + ).thenAnswer( + (_) => http.StreamedResponse( + Stream.value( + utf8.encode( + json.encode( + { + 'ocs': { + 'meta': { + 'status': '', + 'statuscode': 0, + }, + 'data': {}, + }, + }, + ), + ), + ), + 200, + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + ), + ); + when( + () => httpRequest( + 'POST', + Uri.parse('https://cloud.example.com:8443/nextcloud/ocs/v2.php/apps/notifications/api/v2/push'), + any(), + any(), + ), + ).thenAnswer( + (_) => http.StreamedResponse( + Stream.value( + utf8.encode( + json.encode( + { + 'ocs': { + 'meta': { + 'status': '', + 'statuscode': 0, + }, + 'data': { + 'publicKey': 'new-publicKey', + 'deviceIdentifier': 'new-deviceIdentifier', + 'signature': 'new-signature', + }, + }, + }, + ), + ), + ), + 201, + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + ), + ); + + repository = NotificationsPushRepository( + accountRepository: accountRepository, + storage: storage, + onMessage: onMessageCallback, + ); + await repository.initialize(); + + endpoint = Uri.parse('https://cloud.example.com:8443/nextcloud/new-unifiedpush'); + + unifiedPushOnNewEndpoint!(endpoint.toString(), account.id); + await Future.delayed(const Duration(milliseconds: 1)); + + verify( + () => httpRequest( + 'DELETE', + Uri.parse('https://cloud.example.com:8443/nextcloud/ocs/v2.php/apps/notifications/api/v2/push'), + BuiltMap( + { + 'Accept': 'application/json', + 'Authorization': 'Bearer user1', + 'OCS-APIRequest': 'true', + 'user-agent': 'neon', + }, + ), + Uint8List(0), + ), + ).called(1); + + verify( + () => httpRequest( + 'POST', + Uri.parse('https://cloud.example.com:8443/nextcloud/ocs/v2.php/apps/notifications/api/v2/push'), + BuiltMap( + { + 'Accept': 'application/json', + 'Authorization': 'Bearer user1', + 'OCS-APIRequest': 'true', + 'Content-Type': 'application/json; charset=utf-8', + 'user-agent': 'neon', + }, + ), + utf8.encode( + json.encode( + notifications.PushRegisterDeviceRequestApplicationJson( + (b) => b + ..pushTokenHash = notifications.generatePushTokenHash(endpoint.toString()) + ..devicePublicKey = privateKey.publicKey.toFormattedPEM() + ..proxyServer = '$endpoint#', + ), + ), + ), + ), + ).called(1); + verifyNever(() => httpRequest(any(), any(), any(), any())); + final expectedSubscription = PushSubscription( + (b) => b + ..endpoint = endpoint.toString() + ..pushDevice.update( + (b) => b + ..publicKey = 'new-publicKey' + ..deviceIdentifier = 'new-deviceIdentifier' + ..signature = 'new-signature', + ), + ); + verify(() => storage.updateSubscription(account.id, expectedSubscription)).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); + }); + + group('Failure at Nextcloud', () { + test('Unregister', () async { + when( + () => httpRequest( + 'DELETE', + Uri.parse('https://cloud.example.com:8443/nextcloud/ocs/v2.php/apps/notifications/api/v2/push'), + any(), + any(), + ), + ).thenAnswer((_) => http.StreamedResponse(const Stream.empty(), 500)); + when( + () => httpRequest( + 'POST', + Uri.parse('https://cloud.example.com:8443/nextcloud/ocs/v2.php/apps/notifications/api/v2/push'), + any(), + any(), + ), + ).thenAnswer( + (_) => http.StreamedResponse( + Stream.value( + utf8.encode( + json.encode( + { + 'ocs': { + 'meta': { + 'status': '', + 'statuscode': 0, + }, + 'data': { + 'publicKey': 'new-publicKey', + 'deviceIdentifier': 'new-deviceIdentifier', + 'signature': 'new-signature', + }, + }, + }, + ), + ), + ), + 201, + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + ), + ); + + repository = NotificationsPushRepository( + accountRepository: accountRepository, + storage: storage, + onMessage: onMessageCallback, + ); + await repository.initialize(); + + endpoint = Uri.parse('https://cloud.example.com:8443/nextcloud/new-unifiedpush'); + + unifiedPushOnNewEndpoint!(endpoint.toString(), account.id); + await Future.delayed(const Duration(milliseconds: 1)); + + verify( + () => httpRequest( + 'DELETE', + Uri.parse('https://cloud.example.com:8443/nextcloud/ocs/v2.php/apps/notifications/api/v2/push'), + BuiltMap( + { + 'Accept': 'application/json', + 'Authorization': 'Bearer user1', + 'OCS-APIRequest': 'true', + 'user-agent': 'neon', + }, + ), + Uint8List(0), + ), + ).called(1); + verify( + () => httpRequest( + 'POST', + Uri.parse('https://cloud.example.com:8443/nextcloud/ocs/v2.php/apps/notifications/api/v2/push'), + BuiltMap( + { + 'Accept': 'application/json', + 'Authorization': 'Bearer user1', + 'OCS-APIRequest': 'true', + 'Content-Type': 'application/json; charset=utf-8', + 'user-agent': 'neon', + }, + ), + utf8.encode( + json.encode( + notifications.PushRegisterDeviceRequestApplicationJson( + (b) => b + ..pushTokenHash = notifications.generatePushTokenHash(endpoint.toString()) + ..devicePublicKey = privateKey.publicKey.toFormattedPEM() + ..proxyServer = '$endpoint#', + ), + ), + ), + ), + ).called(1); + verifyNever(() => httpRequest(any(), any(), any(), any())); + final expectedSubscription = PushSubscription( + (b) => b + ..endpoint = endpoint.toString() + ..pushDevice.update( + (b) => b + ..publicKey = 'new-publicKey' + ..deviceIdentifier = 'new-deviceIdentifier' + ..signature = 'new-signature', + ), + ); + verify(() => storage.updateSubscription(account.id, expectedSubscription)).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); + }); + + test('Register', () async { + when( + () => httpRequest( + 'DELETE', + Uri.parse('https://cloud.example.com:8443/nextcloud/ocs/v2.php/apps/notifications/api/v2/push'), + any(), + any(), + ), + ).thenAnswer( + (_) => http.StreamedResponse( + Stream.value( + utf8.encode( + json.encode( + { + 'ocs': { + 'meta': { + 'status': '', + 'statuscode': 0, + }, + 'data': {}, + }, + }, + ), + ), + ), + 200, + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + ), + ); + when( + () => httpRequest( + 'POST', + Uri.parse('https://cloud.example.com:8443/nextcloud/ocs/v2.php/apps/notifications/api/v2/push'), + any(), + any(), + ), + ).thenAnswer((_) => http.StreamedResponse(const Stream.empty(), 500)); + + repository = NotificationsPushRepository( + accountRepository: accountRepository, + storage: storage, + onMessage: onMessageCallback, + ); + await repository.initialize(); + + endpoint = Uri.parse('https://cloud.example.com:8443/nextcloud/new-unifiedpush'); + + unifiedPushOnNewEndpoint!(endpoint.toString(), account.id); + await Future.delayed(const Duration(milliseconds: 1)); + + verify( + () => httpRequest( + 'DELETE', + Uri.parse('https://cloud.example.com:8443/nextcloud/ocs/v2.php/apps/notifications/api/v2/push'), + BuiltMap( + { + 'Accept': 'application/json', + 'Authorization': 'Bearer user1', + 'OCS-APIRequest': 'true', + 'user-agent': 'neon', + }, + ), + Uint8List(0), + ), + ).called(1); + verify( + () => httpRequest( + 'POST', + Uri.parse('https://cloud.example.com:8443/nextcloud/ocs/v2.php/apps/notifications/api/v2/push'), + BuiltMap( + { + 'Accept': 'application/json', + 'Authorization': 'Bearer user1', + 'OCS-APIRequest': 'true', + 'Content-Type': 'application/json; charset=utf-8', + 'user-agent': 'neon', + }, + ), + utf8.encode( + json.encode( + notifications.PushRegisterDeviceRequestApplicationJson( + (b) => b + ..pushTokenHash = notifications.generatePushTokenHash(endpoint.toString()) + ..devicePublicKey = privateKey.publicKey.toFormattedPEM() + ..proxyServer = '$endpoint#', + ), + ), + ), + ), + ).called(1); + verifyNever(() => httpRequest(any(), any(), any(), any())); + final expectedSubscription = PushSubscription( + (b) => b.endpoint = endpoint.toString(), ); - verify(() => storage.saveSubscriptions(BuiltMap({account.id: expectedSubscription}))).called(1); - verifyNever(() => storage.saveSubscriptions(any())); + verify(() => storage.updateSubscription(account.id, expectedSubscription)).called(1); + verifyNever(() => storage.updateSubscription(any(), any())); }); }); }); diff --git a/packages/neon_framework/packages/notifications_push_repository/test/notifications_push_storage_test.dart b/packages/neon_framework/packages/notifications_push_repository/test/notifications_push_storage_test.dart index aba21779848..26d4b68304a 100644 --- a/packages/neon_framework/packages/notifications_push_repository/test/notifications_push_storage_test.dart +++ b/packages/neon_framework/packages/notifications_push_repository/test/notifications_push_storage_test.dart @@ -94,21 +94,24 @@ MDsCAQAwDQYJKoZIhvcNAQEBBQAEJzAlAgEAAgMBchMCAwEAAQICFgECAgHBAgIA0wICAQECAgCtAgIB verifyNever(() => pushSubscriptionsPersistence.getValue(any())); }); - test('saveSubscriptions', () async { + test('updateSubscription', () async { when(() => pushSubscriptionsPersistence.setValue(any(), any())).thenReturn(true); - when(() => pushSubscriptionsPersistence.keys()).thenReturn(['a', 'b', 'c']); when(() => pushSubscriptionsPersistence.remove(any())).thenReturn(true); - await storage.saveSubscriptions(subscriptionsMap); - + await storage.updateSubscription('a', subscriptionsMap['a']!); verify(() => pushSubscriptionsPersistence.setValue('a', serializedSubscriptions['a']!)).called(1); - verify(() => pushSubscriptionsPersistence.setValue('b', serializedSubscriptions['b']!)).called(1); verifyNever(() => pushSubscriptionsPersistence.setValue(any(), any())); + verifyNever(() => pushSubscriptionsPersistence.remove(any())); - verify(() => pushSubscriptionsPersistence.keys()).called(1); + await storage.updateSubscription('b', subscriptionsMap['b']!); + verify(() => pushSubscriptionsPersistence.setValue('b', serializedSubscriptions['b']!)).called(1); + verifyNever(() => pushSubscriptionsPersistence.setValue(any(), any())); + verifyNever(() => pushSubscriptionsPersistence.remove(any())); + await storage.updateSubscription('c', PushSubscription()); verify(() => pushSubscriptionsPersistence.remove('c')).called(1); verifyNever(() => pushSubscriptionsPersistence.remove(any())); + verifyNever(() => pushSubscriptionsPersistence.setValue(any(), any())); }); }); });