fix(xep): Fix usage of 'max' in publish options (#33)

This commit fixes two issues:
1. Fix an issue where [PubSubManager.publish] would always, if given
   publish options with maxItems set to 'max', use 'max' in the
   max_items publish options, even if the server indicates it does not
   support that.
2. Fix an issue with the StanzaExpectation, where it would let every
   stanza pass.
This commit is contained in:
PapaTutuWawa 2023-03-12 19:11:55 +01:00
parent 324ef9ca29
commit f49eb66bb7
5 changed files with 348 additions and 166 deletions

View File

@ -1,3 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart';
@ -130,7 +132,10 @@ class PubSubManager extends XmppManagerBase {
return count;
}
Future<PubSubPublishOptions> _preprocessPublishOptions(
// TODO(PapaTutuWawa): This should return a Result<T> in case we cannot proceed
// with the requested configuration.
@visibleForTesting
Future<PubSubPublishOptions> preprocessPublishOptions(
String jid,
String node,
PubSubPublishOptions options,
@ -285,7 +290,7 @@ class PubSubManager extends XmppManagerBase {
}) async {
PubSubPublishOptions? pubOptions;
if (options != null) {
pubOptions = await _preprocessPublishOptions(jid, node, options);
pubOptions = await preprocessPublishOptions(jid, node, options);
}
final result = await getAttributes().sendStanza(
@ -310,14 +315,11 @@ class PubSubManager extends XmppManagerBase {
)
],
),
...options != null
? [
if (pubOptions != null)
XMLNode(
tag: 'publish-options',
children: [options.toXml()],
children: [pubOptions.toXml()],
),
]
: [],
],
)
],

View File

