fix(core): An empty iq is okay with roster versioning
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
PapaTutuWawa 2023-10-25 21:03:04 +02:00
parent ddf781daff
commit d4166d087e
3 changed files with 76 additions and 3 deletions

View File

@ -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 <query /> 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<Result<RosterRequestResult, RosterError>> _handleRosterResponse(
XMLNode? query,
String? requestedRosterVersion,
) async {
final List<XmppRosterItem> 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 <query /> 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<Result<RosterRequestResult?, RosterError>>
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() {

View File

@ -89,6 +89,7 @@ List<ExpectationBase> buildAuthenticatedPlay(ConnectionSettings settings) {
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<ver xmlns='urn:xmpp:features:rosterver'/>
</stream:features>
''',
),

View File

@ -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(
'<iq xmlns="jabber:client" id="r1h3vzp7" type="get"><query xmlns="jabber:iq:roster" ver="ver14"/></iq>',
'<iq xmlns="jabber:client" id="r1h3vzp7" type="result" />',
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<RosterRequestResult>(), true);
final result = rawResult.get<RosterRequestResult>();
expect(result.items.isEmpty, true);
});
}