Compare commits

..

12 Commits

Author SHA1 Message Date
9cb6346c4d fix(style): Format and lint test helpers 2023-03-12 19:19:53 +01:00
f49eb66bb7 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.
2023-03-12 19:11:55 +01:00
324ef9ca29 Merge pull request 'Merge connect and connectAwaitable' (#32) from refactor/connect into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/32
2023-03-12 16:31:21 +00:00
5b4dcc67b2 refactor(core): Remove XmppConnectionResult 2023-03-11 19:07:03 +01:00
9010218b10 refactor(core): Move connection errors into their own file 2023-03-11 19:06:28 +01:00
61144a10b3 fix(core): Minor API change 2023-03-11 19:01:55 +01:00
7a1f737c65 chore(meta): Bump version 2023-03-11 19:00:42 +01:00
546c032d43 fix(tests): Fix broken test 2023-03-11 18:59:40 +01:00
b1869be3d9 chore(docs): Update changelog 2023-03-11 18:59:22 +01:00
574fdfecaa feat(core): Merge connect and connectAwaitable 2023-03-11 18:54:36 +01:00
25c778965c feat(core): Merge connect and connectAwaitable 2023-03-11 00:10:50 +01:00
976c0040b5 chore(meta): Update JDK to 17 2023-03-11 00:10:50 +01:00
13 changed files with 545 additions and 249 deletions

View File

@ -117,19 +117,19 @@ class _MyHomePageState extends State<MyHomePage> {
allowPlainAuth: true, allowPlainAuth: true,
), ),
); );
final result = await connection.connectAwaitable(); final result = await connection.connect(waitUntilLogin: true);
setState(() { setState(() {
connected = result.success; connected = result.isType<bool>() && result.get<bool>();
loading = false; loading = false;
}); });
if (result.error != null) { if (result.isType<XmppConnectionError>()) {
logger.severe(result.error); logger.severe(result.get<XmppConnectionError>());
if (context.mounted) { if (context.mounted) {
showDialog( showDialog(
context: context, context: context,
builder: (_) => AlertDialog( builder: (_) => AlertDialog(
title: const Text('Error'), title: const Text('Error'),
content: Text(result.error.toString()), content: Text(result.get<XmppConnectionError>().toString()),
), ),
); );
} }

View File

@ -29,7 +29,7 @@
useGoogleAPIs = false; useGoogleAPIs = false;
useGoogleTVAddOns = false; useGoogleTVAddOns = false;
}; };
pinnedJDK = pkgs.jdk; pinnedJDK = pkgs.jdk17;
pythonEnv = pkgs.python3.withPackages (ps: with ps; [ pythonEnv = pkgs.python3.withPackages (ps: with ps; [
pyyaml pyyaml

View File

@ -1,3 +1,7 @@
## 0.3.0
- **BREAKING**: Removed `connectAwaitable` and merged it with `connect`.
## 0.1.6+1 ## 0.1.6+1
- **FIX**: Fix LMC not working. - **FIX**: Fix LMC not working.

View File

@ -4,6 +4,7 @@ import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/awaiter.dart'; import 'package:moxxmpp/src/awaiter.dart';
import 'package:moxxmpp/src/buffer.dart'; import 'package:moxxmpp/src/buffer.dart';
import 'package:moxxmpp/src/connection_errors.dart';
import 'package:moxxmpp/src/connectivity.dart'; import 'package:moxxmpp/src/connectivity.dart';
import 'package:moxxmpp/src/errors.dart'; import 'package:moxxmpp/src/errors.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
@ -24,6 +25,7 @@ import 'package:moxxmpp/src/settings.dart';
import 'package:moxxmpp/src/socket.dart'; import 'package:moxxmpp/src/socket.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart'; import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
@ -75,21 +77,6 @@ class StreamHeaderNonza extends XMLNode {
); );
} }
/// The result of an awaited connection.
class XmppConnectionResult {
const XmppConnectionResult(
this.success, {
this.error,
});
/// True if the connection was successful. False if it failed for any reason.
final bool success;
// If a connection attempt fails, i.e. success is false, then this indicates the
// reason the connection failed.
final XmppError? error;
}
/// This class is a connection to the server. /// This class is a connection to the server.
class XmppConnection { class XmppConnection {
XmppConnection( XmppConnection(
@ -180,7 +167,7 @@ class XmppConnection {
/// Completers for certain actions /// Completers for certain actions
// ignore: use_late_for_private_fields_and_variables // ignore: use_late_for_private_fields_and_variables
Completer<XmppConnectionResult>? _connectionCompleter; Completer<Result<bool, XmppConnectionError>>? _connectionCompleter;
/// Negotiators /// Negotiators
final Map<String, XmppFeatureNegotiatorBase> _featureNegotiators = {}; final Map<String, XmppFeatureNegotiatorBase> _featureNegotiators = {};
@ -198,6 +185,9 @@ class XmppConnection {
bool _isConnectionRunning = false; bool _isConnectionRunning = false;
final Lock _connectionRunningLock = Lock(); final Lock _connectionRunningLock = Lock();
/// Flag indicating whether reconnection should be enabled after a successful connection.
bool _enableReconnectOnSuccess = false;
/// Enters the critical section for accessing [XmppConnection._isConnectionRunning] /// Enters the critical section for accessing [XmppConnection._isConnectionRunning]
/// and does the following: /// and does the following:
/// - if _isConnectionRunning is false, set it to true and return false. /// - if _isConnectionRunning is false, set it to true and return false.
@ -404,9 +394,10 @@ class XmppConnection {
state: XmppConnectionState.error, state: XmppConnectionState.error,
); );
_connectionCompleter?.complete( _connectionCompleter?.complete(
XmppConnectionResult( Result(
false, StreamFailureError(
error: error, error,
),
), ),
); );
_connectionCompleter = null; _connectionCompleter = null;
@ -427,7 +418,14 @@ class XmppConnection {
// The error is recoverable // The error is recoverable
await _setConnectionState(XmppConnectionState.notConnected); await _setConnectionState(XmppConnectionState.notConnected);
await _reconnectionPolicy.onFailure();
if (await _reconnectionPolicy.getShouldReconnect()) {
await _reconnectionPolicy.onFailure();
} else {
_log.info(
'Not passing connection failure to reconnection policy as it indicates that we should not reconnect',
);
}
} }
/// Called whenever the socket creates an event /// Called whenever the socket creates an event
@ -859,8 +857,13 @@ class XmppConnection {
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
await _setConnectionState(XmppConnectionState.connected); await _setConnectionState(XmppConnectionState.connected);
// Enable reconnections
if (_enableReconnectOnSuccess) {
await _reconnectionPolicy.setShouldReconnect(true);
}
// Resolve the connection completion future // Resolve the connection completion future
_connectionCompleter?.complete(const XmppConnectionResult(true)); _connectionCompleter?.complete(const Result(true));
_connectionCompleter = null; _connectionCompleter = null;
// Tell consumers of the event stream that we're done with stream feature // Tell consumers of the event stream that we're done with stream feature
@ -1112,47 +1115,30 @@ class XmppConnection {
); );
} }
/// Like [connect] but the Future resolves when the resource binding is either done or Future<Result<bool, XmppConnectionError>> _connectImpl({
/// SASL has failed.
Future<XmppConnectionResult> connectAwaitable({
String? lastResource,
bool waitForConnection = false,
}) async {
_runPreConnectionAssertions();
await _resetIsConnectionRunning();
_connectionCompleter = Completer();
_log.finest('Calling connect() from connectAwaitable');
await connect(
lastResource: lastResource,
waitForConnection: waitForConnection,
shouldReconnect: false,
);
return _connectionCompleter!.future;
}
/// Start the connection process using the provided connection settings.
Future<void> connect({
String? lastResource, String? lastResource,
bool waitForConnection = false, bool waitForConnection = false,
bool shouldReconnect = true, bool shouldReconnect = true,
bool waitUntilLogin = false,
bool enableReconnectOnSuccess = true,
}) async { }) async {
if (_connectionState != XmppConnectionState.notConnected &&
_connectionState != XmppConnectionState.error) {
_log.fine(
'Cancelling this connection attempt as one appears to be already running.',
);
return;
}
_runPreConnectionAssertions(); _runPreConnectionAssertions();
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
if (waitUntilLogin) {
_log.finest('Setting up completer for awaiting completed login');
_connectionCompleter = Completer();
}
if (lastResource != null) { if (lastResource != null) {
setResource(lastResource); setResource(lastResource);
} }
_enableReconnectOnSuccess = enableReconnectOnSuccess;
if (shouldReconnect) { if (shouldReconnect) {
await _reconnectionPolicy.setShouldReconnect(true); await _reconnectionPolicy.setShouldReconnect(true);
} else {
await _reconnectionPolicy.setShouldReconnect(false);
} }
await _reconnectionPolicy.reset(); await _reconnectionPolicy.reset();
@ -1182,6 +1168,8 @@ class XmppConnection {
); );
if (!result) { if (!result) {
await handleError(NoConnectionError()); await handleError(NoConnectionError());
return Result(NoConnectionPossibleError());
} else { } else {
await _reconnectionPolicy.onSuccess(); await _reconnectionPolicy.onSuccess();
_log.fine('Preparing the internal state for a connection attempt'); _log.fine('Preparing the internal state for a connection attempt');
@ -1190,6 +1178,67 @@ class XmppConnection {
_updateRoutingState(RoutingState.negotiating); _updateRoutingState(RoutingState.negotiating);
_isAuthenticated = false; _isAuthenticated = false;
_sendStreamHeader(); _sendStreamHeader();
if (waitUntilLogin) {
return _connectionCompleter!.future;
} else {
return const Result(true);
}
}
}
/// Start the connection process using the provided connection settings.
///
/// If [lastResource] is set, then its value is used as the connection's resource.
/// Useful for stream resumption.
///
/// [shouldReconnect] indicates whether the reconnection attempts should be
/// automatically performed after a fatal failure of any kind occurs.
///
/// [waitForConnection] indicates whether the connection should wait for the "go"
/// signal from a registered connectivity manager.
///
/// If [waitUntilLogin] is set to true, the future will resolve when either
/// the connection has been successfully established (authentication included) or
/// a failure occured. If set to false, then the future will immediately resolve
/// to true.
///
/// [enableReconnectOnSuccess] indicates that automatic reconnection is to be
/// enabled once the connection has been successfully established.
Future<Result<bool, XmppConnectionError>> connect({
String? lastResource,
bool? shouldReconnect,
bool waitForConnection = false,
bool waitUntilLogin = false,
bool enableReconnectOnSuccess = true,
}) async {
if (_connectionState != XmppConnectionState.notConnected &&
_connectionState != XmppConnectionState.error) {
_log.fine(
'Cancelling this connection attempt as one appears to be already running.',
);
return Future.value(
Result(
ConnectionAlreadyRunningError(),
),
);
}
final result = _connectImpl(
lastResource: lastResource,
shouldReconnect: shouldReconnect ?? !waitUntilLogin,
waitForConnection: waitForConnection,
waitUntilLogin: waitUntilLogin,
enableReconnectOnSuccess: enableReconnectOnSuccess,
);
if (waitUntilLogin) {
return result;
} else {
return Future.value(
const Result(
true,
),
);
} }
} }
} }