@ -0,0 +1,63 @@
import 'dart:async';
import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/connectivity.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/attributes.dart';
import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/reconnect.dart';
import 'package:moxxmpp/src/settings.dart';
import 'package:moxxmpp/src/socket.dart';
import 'package:moxxmpp/src/stringxml.dart';
import '../helpers/xmpp.dart';
/// This class allows registering managers for easier testing.
class TestingManagerHolder {
TestingManagerHolder({
BaseSocketWrapper? socket,
}) : _socket = socket ?? StubTCPSocket([]);
final BaseSocketWrapper _socket;
final Map<String, XmppManagerBase> _managers = {};
static final JID jid = JID.fromString('testuser@example.org/abc123');
static final ConnectionSettings settings = ConnectionSettings(
jid: jid,
password: 'abc123',
useDirectTLS: true,
allowPlainAuth: true,
);
Future<XMLNode> _sendStanza(stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async {
return XMLNode.fromString('<iq />');
}
T? _getManagerById<T extends XmppManagerBase>(String id) {
return _managers[id] as T?;
}
Future<void> register(XmppManagerBase manager) async {
manager.register(
XmppManagerAttributes(
sendStanza: _sendStanza,
getConnection: () => XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
_socket,
),
getConnectionSettings: () => settings,
sendNonza: (_) {},
sendEvent: (_) {},
getSocket: () => _socket,
isFeatureSupported: (_) => false,
getNegotiatorById: getNegotiatorNullStub,
getFullJID: () => jid,
getManagerById: _getManagerById,
),
);
await manager.postRegisterCallback();
_managers[manager.id] = manager;
}
}

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
@ -13,8 +14,8 @@ T? getManagerNullStub<T extends XmppManagerBase>(String id) {
}
abstract class ExpectationBase {
ExpectationBase(this.expectation, this.response);
final String expectation;
final String response;
@ -33,27 +34,84 @@ class StringExpectation extends ExpectationBase {
///
class StanzaExpectation extends ExpectationBase {
StanzaExpectation(String expectation, String response, {this.ignoreId = false, this.adjustId = false }) : super(expectation, response);
final bool ignoreId;
final bool adjustId;
@override
bool matches(String input) {
final ex = XMLNode.fromString(expectation);
final recv = XMLNode.fromString(expectation);
final recv = XMLNode.fromString(input);
return compareXMLNodes(recv, ex, ignoreId: ignoreId);
}
}
class StubTCPSocket extends BaseSocketWrapper { // Request -> Response(s)
/// Use [settings] to build the beginning of a play that can be used with StubTCPSocket. [settings]'s allowPlainAuth must
/// be set to true.
List<ExpectationBase> buildAuthenticatedPlay(ConnectionSettings settings) {
assert(settings.allowPlainAuth, 'SASL PLAIN must be allowed');
final plain = base64.encode(utf8.encode('\u0000${settings.jid.local}\u0000${settings.password}'));
return [
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="${settings.jid.domain}"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>$plain</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />'
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>${settings.jid.toBare()}/MU29eEZn</jid></bind></iq>',
ignoreId: true,
),
StanzaExpectation(
"<presence xmlns='jabber:client' from='${settings.jid.toBare()}/MU29eEZn'><show>chat</show></presence>",
'',
),
];
}
class StubTCPSocket extends BaseSocketWrapper { // Request -> Response(s)
StubTCPSocket(this._play);
StubTCPSocket.authenticated(ConnectionSettings settings, List<ExpectationBase> play) : _play = [
...buildAuthenticatedPlay(settings),
...play,
];
StubTCPSocket({ required List<ExpectationBase> play })
: _play = play,
_dataStream = StreamController<String>.broadcast(),
_eventStream = StreamController<XmppSocketEvent>.broadcast();
int _state = 0;
final StreamController<String> _dataStream;
final StreamController<XmppSocketEvent> _eventStream;
final StreamController<String> _dataStream = StreamController<String>.broadcast();
final StreamController<XmppSocketEvent> _eventStream = StreamController<XmppSocketEvent>.broadcast();
final List<ExpectationBase> _play;
String? lastId;
@ -99,9 +157,11 @@ class StubTCPSocket extends BaseSocketWrapper { // Request -> Response(s)
str = str.substring(0, str.length - 16);
}
if (!expectation.matches(str)) {
expect(true, false, reason: 'Expected ${expectation.expectation}, got $str');
}
expect(
expectation.matches(str),
true,
reason: 'Expected ${expectation.expectation}, got $str',
);
// Make sure to only progress if everything passed so far
_state++;
@ -109,7 +169,7 @@ class StubTCPSocket extends BaseSocketWrapper { // Request -> Response(s)
var response = expectation.response;
if (expectation is StanzaExpectation) {
final inputNode = XMLNode.fromString(str);
lastId = inputNode.attributes['id'];
lastId = inputNode.attributes['id'] as String?;
if (expectation.adjustId) {
final outputNode = XMLNode.fromString(response);

View File

@ -29,4 +29,52 @@ void main() {
expect(compareXMLNodes(node1.firstTag('body')!, XMLNode.fromString('<body>Hallo</body>')), true);
expect(compareXMLNodes(node1.firstTagByXmlns('a')!, XMLNode.fromString('<a xmlns="a" />')), true);
});
test('Test compareXMLNodes', () {
final node1 = XMLNode.fromString('''
<iq type='set' id='0327c373-2e34-46bd-ab7f-1274a6f7095f' to='pubsub.server.example.org' from='testuser@example.org/MU29eEZn' xmlns='jabber:client'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='princely_musings'>
<item id='current'>
<test-item />
</item>
</publish>
<publish-options >
<x xmlns='jabber:x:data' type='submit'>
<field var='FORM_TYPE' type='hidden'>
<value>http://jabber.org/protocol/pubsub#publish-options</value>
</field>
<field var='pubsub#max_items'>
<value>max</value>
</field>
</x>
</publish-options>
</pubsub>
</iq>
''',
);
final node2 = XMLNode.fromString('''
<iq type="set" to="pubsub.server.example.org" id="a">
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='princely_musings'>
<item id="current">
<test-item />
</item>
</publish>
<publish-options>
<x xmlns='jabber:x:data' type='submit'>
<field var='FORM_TYPE' type='hidden'>
<value>http://jabber.org/protocol/pubsub#publish-options</value>
</field>
<field var='pubsub#max_items'>
<value>1</value>
</field>
</x>
</publish-options>
</pubsub>
</iq>
''');
expect(compareXMLNodes(node1, node2, ignoreId: true), false);
});
}

View File

@ -2,121 +2,23 @@ import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import '../helpers/logging.dart';
import '../helpers/manager.dart';
import '../helpers/xmpp.dart';
class StubbedDiscoManager extends DiscoManager {
StubbedDiscoManager() : super([]);
StubbedDiscoManager(this._itemError) : super([]);
final bool _itemError;
@override
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node, bool shouldEncrypt = true }) async {
final result = DiscoInfo.fromQuery(
XMLNode.fromString(
'''<query xmlns='http://jabber.org/protocol/disco#info'>
<identity category='account' type='registered'/>
<identity type='service' category='pubsub' name='PubSub acs-clustered'/>
<feature var='http://jabber.org/protocol/pubsub#retrieve-default'/>
<feature var='http://jabber.org/protocol/pubsub#purge-nodes'/>
<feature var='http://jabber.org/protocol/pubsub#subscribe'/>
<feature var='http://jabber.org/protocol/pubsub#member-affiliation'/>
<feature var='http://jabber.org/protocol/pubsub#subscription-notifications'/>
<feature var='http://jabber.org/protocol/pubsub#create-nodes'/>
<feature var='http://jabber.org/protocol/pubsub#outcast-affiliation'/>
<feature var='http://jabber.org/protocol/pubsub#get-pending'/>
<feature var='http://jabber.org/protocol/pubsub#presence-notifications'/>
<feature var='urn:xmpp:ping'/>
<feature var='http://jabber.org/protocol/pubsub#delete-nodes'/>
<feature var='http://jabber.org/protocol/pubsub#config-node'/>
<feature var='http://jabber.org/protocol/pubsub#retrieve-items'/>
<feature var='http://jabber.org/protocol/pubsub#access-whitelist'/>
<feature var='http://jabber.org/protocol/pubsub#access-presence'/>
<feature var='http://jabber.org/protocol/disco#items'/>
<feature var='http://jabber.org/protocol/pubsub#meta-data'/>
<feature var='http://jabber.org/protocol/pubsub#multi-items'/>
<feature var='http://jabber.org/protocol/pubsub#item-ids'/>
<feature var='urn:xmpp:mam:1'/>
<feature var='http://jabber.org/protocol/pubsub#instant-nodes'/>
<feature var='urn:xmpp:mam:2'/>
<feature var='urn:xmpp:mam:2#extended'/>
<feature var='http://jabber.org/protocol/pubsub#modify-affiliations'/>
<feature var='http://jabber.org/protocol/pubsub#multi-collection'/>
<feature var='http://jabber.org/protocol/pubsub#persistent-items'/>
<feature var='http://jabber.org/protocol/pubsub#create-and-configure'/>
<feature var='http://jabber.org/protocol/pubsub#publisher-affiliation'/>
<feature var='http://jabber.org/protocol/pubsub#access-open'/>
<feature var='http://jabber.org/protocol/pubsub#retrieve-affiliations'/>
<feature var='http://jabber.org/protocol/pubsub#access-authorize'/>
<feature var='jabber:iq:version'/>
<feature var='http://jabber.org/protocol/pubsub#retract-items'/>
<feature var='http://jabber.org/protocol/pubsub#manage-subscriptions'/>
<feature var='http://jabber.org/protocol/commands'/>
<feature var='http://jabber.org/protocol/pubsub#auto-subscribe'/>
<feature var='http://jabber.org/protocol/pubsub#publish-options'/>
<feature var='http://jabber.org/protocol/pubsub#access-roster'/>
<feature var='http://jabber.org/protocol/pubsub#publish'/>
<feature var='http://jabber.org/protocol/pubsub#collections'/>
<feature var='http://jabber.org/protocol/pubsub#retrieve-subscriptions'/>
<feature var='http://jabber.org/protocol/disco#info'/>
<x type='result' xmlns='jabber:x:data'>
<field type='hidden' var='FORM_TYPE'>
<value>http://jabber.org/network/serverinfo</value>
</field>
<field type='list-multi' var='abuse-addresses'>
<value>mailto:support@tigase.net</value>
<value>xmpp:tigase@mix.tigase.im</value>
<value>xmpp:tigase@muc.tigase.org</value>
<value>https://tigase.net/technical-support</value>
</field>
</x>
<feature var='http://jabber.org/protocol/pubsub#auto-create'/>
<feature var='http://jabber.org/protocol/pubsub#auto-subscribe'/>
<feature var='urn:xmpp:mix:pam:2'/>
<feature var='urn:xmpp:carbons:2'/>
<feature var='urn:xmpp:carbons:rules:0'/>
<feature var='jabber:iq:auth'/>
<feature var='vcard-temp'/>
<feature var='http://jabber.org/protocol/amp'/>
<feature var='msgoffline'/>
<feature var='http://jabber.org/protocol/disco#info'/>
<feature var='http://jabber.org/protocol/disco#items'/>
<feature var='urn:xmpp:blocking'/>
<feature var='urn:xmpp:reporting:0'/>
<feature var='urn:xmpp:reporting:abuse:0'/>
<feature var='urn:xmpp:reporting:spam:0'/>
<feature var='urn:xmpp:reporting:1'/>
<feature var='urn:xmpp:ping'/>
<feature var='urn:ietf:params:xml:ns:xmpp-sasl'/>
<feature var='http://jabber.org/protocol/pubsub'/>
<feature var='http://jabber.org/protocol/pubsub#owner'/>
<feature var='http://jabber.org/protocol/pubsub#publish'/>
<identity type='pep' category='pubsub'/>
<feature var='urn:xmpp:pep-vcard-conversion:0'/>
<feature var='urn:xmpp:bookmarks-conversion:0'/>
<feature var='urn:xmpp:archive:auto'/>
<feature var='urn:xmpp:archive:manage'/>
<feature var='urn:xmpp:push:0'/>
<feature var='tigase:push:away:0'/>
<feature var='tigase:push:encrypt:0'/>
<feature var='tigase:push:encrypt:aes-128-gcm'/>
<feature var='tigase:push:filter:ignore-unknown:0'/>
<feature var='tigase:push:filter:groupchat:0'/>
<feature var='tigase:push:filter:muted:0'/>
<feature var='tigase:push:priority:0'/>
<feature var='tigase:push:jingle:0'/>
<feature var='jabber:iq:roster'/>
<feature var='jabber:iq:roster-dynamic'/>
<feature var='urn:xmpp:mam:1'/>
<feature var='urn:xmpp:mam:2'/>
<feature var='urn:xmpp:mam:2#extended'/>
<feature var='urn:xmpp:mix:pam:2#archive'/>
<feature var='jabber:iq:version'/>
<feature var='urn:xmpp:time'/>
<feature var='jabber:iq:privacy'/>
<feature var='urn:ietf:params:xml:ns:xmpp-bind'/>
<feature var='urn:xmpp:extdisco:2'/>
<feature var='http://jabber.org/protocol/commands'/>
<feature var='urn:ietf:params:xml:ns:vcard-4.0'/>
<feature var='jabber:iq:private'/>
<feature var='urn:ietf:params:xml:ns:xmpp-session'/>
'''
<query xmlns='http://jabber.org/protocol/disco#info'>
<identity category='pubsub' type='service' />
<feature var="http://jabber.org/protocol/pubsub" />
<feature var="http://jabber.org/protocol/pubsub#multi-items" />
</query>'''
),
JID.fromString('pubsub.server.example.org'),
@ -124,49 +26,156 @@ class StubbedDiscoManager extends DiscoManager {
return Result(result);
}
}
T? getDiscoManagerStub<T extends XmppManagerBase>(String id) {
return StubbedDiscoManager() as T;
@override
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(String entity, {String? node, bool shouldEncrypt = true}) async {
if (_itemError) {
return Result(
UnknownDiscoError(),
);
}
return const Result<DiscoError, List<DiscoItem>>(
<DiscoItem>[],
);
}
}
void main() {
initLogger();
test('Test publishing with pubsub#max_items when the server does not support it', () async {
XMLNode? sent;
test('Test pre-processing with pubsub#max_items when the server does not support it (1/2)', () async {
final manager = PubSubManager();
manager.register(
XmppManagerAttributes(
sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async {
sent = stanza;
final TestingManagerHolder tm = TestingManagerHolder();
await tm.register(StubbedDiscoManager(false));
await tm.register(manager);
return XMLNode.fromString('<iq />');
},
sendNonza: (_) {},
sendEvent: (_) {},
getManagerById: getDiscoManagerStub,
getConnectionSettings: () => ConnectionSettings(
jid: JID.fromString('hallo@example.server'),
password: 'password',
useDirectTLS: true,
allowPlainAuth: false,
),
isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('hallo@example.server/uwu'),
getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
getNegotiatorById: getNegotiatorNullStub,
),
final result = await manager.preprocessPublishOptions(
'pubsub.server.example.org',
'urn:xmpp:omemo:2:bundles',
const PubSubPublishOptions(maxItems: 'max'),
);
// final result = await manager.preprocessPublishOptions(
// 'pubsub.server.example.org',
// 'example:node',
// PubSubPublishOptions(
// maxItems: 'max',
// ),
// );
expect(result.maxItems, '1');
});
test('Test pre-processing with pubsub#max_items when the server does not support it (2/2)', () async {
final manager = PubSubManager();
final TestingManagerHolder tm = TestingManagerHolder();
await tm.register(StubbedDiscoManager(true));
await tm.register(manager);
final result = await manager.preprocessPublishOptions(
'pubsub.server.example.org',
'urn:xmpp:omemo:2:bundles',
const PubSubPublishOptions(maxItems: 'max'),
);
expect(result.maxItems, '1');
});
test('Test publishing with pubsub#max_items when the server does not support it', () async {
final socket = StubTCPSocket.authenticated(
TestingManagerHolder.settings,
[
StanzaExpectation(
'''
<iq type="get" to="pubsub.server.example.org" id="a" from="testuser@example.org/MU29eEZn" xmlns="jabber:client">
<query xmlns="http://jabber.org/protocol/disco#info" />
</iq>
''',
'''
<iq type="result" from="pubsub.server.example.org" id="a" xmlns="jabber:client">
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category='pubsub' type='service' />
<feature var="http://jabber.org/protocol/pubsub" />
<feature var="http://jabber.org/protocol/pubsub#multi-items" />
</query>
</iq>
''',
ignoreId: true,
adjustId: true,
),
StanzaExpectation(
'''
<iq type="get" to="pubsub.server.example.org" id="a" from="testuser@example.org/MU29eEZn" xmlns="jabber:client">
<query xmlns="http://jabber.org/protocol/disco#items" node="princely_musings" />
</iq>
''',
'''
<iq type="result" from="pubsub.server.example.org" id="a" xmlns="jabber:client">
<query xmlns="http://jabber.org/protocol/disco#items" node="princely_musings" />
</iq>
''',
ignoreId: true,
adjustId: true,
),
StanzaExpectation(
'''
<iq type="set" to="pubsub.server.example.org" id="a" from="testuser@example.org/MU29eEZn" xmlns="jabber:client">
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='princely_musings'>
<item id="current">
<test-item />
</item>
</publish>
<publish-options>
<x xmlns='jabber:x:data' type='submit'>
<field var='FORM_TYPE' type='hidden'>
<value>http://jabber.org/protocol/pubsub#publish-options</value>
</field>
<field var='pubsub#max_items'>
<value>1</value>
</field>
</x>
</publish-options>
</pubsub>
</iq>''',
'''
<iq type="result" from="pubsub.server.example.org" id="a" xmlns="jabber:client">
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='princely_musings'>
<item id='current'/>
</publish>
</pubsub>
</iq>''',
ignoreId: true,
adjustId: true,
)
],
);
final connection = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
socket,
);
await connection.registerManagers([
PubSubManager(),
DiscoManager([]),
PresenceManager(),
MessageManager(),
RosterManager(TestingRosterStateManager(null, [])),
PingManager(),
]);
connection..registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
])
..setConnectionSettings(TestingManagerHolder.settings);
await connection.connect(
waitUntilLogin: true,
);
final item = XMLNode(tag: "test-item");
final result = await connection.getManagerById<PubSubManager>(pubsubManager)!.publish(
'pubsub.server.example.org',
'princely_musings',
item,
id: 'current',
options: const PubSubPublishOptions(maxItems: 'max'),
);
expect(result.isType<bool>(), true);
});
}