From e2c8f79429e8b9bd8a8a42fdefe83a13d1623762 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 7 Jan 2023 15:51:10 +0100 Subject: [PATCH] feat: Add a management class for roster state --- packages/moxxmpp/lib/moxxmpp.dart | 1 + packages/moxxmpp/lib/src/roster/roster.dart | 5 +- packages/moxxmpp/lib/src/roster/state.dart | 142 ++++++++ packages/moxxmpp/test/roster_state_test.dart | 93 ++++++ packages/moxxmpp/test/roster_test.dart | 322 ------------------- 5 files changed, 239 insertions(+), 324 deletions(-) create mode 100644 packages/moxxmpp/lib/src/roster/state.dart create mode 100644 packages/moxxmpp/test/roster_state_test.dart delete mode 100644 packages/moxxmpp/test/roster_test.dart diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index 7336a71..542b46d 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -29,6 +29,7 @@ export 'package:moxxmpp/src/rfcs/rfc_2782.dart'; export 'package:moxxmpp/src/rfcs/rfc_4790.dart'; export 'package:moxxmpp/src/roster/errors.dart'; export 'package:moxxmpp/src/roster/roster.dart'; +export 'package:moxxmpp/src/roster/state.dart'; export 'package:moxxmpp/src/settings.dart'; export 'package:moxxmpp/src/socket.dart'; export 'package:moxxmpp/src/stanza.dart'; diff --git a/packages/moxxmpp/lib/src/roster/roster.dart b/packages/moxxmpp/lib/src/roster/roster.dart index 2534a3c..cc3583e 100644 --- a/packages/moxxmpp/lib/src/roster/roster.dart +++ b/packages/moxxmpp/lib/src/roster/roster.dart @@ -65,7 +65,6 @@ class RosterFeatureNegotiator extends XmppFeatureNegotiatorBase { /// This manager requires a RosterFeatureNegotiator to be registered. class RosterManager extends XmppManagerBase { - RosterManager() : _rosterVersion = null, super(); String? _rosterVersion; @@ -88,8 +87,10 @@ class RosterManager extends XmppManagerBase { @override Future isSupported() async => true; - /// Override-able functions + /// Commit the current roster to storage. Future commitLastRosterVersion(String version) async {} + + /// Load the last roster data Future loadLastRosterVersion() async {} void setRosterVersion(String ver) { diff --git a/packages/moxxmpp/lib/src/roster/state.dart b/packages/moxxmpp/lib/src/roster/state.dart new file mode 100644 index 0000000..6d696b9 --- /dev/null +++ b/packages/moxxmpp/lib/src/roster/state.dart @@ -0,0 +1,142 @@ +import 'package:meta/meta.dart'; +import 'package:moxxmpp/src/roster/roster.dart'; + +class _RosterProcessTriple { + const _RosterProcessTriple(this.removed, this.modified, this.added); + final String? removed; + final XmppRosterItem? modified; + final XmppRosterItem? added; +} + +class RosterCacheLoadResult { + const RosterCacheLoadResult(this.version, this.roster); + final String? version; + final List roster; +} + +abstract class BaseRosterStateManager { + List? currentRoster; + String? currentVersion; + + Future loadRosterCache(); + + Future commitRoster(String? version, List removed, List modified, List added); + + Future _loadRosterCache() async { + if (currentRoster == null) { + final result = await loadRosterCache(); + + currentRoster = result.roster; + currentVersion = result.version; + } + } + + _RosterProcessTriple _handleRosterItem(XmppRosterItem item) { + if (item.subscription == 'remove') { + // The item has been removed + currentRoster!.removeWhere((i) => i.jid == item.jid); + return _RosterProcessTriple( + item.jid, + null, + null, + ); + } + + final index = currentRoster!.indexWhere((i) => i.jid == item.jid); + if (index == -1) { + // The item does not exist + currentRoster!.add(item); + return _RosterProcessTriple( + null, + null, + item, + ); + } else { + // The item is updated + currentRoster![index] = item; + return _RosterProcessTriple( + null, + item, + null, + ); + } + } + + Future handleRosterPush(RosterPushEvent event) async { + await _loadRosterCache(); + + currentVersion = event.ver; + final result = _handleRosterItem(event.item); + + if (result.removed != null) { + return commitRoster( + currentVersion, + [result.removed!], + [], + [], + ); + } else if (result.modified != null) { + return commitRoster( + currentVersion, + [], + [result.modified!], + [], + ); + } else if (result.added != null) { + return commitRoster( + currentVersion, + [], + [], + [result.added!], + ); + } + } + + Future handleRosterFetch(RosterRequestResult result) async { + final removed = List.empty(growable: true); + final modified = List.empty(growable: true); + final added = List.empty(growable: true); + + await _loadRosterCache(); + + currentVersion = result.ver; + for (final item in result.items) { + final result = _handleRosterItem(item); + + if (result.removed != null) removed.add(result.removed!); + if (result.modified != null) modified.add(result.modified!); + if (result.added != null) added.add(result.added!); + } + + await commitRoster( + currentVersion, + removed, + modified, + added, + ); + } +} + +@visibleForTesting +class TestingRosterStateManager extends BaseRosterStateManager { + TestingRosterStateManager( + this.initialRosterVersion, + this.initialRoster, + ); + final String? initialRosterVersion; + final List initialRoster; + int loadCount = 0; + + @override + Future loadRosterCache() async { + loadCount++; + return RosterCacheLoadResult( + initialRosterVersion, + initialRoster, + ); + } + + @override + Future commitRoster(String? version, List removed, List modified, List added) async {} + +} diff --git a/packages/moxxmpp/test/roster_state_test.dart b/packages/moxxmpp/test/roster_state_test.dart new file mode 100644 index 0000000..6fd5ebe --- /dev/null +++ b/packages/moxxmpp/test/roster_state_test.dart @@ -0,0 +1,93 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; + +void main() { + test('Test receiving a roster push', () async { + final rs = TestingRosterStateManager(null, []); + + await rs.handleRosterPush( + RosterPushEvent( + item: XmppRosterItem( + jid: 'testuser@server.example', + subscription: 'both', + ), + ), + ); + + expect( + rs.currentRoster!.indexWhere((item) => item.jid == 'testuser@server.example') != -1, + true, + ); + expect(rs.loadCount, 1); + expect(rs.currentRoster!.length, 1); + + // Receive another roster push + await rs.handleRosterPush( + RosterPushEvent( + item: XmppRosterItem( + jid: 'testuser2@server2.example', + subscription: 'to', + ), + ), + ); + + expect( + rs.currentRoster!.indexWhere((item) => item.jid == 'testuser2@server2.example') != -1, + true, + ); + expect(rs.loadCount, 1); + expect(rs.currentRoster!.length, 2); + + // Remove one of the items + await rs.handleRosterPush( + RosterPushEvent( + item: XmppRosterItem( + jid: 'testuser2@server2.example', + subscription: 'remove', + ), + ), + ); + + expect( + rs.currentRoster!.indexWhere((item) => item.jid == 'testuser2@server2.example') == -1, + true, + ); + expect( + rs.currentRoster!.indexWhere((item) => item.jid == 'testuser@server.example') != 1, + true, + ); + expect(rs.loadCount, 1); + expect(rs.currentRoster!.length, 1); + }); + + test('Test a roster fetch', () async { + final rs = TestingRosterStateManager(null, []); + + // Fetch the roster + await rs.handleRosterFetch( + RosterRequestResult( + items: [ + XmppRosterItem( + jid: 'testuser@server.example', + subscription: 'both', + ), + XmppRosterItem( + jid: 'testuser2@server2.example', + subscription: 'to', + ), + XmppRosterItem( + jid: 'testuser3@server3.example', + subscription: 'from', + ), + ], + ver: 'aaaaaaaa', + ), + ); + + expect(rs.loadCount, 1); + expect(rs.currentRoster!.length, 3); + expect(rs.currentRoster!.indexWhere((item) => item.jid == 'testuser@server.example') != -1, true); + expect(rs.currentRoster!.indexWhere((item) => item.jid == 'testuser2@server2.example') != -1, true); + expect(rs.currentRoster!.indexWhere((item) => item.jid == 'testuser3@server3.example') != -1, true); + }); +} diff --git a/packages/moxxmpp/test/roster_test.dart b/packages/moxxmpp/test/roster_test.dart deleted file mode 100644 index 8842b11..0000000 --- a/packages/moxxmpp/test/roster_test.dart +++ /dev/null @@ -1,322 +0,0 @@ -import 'package:moxxmpp/moxxmpp.dart'; -import 'package:test/test.dart'; - -// TODO(PapaTutuWawa): Fix tests - -typedef AddRosterItemFunction = Future Function( - String avatarUrl, - String avatarHash, - String jid, - String title, - String subscription, - String ask, - { - List groups, - } -); - -typedef UpdateRosterItemFunction = Future Function( - int id, { - String? avatarUrl, - String? avatarHash, - String? title, - String? subscription, - String? ask, - List? groups, - } -); - -AddRosterItemFunction mkAddRosterItem(void Function(String) callback) { - return ( - String avatarUrl, - String avatarHash, - String jid, - String title, - String subscription, - String ask, - { - List groups = const [], - } - ) async { - callback(jid); - return await addRosterItemFromData( - avatarUrl, - avatarHash, - jid, - title, - subscription, - ask, - groups: groups, - ); - }; -} - -Future addRosterItemFromData( - String avatarUrl, - String avatarHash, - String jid, - String title, - String subscription, - String ask, - { - List groups = const [], - } -) async => RosterItem( - 0, - avatarUrl, - avatarHash, - jid, - title, - subscription, - ask, - groups, -); - -UpdateRosterItemFunction mkRosterUpdate(List roster) { - return ( - int id, { - String? avatarUrl, - String? avatarHash, - String? title, - String? subscription, - String? ask, - List? groups, - } - ) async { - final item = firstWhereOrNull(roster, (RosterItem item) => item.id == id)!; - return item.copyWith( - avatarUrl: avatarUrl ?? item.avatarUrl, - avatarHash: avatarHash ?? item.avatarHash, - title: title ?? item.title, - subscription: subscription ?? item.subscription, - ask: ask ?? item.ask, - groups: groups ?? item.groups, - ); - }; -} - -void main() { - final localRosterSingle = [ - RosterItem( - 0, - '', - '', - 'hallo@server.example', - 'hallo', - 'none', - '', - [], - ) - ]; - final localRosterDouble = [ - RosterItem( - 0, - '', - '', - 'hallo@server.example', - 'hallo', - 'none', - '', - [], - ), - RosterItem( - 1, - '', - '', - 'welt@different.server.example', - 'welt', - 'from', - '', - [ 'Friends' ], - ) - ]; - - group('Test roster pushes', () { - test('Test removing an item', () async { - var removeCalled = false; - var addCalled = false; - final result = await processRosterDiff( - localRosterDouble, - [ - XmppRosterItem( - jid: 'hallo@server.example', subscription: 'remove', - ) - ], - true, - mkAddRosterItem((_) { addCalled = true; }), - mkRosterUpdate(localRosterDouble), - (jid) async { - if (jid == 'hallo@server.example') { - removeCalled = true; - } - }, - (_) async => null, - (_, { String? id }) async {}, - ); - - expect(result.removed, [ 'hallo@server.example' ]); - expect(result.modified.length, 0); - expect(result.added.length, 0); - expect(removeCalled, true); - expect(addCalled, false); - }); - - test('Test adding an item', () async { - var removeCalled = false; - var addCalled = false; - final result = await processRosterDiff( - localRosterSingle, - [ - XmppRosterItem( - jid: 'welt@different.server.example', - subscription: 'from', - ) - ], - true, - mkAddRosterItem( - (jid) { - if (jid == 'welt@different.server.example') { - addCalled = true; - } - } - ), - mkRosterUpdate(localRosterSingle), - (_) async { removeCalled = true; }, - (_) async => null, - (_, { String? id }) async {}, - ); - - expect(result.removed, [ ]); - expect(result.modified.length, 0); - expect(result.added.length, 1); - expect(result.added.first.subscription, 'from'); - expect(removeCalled, false); - expect(addCalled, true); - }); - - test('Test modifying an item', () async { - var removeCalled = false; - var addCalled = false; - final result = await processRosterDiff( - localRosterDouble, - [ - XmppRosterItem( - jid: 'welt@different.server.example', - subscription: 'both', - name: 'The World', - ) - ], - true, - mkAddRosterItem((_) { addCalled = false; }), - mkRosterUpdate(localRosterDouble), - (_) async { removeCalled = true; }, - (_) async => null, - (_, { String? id }) async {}, - ); - - expect(result.removed, [ ]); - expect(result.modified.length, 1); - expect(result.added.length, 0); - expect(result.modified.first.subscription, 'both'); - expect(result.modified.first.jid, 'welt@different.server.example'); - expect(result.modified.first.title, 'The World'); - expect(removeCalled, false); - expect(addCalled, false); - }); - }); - - group('Test roster requests', () { - test('Test removing an item', () async { - var removeCalled = false; - var addCalled = false; - final result = await processRosterDiff( - localRosterSingle, - [], - false, - mkAddRosterItem((_) { addCalled = true; }), - mkRosterUpdate(localRosterDouble), - (jid) async { - if (jid == 'hallo@server.example') { - removeCalled = true; - } - }, - (_) async => null, - (_, { String? id }) async {}, - ); - - expect(result.removed, [ 'hallo@server.example' ]); - expect(result.modified.length, 0); - expect(result.added.length, 0); - expect(removeCalled, true); - expect(addCalled, false); - }); - - test('Test adding an item', () async { - var removeCalled = false; - var addCalled = false; - final result = await processRosterDiff( - localRosterSingle, - [ - XmppRosterItem( - jid: 'hallo@server.example', - name: 'hallo', - subscription: 'none', - ), - XmppRosterItem( - jid: 'welt@different.server.example', - subscription: 'both', - ) - ], - false, - mkAddRosterItem( - (jid) { - if (jid == 'welt@different.server.example') { - addCalled = true; - } - } - ), - mkRosterUpdate(localRosterSingle), - (_) async { removeCalled = true; }, - (_) async => null, - (_, { String? id }) async {}, - ); - - expect(result.removed, [ ]); - expect(result.modified.length, 0); - expect(result.added.length, 1); - expect(result.added.first.subscription, 'both'); - expect(removeCalled, false); - expect(addCalled, true); - }); - - test('Test modifying an item', () async { - var removeCalled = false; - var addCalled = false; - final result = await processRosterDiff( - localRosterSingle, - [ - XmppRosterItem( - jid: 'hallo@server.example', - subscription: 'both', - name: 'Hallo Welt', - ) - ], - false, - mkAddRosterItem((_) { addCalled = false; }), - mkRosterUpdate(localRosterDouble), - (_) async { removeCalled = true; }, - (_) async => null, - (_, { String? id }) async {}, - ); - - expect(result.removed, [ ]); - expect(result.modified.length, 1); - expect(result.added.length, 0); - expect(result.modified.first.subscription, 'both'); - expect(result.modified.first.jid, 'hallo@server.example'); - expect(result.modified.first.title, 'Hallo Welt'); - expect(removeCalled, false); - expect(addCalled, false); - }); - }); -}