View File

@ -0,0 +1,28 @@
import 'package:moxxmpp/src/errors.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
/// The reason a call to `XmppConnection.connect` failed.
abstract class XmppConnectionError {}
/// Returned by `XmppConnection.connect` when a connection is already active.
class ConnectionAlreadyRunningError extends XmppConnectionError {}
/// Returned by `XmppConnection.connect` when a negotiator returned an unrecoverable
/// error. Only returned when waitUntilLogin is true.
class NegotiatorReturnedError extends XmppConnectionError {
NegotiatorReturnedError(this.error);
/// The error returned by the negotiator.
final NegotiatorError error;
}
class StreamFailureError extends XmppConnectionError {
StreamFailureError(this.error);
/// The error that causes a connection failure.
final XmppError error;
}
/// Returned by `XmppConnection.connect` when no connection could
/// be established.
class NoConnectionPossibleError extends XmppConnectionError {}

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

@ -1,6 +1,6 @@
name: moxxmpp name: moxxmpp
description: A pure-Dart XMPP library description: A pure-Dart XMPP library
version: 0.2.0 version: 0.3.0
homepage: https://codeberg.org/moxxy/moxxmpp homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub

View File

@ -5,6 +5,8 @@ void initLogger() {
Logger.root.level = Level.ALL; Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) { Logger.root.onRecord.listen((record) {
// ignore: avoid_print // ignore: avoid_print
print('[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}'); print(
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
);
}); });
} }

