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/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@ -130,7 +132,10 @@ class PubSubManager extends XmppManagerBase {
return count; 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 jid,
String node, String node,
PubSubPublishOptions options, PubSubPublishOptions options,
@ -285,7 +290,7 @@ class PubSubManager extends XmppManagerBase {
}) async { }) async {
PubSubPublishOptions? pubOptions; PubSubPublishOptions? pubOptions;
if (options != null) { if (options != null) {
pubOptions = await _preprocessPublishOptions(jid, node, options); pubOptions = await preprocessPublishOptions(jid, node, options);
} }
final result = await getAttributes().sendStanza( final result = await getAttributes().sendStanza(
@ -310,14 +315,11 @@ class PubSubManager extends XmppManagerBase {
) )
], ],
), ),
...options != null if (pubOptions != null)
? [ XMLNode(
XMLNode( tag: 'publish-options',
tag: 'publish-options', children: [pubOptions.toXml()],
children: [options.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:async';
import 'dart:convert';
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -13,8 +14,8 @@ T? getManagerNullStub<T extends XmppManagerBase>(String id) {
} }
abstract class ExpectationBase { abstract class ExpectationBase {
ExpectationBase(this.expectation, this.response); ExpectationBase(this.expectation, this.response);
final String expectation; final String expectation;
final String response; final String response;
@ -33,27 +34,84 @@ class StringExpectation extends ExpectationBase {
/// ///
class StanzaExpectation extends ExpectationBase { class StanzaExpectation extends ExpectationBase {
StanzaExpectation(String expectation, String response, {this.ignoreId = false, this.adjustId = false }) : super(expectation, response); StanzaExpectation(String expectation, String response, {this.ignoreId = false, this.adjustId = false }) : super(expectation, response);
final bool ignoreId; final bool ignoreId;
final bool adjustId; final bool adjustId;
@override @override
bool matches(String input) { bool matches(String input) {
final ex = XMLNode.fromString(expectation); final ex = XMLNode.fromString(expectation);
final recv = XMLNode.fromString(expectation); final recv = XMLNode.fromString(input);
return compareXMLNodes(recv, ex, ignoreId: ignoreId); 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; int _state = 0;
final StreamController<String> _dataStream; final StreamController<String> _dataStream = StreamController<String>.broadcast();
final StreamController<XmppSocketEvent> _eventStream; final StreamController<XmppSocketEvent> _eventStream = StreamController<XmppSocketEvent>.broadcast();
final List<ExpectationBase> _play; final List<ExpectationBase> _play;
String? lastId; String? lastId;
@ -99,9 +157,11 @@ class StubTCPSocket extends BaseSocketWrapper { // Request -> Response(s)
str = str.substring(0, str.length - 16); str = str.substring(0, str.length - 16);
} }
if (!expectation.matches(str)) { expect(
expect(true, false, reason: 'Expected ${expectation.expectation}, got $str'); expectation.matches(str),
} true,
reason: 'Expected ${expectation.expectation}, got $str',
);
// Make sure to only progress if everything passed so far // Make sure to only progress if everything passed so far
_state++; _state++;
@ -109,7 +169,7 @@ class StubTCPSocket extends BaseSocketWrapper { // Request -> Response(s)
var response = expectation.response; var response = expectation.response;
if (expectation is StanzaExpectation) { if (expectation is StanzaExpectation) {
final inputNode = XMLNode.fromString(str); final inputNode = XMLNode.fromString(str);
lastId = inputNode.attributes['id']; lastId = inputNode.attributes['id'] as String?;
if (expectation.adjustId) { if (expectation.adjustId) {
final outputNode = XMLNode.fromString(response); final outputNode = XMLNode.fromString(response);
@ -134,4 +194,4 @@ class StubTCPSocket extends BaseSocketWrapper { // Request -> Response(s)
@override @override
bool managesKeepalives() => false; bool managesKeepalives() => false;
} }

View File

@ -29,4 +29,52 @@ void main() {
expect(compareXMLNodes(node1.firstTag('body')!, XMLNode.fromString('<body>Hallo</body>')), true); expect(compareXMLNodes(node1.firstTag('body')!, XMLNode.fromString('<body>Hallo</body>')), true);
expect(compareXMLNodes(node1.firstTagByXmlns('a')!, XMLNode.fromString('<a xmlns="a" />')), 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 'package:test/test.dart';
import '../helpers/logging.dart'; import '../helpers/logging.dart';
import '../helpers/manager.dart';
import '../helpers/xmpp.dart'; import '../helpers/xmpp.dart';
class StubbedDiscoManager extends DiscoManager { class StubbedDiscoManager extends DiscoManager {
StubbedDiscoManager() : super([]); StubbedDiscoManager(this._itemError) : super([]);
final bool _itemError;
@override @override
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node, bool shouldEncrypt = true }) async { Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node, bool shouldEncrypt = true }) async {
final result = DiscoInfo.fromQuery( final result = DiscoInfo.fromQuery(
XMLNode.fromString( XMLNode.fromString(
'''<query xmlns='http://jabber.org/protocol/disco#info'> '''
<identity category='account' type='registered'/> <query xmlns='http://jabber.org/protocol/disco#info'>
<identity type='service' category='pubsub' name='PubSub acs-clustered'/> <identity category='pubsub' type='service' />
<feature var='http://jabber.org/protocol/pubsub#retrieve-default'/> <feature var="http://jabber.org/protocol/pubsub" />
<feature var='http://jabber.org/protocol/pubsub#purge-nodes'/> <feature var="http://jabber.org/protocol/pubsub#multi-items" />
<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>''' </query>'''
), ),
JID.fromString('pubsub.server.example.org'), JID.fromString('pubsub.server.example.org'),
@ -124,49 +26,156 @@ class StubbedDiscoManager extends DiscoManager {
return Result(result); return Result(result);
} }
}
T? getDiscoManagerStub<T extends XmppManagerBase>(String id) { @override
return StubbedDiscoManager() as T; 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() { void main() {
initLogger(); initLogger();
test('Test publishing with pubsub#max_items when the server does not support it', () async { test('Test pre-processing with pubsub#max_items when the server does not support it (1/2)', () async {
XMLNode? sent;
final manager = PubSubManager(); final manager = PubSubManager();
manager.register( final TestingManagerHolder tm = TestingManagerHolder();
XmppManagerAttributes( await tm.register(StubbedDiscoManager(false));
sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async { await tm.register(manager);
sent = stanza;
return XMLNode.fromString('<iq />'); final result = await manager.preprocessPublishOptions(
}, 'pubsub.server.example.org',
sendNonza: (_) {}, 'urn:xmpp:omemo:2:bundles',
sendEvent: (_) {}, const PubSubPublishOptions(maxItems: 'max'),
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( expect(result.maxItems, '1');
// 'pubsub.server.example.org',
// 'example:node',
// PubSubPublishOptions(
// maxItems: 'max',
// ),
// );
}); });
}
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);
});
}