diff --git a/packages/moxxmpp/lib/src/roster/roster.dart b/packages/moxxmpp/lib/src/roster/roster.dart index 7a51efc..ff4c640 100644 --- a/packages/moxxmpp/lib/src/roster/roster.dart +++ b/packages/moxxmpp/lib/src/roster/roster.dart @@ -182,8 +182,18 @@ class RosterManager extends XmppManagerBase { /// Shared code between requesting rosters without and with roster versioning, if /// the server deems a regular roster response more efficient than n roster pushes. + /// + /// [query] is the child of the iq, if available. + /// + /// If roster versioning was used, then [requestedRosterVersion] is the version + /// we requested the roster with. + /// + /// Note that if roster versioning is used and the server returns us an empty iq, + /// it means that the roster did not change since the last version. In that case, + /// we do nothing and just return. The roster state manager will not be notified. Future> _handleRosterResponse( XMLNode? query, + String? requestedRosterVersion, ) async { final List items; String? rosterVersion; @@ -204,6 +214,14 @@ class RosterManager extends XmppManagerBase { .toList(); rosterVersion = query.attributes['ver'] as String?; + } else if (requestedRosterVersion != null) { + // Skip the handleRosterFetch call since nothing changed. + return Result( + RosterRequestResult( + [], + requestedRosterVersion, + ), + ); } else { logger.warning( 'Server response to roster request without roster versioning does not contain a element, while the type is not error. This violates RFC6121', @@ -258,7 +276,7 @@ class RosterManager extends XmppManagerBase { } final responseQuery = response.firstTag('query', xmlns: rosterXmlns); - return _handleRosterResponse(responseQuery); + return _handleRosterResponse(responseQuery, rosterVersion); } /// Requests a series of roster pushes according to RFC6121. Requires that the server @@ -266,6 +284,7 @@ class RosterManager extends XmppManagerBase { Future> requestRosterPushes() async { final attrs = getAttributes(); + final rosterVersion = await _stateManager.getRosterVersion(); final result = (await attrs.sendStanza( StanzaDetails( Stanza.iq( @@ -275,7 +294,7 @@ class RosterManager extends XmppManagerBase { tag: 'query', xmlns: rosterXmlns, attributes: { - 'ver': await _stateManager.getRosterVersion() ?? '', + 'ver': rosterVersion ?? '', }, ), ], @@ -289,7 +308,7 @@ class RosterManager extends XmppManagerBase { } final query = result.firstTag('query', xmlns: rosterXmlns); - return _handleRosterResponse(query); + return _handleRosterResponse(query, rosterVersion); } bool rosterVersioningAvailable() { diff --git a/packages/moxxmpp/test/helpers/xmpp.dart b/packages/moxxmpp/test/helpers/xmpp.dart index 4d25475..9d2dc41 100644 --- a/packages/moxxmpp/test/helpers/xmpp.dart +++ b/packages/moxxmpp/test/helpers/xmpp.dart @@ -89,6 +89,7 @@ List buildAuthenticatedPlay(ConnectionSettings settings) { + ''', ), diff --git a/packages/moxxmpp/test/roster_test.dart b/packages/moxxmpp/test/roster_test.dart new file mode 100644 index 0000000..a76b7b2 --- /dev/null +++ b/packages/moxxmpp/test/roster_test.dart @@ -0,0 +1,53 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; +import 'helpers/logging.dart'; +import 'helpers/xmpp.dart'; + +void main() { + initLogger(); + + test('Test a versioned roster fetch returning an empty iq', () async { + final sm = TestingRosterStateManager('ver14', []); + final rm = RosterManager(sm); + final cs = ConnectionSettings( + jid: JID.fromString('user@example.org'), + password: 'abc123', + ); + final socket = StubTCPSocket([ + ...buildAuthenticatedPlay(cs), + StanzaExpectation( + '', + '', + ignoreId: true, + adjustId: true, + ), + ]); + final conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + ClientToServerNegotiator(), + socket, + )..connectionSettings = cs; + await conn.registerManagers([ + rm, + PresenceManager(), + ]); + await conn.registerFeatureNegotiators([ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + RosterFeatureNegotiator(), + ]); + + // Connect + await conn.connect( + shouldReconnect: false, + waitUntilLogin: true, + ); + + // Request the roster + final rawResult = await rm.requestRoster(); + expect(rawResult.isType(), true); + final result = rawResult.get(); + expect(result.items.isEmpty, true); + }); +}