Compare commits
12 Commits
b53c62b40c
...
9cb6346c4d
Author | SHA1 | Date | |
---|---|---|---|
9cb6346c4d | |||
f49eb66bb7 | |||
324ef9ca29 | |||
5b4dcc67b2 | |||
9010218b10 | |||
61144a10b3 | |||
7a1f737c65 | |||
546c032d43 | |||
b1869be3d9 | |||
574fdfecaa | |||
25c778965c | |||
976c0040b5 |
@ -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()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
packages/moxxmpp/lib/src/connection_errors.dart
Normal file
28
packages/moxxmpp/lib/src/connection_errors.dart
Normal 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 {}
|
@ -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()],
|
),
|
||||||
),
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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}',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
70
packages/moxxmpp/test/helpers/manager.dart
Normal file
70
packages/moxxmpp/test/helpers/manager.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user