View File

@ -0,0 +1,70 @@
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,28 +1,37 @@
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
bool compareXMLNodes(XMLNode actual, XMLNode expectation, { bool ignoreId = true}) { bool compareXMLNodes(
XMLNode actual,
XMLNode expectation, {
bool ignoreId = true,
}) {
// Compare attributes // Compare attributes
if (expectation.tag != actual.tag) return false; if (expectation.tag != actual.tag) return false;
final attributesEqual = expectation.attributes.keys.every((key) { final attributesEqual = expectation.attributes.keys.every((key) {
// Ignore the stanza ID // Ignore the stanza ID
if (key == 'id' && ignoreId) return true; if (key == 'id' && ignoreId) return true;
return actual.attributes[key] == expectation.attributes[key]; return actual.attributes[key] == expectation.attributes[key];
}); });
if (!attributesEqual) return false; if (!attributesEqual) return false;
final actualAttributeLength = !ignoreId ? actual.attributes.length : ( final actualAttributeLength = !ignoreId
actual.attributes.containsKey('id') ? actual.attributes.length - 1 : actual.attributes.length ? actual.attributes.length
); : (actual.attributes.containsKey('id')
final expectedAttributeLength = !ignoreId ? expectation.attributes.length : ( ? actual.attributes.length - 1
expectation.attributes.containsKey('id') ? expectation.attributes.length - 1 : expectation.attributes.length : actual.attributes.length);
); final expectedAttributeLength = !ignoreId
? expectation.attributes.length
: (expectation.attributes.containsKey('id')
? expectation.attributes.length - 1
: expectation.attributes.length);
if (actualAttributeLength != expectedAttributeLength) return false; if (actualAttributeLength != expectedAttributeLength) return false;
if (expectation.innerText() != '' && actual.innerText() != expectation.innerText()) return false; if (expectation.innerText() != '' &&
actual.innerText() != expectation.innerText()) return false;
return expectation.children.every((childe) { return expectation.children.every((childe) {
return actual.children.any((childa) => compareXMLNodes(childa, childe)); return actual.children.any((childa) => compareXMLNodes(childa, childe));
}); });
} }

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;
@ -24,7 +25,7 @@ abstract class ExpectationBase {
/// Literally compare the input with the expectation /// Literally compare the input with the expectation
class StringExpectation extends ExpectationBase { class StringExpectation extends ExpectationBase {
StringExpectation(String expectation, String response) : super(expectation, response); StringExpectation(super.expectation, super.response);
@override @override
bool matches(String input) => input == expectation; bool matches(String input) => input == expectation;
@ -32,28 +33,97 @@ 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(
super.expectation,
super.response, {
this.ignoreId = false,
this.adjustId = false,
});
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 =
final StreamController<XmppSocketEvent> _eventStream; StreamController<String>.broadcast();
final StreamController<XmppSocketEvent> _eventStream =
StreamController<XmppSocketEvent>.broadcast();
final List<ExpectationBase> _play; final List<ExpectationBase> _play;
String? lastId; String? lastId;
@ -64,22 +134,24 @@ class StubTCPSocket extends BaseSocketWrapper { // Request -> Response(s)
Future<bool> secure(String domain) async => true; Future<bool> secure(String domain) async => true;
@override @override
Future<bool> connect(String domain, { String? host, int? port }) async => true; Future<bool> connect(String domain, {String? host, int? port}) async => true;
@override @override
Stream<String> getDataStream() => _dataStream.stream.asBroadcastStream(); Stream<String> getDataStream() => _dataStream.stream.asBroadcastStream();
@override @override
Stream<XmppSocketEvent> getEventStream() => _eventStream.stream.asBroadcastStream(); Stream<XmppSocketEvent> getEventStream() =>
_eventStream.stream.asBroadcastStream();
/// Let the "connection" receive [data]. /// Let the "connection" receive [data].
void injectRawXml(String data) { void injectRawXml(String data) {
// ignore: avoid_print
print('<== $data'); print('<== $data');
_dataStream.add(data); _dataStream.add(data);
} }
@override @override
void write(Object? object, { String? redact }) { void write(Object? object, {String? redact}) {
var str = object as String; var str = object! as String;
// ignore: avoid_print // ignore: avoid_print
print('==> $str'); print('==> $str');
@ -90,7 +162,7 @@ class StubTCPSocket extends BaseSocketWrapper { // Request -> Response(s)
final expectation = _play[_state]; final expectation = _play[_state];
// TODO: Implement an XML matcher // TODO(Unknown): Implement an XML matcher
if (str.startsWith("<?xml version='1.0'?>")) { if (str.startsWith("<?xml version='1.0'?>")) {
str = str.substring(21); str = str.substring(21);
} }
@ -99,9 +171,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,17 +183,18 @@ 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);
outputNode.attributes['id'] = inputNode.attributes['id']!; outputNode.attributes['id'] = inputNode.attributes['id'];
response = outputNode.toXml(); response = outputNode.toXml();
} }
} }
print("<== $response"); // ignore: avoid_print
print('<== $response');
_dataStream.add(response); _dataStream.add(response);
} }

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);
}); });
} }