diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index 542b46d..caa1cee 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -1,6 +1,7 @@ library moxxmpp; export 'package:moxxmpp/src/connection.dart'; +export 'package:moxxmpp/src/connectivity.dart'; export 'package:moxxmpp/src/errors.dart'; export 'package:moxxmpp/src/events.dart'; export 'package:moxxmpp/src/iq.dart'; diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index cd19f18..8abbcf8 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -4,6 +4,7 @@ import 'package:meta/meta.dart'; import 'package:moxlib/moxlib.dart'; import 'package:moxxmpp/src/awaiter.dart'; import 'package:moxxmpp/src/buffer.dart'; +import 'package:moxxmpp/src/connectivity.dart'; import 'package:moxxmpp/src/errors.dart'; import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/iq.dart'; @@ -94,22 +95,24 @@ class XmppConnectionResult { class XmppConnection { XmppConnection( ReconnectionPolicy reconnectionPolicy, + ConnectivityManager connectivityManager, this._socket, { this.connectionPingDuration = const Duration(minutes: 3), this.connectingTimeout = const Duration(minutes: 2), } - ) : _reconnectionPolicy = reconnectionPolicy { - // Allow the reconnection policy to perform reconnections by itself - _reconnectionPolicy.register( - _attemptReconnection, - _onNetworkConnectionLost, - ); + ) : _reconnectionPolicy = reconnectionPolicy, + _connectivityManager = connectivityManager { + // Allow the reconnection policy to perform reconnections by itself + _reconnectionPolicy.register( + _attemptReconnection, + _onNetworkConnectionLost, + ); - _socketStream = _socket.getDataStream(); - // TODO(Unknown): Handle on done - _socketStream.transform(_streamBuffer).forEach(handleXmlStream); - _socket.getEventStream().listen(_handleSocketEvent); + _socketStream = _socket.getDataStream(); + // TODO(Unknown): Handle on done + _socketStream.transform(_streamBuffer).forEach(handleXmlStream); + _socket.getEventStream().listen(_handleSocketEvent); } @@ -122,13 +125,16 @@ class XmppConnection { /// The data stream of the socket late final Stream _socketStream; - /// Connection settings late ConnectionSettings _connectionSettings; /// A policy on how to reconnect final ReconnectionPolicy _reconnectionPolicy; + /// The class responsible for preventing errors on initial connection due + /// to no network. + final ConnectivityManager _connectivityManager; + /// A helper for handling await semantics with stanzas final StanzaAwaiter _stanzaAwaiter = StanzaAwaiter(); @@ -378,7 +384,7 @@ class XmppConnection { // Connect again // ignore: cascade_invocations _log.finest('Calling connect() from _attemptReconnection'); - await connect(); + await connect(waitForConnection: true); } /// Called when a stream ending error has occurred @@ -401,7 +407,11 @@ class XmppConnection { return; } - await _setConnectionState(XmppConnectionState.error); + if (await _connectivityManager.hasConnection()) { + await _setConnectionState(XmppConnectionState.error); + } else { + await _setConnectionState(XmppConnectionState.notConnected); + } await _reconnectionPolicy.onFailure(); } @@ -832,7 +842,6 @@ class XmppConnection { if (_isMandatoryNegotiationDone(_streamFeatures) && !_isNegotiationPossible(_streamFeatures)) { _log.finest('Negotiations done!'); _updateRoutingState(RoutingState.handleStanzas); - await _reconnectionPolicy.onSuccess(); await _resetIsConnectionRunning(); await _onNegotiationsDone(); } else { @@ -857,7 +866,6 @@ class XmppConnection { _log.finest('Negotiations done!'); _updateRoutingState(RoutingState.handleStanzas); - await _reconnectionPolicy.onSuccess(); await _resetIsConnectionRunning(); await _onNegotiationsDone(); } else { @@ -875,7 +883,6 @@ class XmppConnection { _log.finest('Negotiator wants to skip the remaining negotiation... Negotiations (assumed) done!'); _updateRoutingState(RoutingState.handleStanzas); - await _reconnectionPolicy.onSuccess(); await _resetIsConnectionRunning(); await _onNegotiationsDone(); break; @@ -987,7 +994,7 @@ class XmppConnection { } Future _disconnect({required XmppConnectionState state, bool triggeredByUser = true}) async { - _reconnectionPolicy.setShouldReconnect(false); + await _reconnectionPolicy.setShouldReconnect(false); if (triggeredByUser) { getPresenceManager().sendUnavailablePresence(); @@ -1018,17 +1025,21 @@ class XmppConnection { /// Like [connect] but the Future resolves when the resource binding is either done or /// SASL has failed. - Future connectAwaitable({ String? lastResource }) async { + Future connectAwaitable({ String? lastResource, bool waitForConnection = false }) async { _runPreConnectionAssertions(); await _resetIsConnectionRunning(); _connectionCompleter = Completer(); _log.finest('Calling connect() from connectAwaitable'); - await connect(lastResource: lastResource); + await connect( + lastResource: lastResource, + waitForConnection: waitForConnection, + shouldReconnect: false, + ); return _connectionCompleter!.future; } /// Start the connection process using the provided connection settings. - Future connect({ String? lastResource }) async { + Future connect({ String? lastResource, bool waitForConnection = false, bool shouldReconnect = true }) async { if (_connectionState != XmppConnectionState.notConnected && _connectionState != XmppConnectionState.error) { _log.fine('Cancelling this connection attempt as one appears to be already running.'); return; @@ -1036,15 +1047,25 @@ class XmppConnection { _runPreConnectionAssertions(); await _resetIsConnectionRunning(); - _reconnectionPolicy.setShouldReconnect(true); if (lastResource != null) { setResource(lastResource); } + if (shouldReconnect) { + await _reconnectionPolicy.setShouldReconnect(true); + } + await _reconnectionPolicy.reset(); await _sendEvent(ConnectingEvent()); + // If requested, wait until we have a network connection + if (waitForConnection) { + _log.info('Waiting for okay from connectivityManager'); + await _connectivityManager.waitForConnection(); + _log.info('Got okay from connectivityManager'); + } + final smManager = getStreamManagementManager(); String? host; int? port; diff --git a/packages/moxxmpp/lib/src/connectivity.dart b/packages/moxxmpp/lib/src/connectivity.dart new file mode 100644 index 0000000..b2b9e7d --- /dev/null +++ b/packages/moxxmpp/lib/src/connectivity.dart @@ -0,0 +1,18 @@ +/// This manager class is responsible to tell the moxxmpp XmppConnection +/// when a connection can be established or not, regarding the network availability. +abstract class ConnectivityManager { + /// Returns true if a network connection is available. If not, returns false. + Future hasConnection(); + + /// Returns a future that resolves once we have a network connection. + Future waitForConnection(); +} + +/// An implementation of [ConnectivityManager] that is always connected. +class AlwaysConnectedConnectivityManager extends ConnectivityManager { + @override + Future hasConnection() async => true; + + @override + Future waitForConnection() async {} +} diff --git a/packages/moxxmpp/lib/src/reconnect.dart b/packages/moxxmpp/lib/src/reconnect.dart index 5e80731..895d6df 100644 --- a/packages/moxxmpp/lib/src/reconnect.dart +++ b/packages/moxxmpp/lib/src/reconnect.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; +import 'package:moxxmpp/src/util/queue.dart'; import 'package:synchronized/synchronized.dart'; /// A callback function to be called when the connection to the server has been lost. @@ -24,10 +25,16 @@ abstract class ReconnectionPolicy { bool _shouldAttemptReconnection = false; /// Indicate if a reconnection attempt is currently running. - bool _isReconnecting = false; + @protected + bool isReconnecting = false; /// And the corresponding lock - final Lock _isReconnectingLock = Lock(); + @protected + final Lock lock = Lock(); + + /// The lock for accessing [_shouldAttemptReconnection] + @protected + final Lock shouldReconnectLock = Lock(); /// Called by XmppConnection to register the policy. void register(PerformReconnectFunction performReconnect, ConnectionLostCallback triggerConnectionLost) { @@ -48,97 +55,122 @@ abstract class ReconnectionPolicy { /// Caled by the XmppConnection when the reconnection was successful. Future onSuccess(); - bool get shouldReconnect => _shouldAttemptReconnection; + Future getShouldReconnect() async { + return shouldReconnectLock.synchronized(() => _shouldAttemptReconnection); + } /// Set whether a reconnection attempt should be made. - void setShouldReconnect(bool value) { - _shouldAttemptReconnection = value; + Future setShouldReconnect(bool value) async { + return shouldReconnectLock.synchronized(() => _shouldAttemptReconnection = value); } /// Returns true if the manager is currently triggering a reconnection. If not, returns /// false. Future isReconnectionRunning() async { - return _isReconnectingLock.synchronized(() => _isReconnecting); + return lock.synchronized(() => isReconnecting); } - /// Set the _isReconnecting state to [value]. + /// Set the isReconnecting state to [value]. @protected Future setIsReconnecting(bool value) async { - await _isReconnectingLock.synchronized(() async { - _isReconnecting = value; + await lock.synchronized(() async { + isReconnecting = value; }); } - @protected - Future testAndSetIsReconnecting() async { - return _isReconnectingLock.synchronized(() { - if (_isReconnecting) { - return false; - } else { - _isReconnecting = true; - return true; - } - }); - } } /// A simple reconnection strategy: Make the reconnection delays exponentially longer /// for every failed attempt. /// NOTE: This ReconnectionPolicy may be broken -class ExponentialBackoffReconnectionPolicy extends ReconnectionPolicy { - ExponentialBackoffReconnectionPolicy(this._maxBackoffTime) : super(); +class RandomBackoffReconnectionPolicy extends ReconnectionPolicy { + RandomBackoffReconnectionPolicy( + this._minBackoffTime, + this._maxBackoffTime, + ) : assert(_minBackoffTime < _maxBackoffTime, '_minBackoffTime must be smaller than _maxBackoffTime'), + super(); - /// The maximum time in seconds that a backoff step should be. + /// The maximum time in seconds that a backoff should be. final int _maxBackoffTime; - /// Amount of consecutive failed reconnections. - int _counter = 0; + /// The minimum time in seconds that a backoff should be. + final int _minBackoffTime; /// Backoff timer. Timer? _timer; + final Lock _timerLock = Lock(); + /// Logger. - final Logger _log = Logger('ExponentialBackoffReconnectionPolicy'); + final Logger _log = Logger('RandomBackoffReconnectionPolicy'); + + /// Event queue + final AsyncQueue _eventQueue = AsyncQueue(); /// Called when the backoff expired Future _onTimerElapsed() async { - final isReconnecting = await isReconnectionRunning(); - if (shouldReconnect) { - if (!isReconnecting) { - await setIsReconnecting(true); - await performReconnect!(); - } else { - // Should never happen. - _log.fine('Backoff timer expired but reconnection is running, so doing nothing.'); + _log.fine('Timer elapsed. Waiting for lock'); + await lock.synchronized(() async { + _log.fine('Lock aquired'); + if (!(await getShouldReconnect())) { + _log.fine('Backoff timer expired but getShouldReconnect() returned false'); + return; } - } + + if (isReconnecting) { + _log.fine('Backoff timer expired but a reconnection is running, so doing nothing.'); + return; + } + + _log.fine('Triggering reconnect'); + isReconnecting = true; + await performReconnect!(); + }); + + await _timerLock.synchronized(() { + _timer?.cancel(); + _timer = null; + }); + } + + Future _reset() async { + _log.finest('Resetting internal state'); + + await _timerLock.synchronized(() { + _timer?.cancel(); + _timer = null; + }); + + await setIsReconnecting(false); } @override Future reset() async { - _log.finest('Resetting internal state'); - _counter = 0; - await setIsReconnecting(false); - - if (_timer != null) { - _timer!.cancel(); - _timer = null; - } + // ignore: unnecessary_lambdas + await _eventQueue.addJob(() => _reset()); } - @override - Future onFailure() async { - _log.finest('Failure occured. Starting exponential backoff'); - _counter++; - - if (_timer != null) { - _timer!.cancel(); + Future _onFailure() async { + final shouldContinue = await _timerLock.synchronized(() { + return _timer == null; + }); + if (!shouldContinue) { + _log.finest('_onFailure: Not backing off since _timer is already running'); + return; } - // Wait at max 80 seconds. - final seconds = min(min(pow(2, _counter).toInt(), 80), _maxBackoffTime); + final seconds = Random().nextInt(_maxBackoffTime - _minBackoffTime) + _minBackoffTime; + _log.finest('Failure occured. Starting random backoff with ${seconds}s'); + _timer?.cancel(); + _timer = Timer(Duration(seconds: seconds), _onTimerElapsed); } + + @override + Future onFailure() async { + // ignore: unnecessary_lambdas + await _eventQueue.addJob(() => _onFailure()); + } @override Future onSuccess() async { diff --git a/packages/moxxmpp/lib/src/util/queue.dart b/packages/moxxmpp/lib/src/util/queue.dart new file mode 100644 index 0000000..f4c2894 --- /dev/null +++ b/packages/moxxmpp/lib/src/util/queue.dart @@ -0,0 +1,56 @@ +import 'dart:async'; +import 'dart:collection'; +import 'package:meta/meta.dart'; +import 'package:synchronized/synchronized.dart'; + +/// A job to be submitted to an [AsyncQueue]. +typedef AsyncQueueJob = Future Function(); + +/// A (hopefully) async-safe queue that attempts to force +/// in-order execution of its jobs. +class AsyncQueue { + /// The lock for accessing [AsyncQueue._lock] and [AsyncQueue._running]. + final Lock _lock = Lock(); + + /// The actual job queue. + final Queue _queue = Queue(); + + /// Indicates whether we are currently executing a job. + bool _running = false; + + @visibleForTesting + Queue get queue => _queue; + + @visibleForTesting + bool get isRunning => _running; + + /// Adds a job [job] to the queue. + Future addJob(AsyncQueueJob job) async { + await _lock.synchronized(() { + _queue.add(job); + + if (!_running && _queue.isNotEmpty) { + _running = true; + unawaited(_popJob()); + } + }); + } + + Future clear() async { + await _lock.synchronized(_queue.clear); + } + + Future _popJob() async { + final job = _queue.removeFirst(); + final future = job(); + await future; + + await _lock.synchronized(() { + if (_queue.isNotEmpty) { + unawaited(_popJob()); + } else { + _running = false; + } + }); + } +} diff --git a/packages/moxxmpp/test/async_queue_test.dart b/packages/moxxmpp/test/async_queue_test.dart new file mode 100644 index 0000000..db78ac1 --- /dev/null +++ b/packages/moxxmpp/test/async_queue_test.dart @@ -0,0 +1,43 @@ +import 'package:moxxmpp/src/util/queue.dart'; +import 'package:test/test.dart'; + +void main() { + test('Test the async queue', () async { + final queue = AsyncQueue(); + int future1Finish = 0; + int future2Finish = 0; + int future3Finish = 0; + + await queue.addJob(() => Future.delayed(const Duration(seconds: 3), () => future1Finish = DateTime.now().millisecondsSinceEpoch)); + await queue.addJob(() => Future.delayed(const Duration(seconds: 3), () => future2Finish = DateTime.now().millisecondsSinceEpoch)); + await queue.addJob(() => Future.delayed(const Duration(seconds: 3), () => future3Finish = DateTime.now().millisecondsSinceEpoch)); + + await Future.delayed(const Duration(seconds: 12)); + + // The three futures must be done + expect(future1Finish != 0, true); + expect(future2Finish != 0, true); + expect(future3Finish != 0, true); + + // The end times of the futures must be ordered (on a timeline) + // |-- future1Finish -- future2Finish -- future3Finish --| + expect( + future1Finish < future2Finish && future1Finish < future3Finish, + true, + ); + expect( + future2Finish < future3Finish && future2Finish > future1Finish, + true, + ); + expect( + future3Finish > future1Finish && future3Finish > future2Finish, + true, + ); + + // The queue must be empty at the end + expect(queue.queue.isEmpty, true); + + // The queue must not be executing anything at the end + expect(queue.isRunning, false); + }); +} diff --git a/packages/moxxmpp/test/negotiator_test.dart b/packages/moxxmpp/test/negotiator_test.dart index f7d5b00..228992e 100644 --- a/packages/moxxmpp/test/negotiator_test.dart +++ b/packages/moxxmpp/test/negotiator_test.dart @@ -54,8 +54,11 @@ void main() { ], ); - final connection = XmppConnection(TestingReconnectionPolicy(), stubSocket) - ..registerFeatureNegotiators([ + final connection = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + stubSocket, + )..registerFeatureNegotiators([ StubNegotiator1(), StubNegotiator2(), ]) diff --git a/packages/moxxmpp/test/xeps/xep_0004_test.dart b/packages/moxxmpp/test/xeps/xep_0004_test.dart index 57cf823..964b7fc 100644 --- a/packages/moxxmpp/test/xeps/xep_0004_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0004_test.dart @@ -3,14 +3,14 @@ import 'package:test/test.dart'; void main() { test('Parsing', () { - const testData = "urn:xmpp:dataforms:softwareinfoipv4ipv6Mac10.5.1Psi0.11"; + const testData = "urn:xmpp:dataforms:softwareinfoipv4ipv6Mac10.5.1Psi0.11"; - final form = parseDataForm(XMLNode.fromString(testData)); - expect(form.getFieldByVar('FORM_TYPE')?.values.first, 'urn:xmpp:dataforms:softwareinfo'); - expect(form.getFieldByVar('ip_version')?.values, [ 'ipv4', 'ipv6' ]); - expect(form.getFieldByVar('os')?.values.first, 'Mac'); - expect(form.getFieldByVar('os_version')?.values.first, '10.5.1'); - expect(form.getFieldByVar('software')?.values.first, 'Psi'); - expect(form.getFieldByVar('software_version')?.values.first, '0.11'); + final form = parseDataForm(XMLNode.fromString(testData)); + expect(form.getFieldByVar('FORM_TYPE')?.values.first, 'urn:xmpp:dataforms:softwareinfo'); + expect(form.getFieldByVar('ip_version')?.values, [ 'ipv4', 'ipv6' ]); + expect(form.getFieldByVar('os')?.values.first, 'Mac'); + expect(form.getFieldByVar('os_version')?.values.first, '10.5.1'); + expect(form.getFieldByVar('software')?.values.first, 'Psi'); + expect(form.getFieldByVar('software_version')?.values.first, '0.11'); }); } diff --git a/packages/moxxmpp/test/xeps/xep_0030.dart b/packages/moxxmpp/test/xeps/xep_0030_test.dart similarity index 95% rename from packages/moxxmpp/test/xeps/xep_0030.dart rename to packages/moxxmpp/test/xeps/xep_0030_test.dart index cfd5272..fb96588 100644 --- a/packages/moxxmpp/test/xeps/xep_0030.dart +++ b/packages/moxxmpp/test/xeps/xep_0030_test.dart @@ -65,7 +65,11 @@ void main() { ], ); - final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), socket: fakeSocket); + final XmppConnection conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + fakeSocket, + ); conn.setConnectionSettings(ConnectionSettings( jid: JID.fromString('polynomdivision@test.server'), password: 'aaaa', @@ -74,7 +78,7 @@ void main() { ),); conn.registerManagers([ PresenceManager('http://moxxmpp.example'), - RosterManager(), + RosterManager(TestingRosterStateManager(null, [])), DiscoManager(), PingManager(), ]); diff --git a/packages/moxxmpp/test/xeps/xep_0115_test.dart b/packages/moxxmpp/test/xeps/xep_0115_test.dart index 244ebb7..a0dab4e 100644 --- a/packages/moxxmpp/test/xeps/xep_0115_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0115_test.dart @@ -4,164 +4,164 @@ import 'package:test/test.dart'; void main() { test('Test XEP example', () async { - final data = DiscoInfo( - [ - 'http://jabber.org/protocol/caps', - 'http://jabber.org/protocol/disco#info', - 'http://jabber.org/protocol/disco#items', - 'http://jabber.org/protocol/muc' - ], - [ - Identity( - category: 'client', - type: 'pc', - name: 'Exodus 0.9.1', - ) - ], - [], - JID.fromString('some@user.local/test'), - ); + final data = DiscoInfo( + [ + 'http://jabber.org/protocol/caps', + 'http://jabber.org/protocol/disco#info', + 'http://jabber.org/protocol/disco#items', + 'http://jabber.org/protocol/muc' + ], + [ + Identity( + category: 'client', + type: 'pc', + name: 'Exodus 0.9.1', + ) + ], + [], + JID.fromString('some@user.local/test'), + ); - final hash = await calculateCapabilityHash(data, Sha1()); - expect(hash, 'QgayPKawpkPSDYmwT/WM94uAlu0='); + final hash = await calculateCapabilityHash(data, Sha1()); + expect(hash, 'QgayPKawpkPSDYmwT/WM94uAlu0='); }); test('Test complex generation example', () async { - const extDiscoDataString = "urn:xmpp:dataforms:softwareinfoipv4ipv6Mac10.5.1Psi0.11"; - final data = DiscoInfo( - [ - 'http://jabber.org/protocol/caps', - 'http://jabber.org/protocol/disco#info', - 'http://jabber.org/protocol/disco#items', - 'http://jabber.org/protocol/muc' - ], - [ - const Identity( - category: 'client', - type: 'pc', - name: 'Psi 0.11', - lang: 'en', - ), - const Identity( - category: 'client', - type: 'pc', - name: 'Ψ 0.11', - lang: 'el', - ), - ], - [ parseDataForm(XMLNode.fromString(extDiscoDataString)) ], - JID.fromString('some@user.local/test'), - ); + const extDiscoDataString = "urn:xmpp:dataforms:softwareinfoipv4ipv6Mac10.5.1Psi0.11"; + final data = DiscoInfo( + [ + 'http://jabber.org/protocol/caps', + 'http://jabber.org/protocol/disco#info', + 'http://jabber.org/protocol/disco#items', + 'http://jabber.org/protocol/muc' + ], + [ + const Identity( + category: 'client', + type: 'pc', + name: 'Psi 0.11', + lang: 'en', + ), + const Identity( + category: 'client', + type: 'pc', + name: 'Ψ 0.11', + lang: 'el', + ), + ], + [ parseDataForm(XMLNode.fromString(extDiscoDataString)) ], + JID.fromString('some@user.local/test'), + ); - final hash = await calculateCapabilityHash(data, Sha1()); - expect(hash, 'q07IKJEyjvHSyhy//CH0CxmKi8w='); + final hash = await calculateCapabilityHash(data, Sha1()); + expect(hash, 'q07IKJEyjvHSyhy//CH0CxmKi8w='); }); test('Test Gajim capability hash computation', () async { - // TODO: This one fails - /* - final data = DiscoInfo( - features: [ - "http://jabber.org/protocol/bytestreams", - "http://jabber.org/protocol/muc", - "http://jabber.org/protocol/commands", - "http://jabber.org/protocol/disco#info", - "jabber:iq:last", - "jabber:x:data", - "jabber:x:encrypted", - "urn:xmpp:ping", - "http://jabber.org/protocol/chatstates", - "urn:xmpp:receipts", - "urn:xmpp:time", - "jabber:iq:version", - "http://jabber.org/protocol/rosterx", - "urn:xmpp:sec-label:0", - "jabber:x:conference", - "urn:xmpp:message-correct:0", - "urn:xmpp:chat-markers:0", - "urn:xmpp:eme:0", - "http://jabber.org/protocol/xhtml-im", - "urn:xmpp:hashes:2", - "urn:xmpp:hash-function-text-names:md5", - "urn:xmpp:hash-function-text-names:sha-1", - "urn:xmpp:hash-function-text-names:sha-256", - "urn:xmpp:hash-function-text-names:sha-512", - "urn:xmpp:hash-function-text-names:sha3-256", - "urn:xmpp:hash-function-text-names:sha3-512", - "urn:xmpp:hash-function-text-names:id-blake2b256", - "urn:xmpp:hash-function-text-names:id-blake2b512", - "urn:xmpp:jingle:1", - "urn:xmpp:jingle:apps:file-transfer:5", - "urn:xmpp:jingle:security:xtls:0", - "urn:xmpp:jingle:transports:s5b:1", - "urn:xmpp:jingle:transports:ibb:1", - "urn:xmpp:avatar:metadata+notify", - "urn:xmpp:message-moderate:0", - "http://jabber.org/protocol/tune+notify", - "http://jabber.org/protocol/geoloc+notify", - "http://jabber.org/protocol/nick+notify", - "eu.siacs.conversations.axolotl.devicelist+notify", - ], - identities: [ - Identity( - category: "client", - type: "pc", - name: "Gajim" - ) - ] - ); + // TODO: This one fails + /* + final data = DiscoInfo( + features: [ + "http://jabber.org/protocol/bytestreams", + "http://jabber.org/protocol/muc", + "http://jabber.org/protocol/commands", + "http://jabber.org/protocol/disco#info", + "jabber:iq:last", + "jabber:x:data", + "jabber:x:encrypted", + "urn:xmpp:ping", + "http://jabber.org/protocol/chatstates", + "urn:xmpp:receipts", + "urn:xmpp:time", + "jabber:iq:version", + "http://jabber.org/protocol/rosterx", + "urn:xmpp:sec-label:0", + "jabber:x:conference", + "urn:xmpp:message-correct:0", + "urn:xmpp:chat-markers:0", + "urn:xmpp:eme:0", + "http://jabber.org/protocol/xhtml-im", + "urn:xmpp:hashes:2", + "urn:xmpp:hash-function-text-names:md5", + "urn:xmpp:hash-function-text-names:sha-1", + "urn:xmpp:hash-function-text-names:sha-256", + "urn:xmpp:hash-function-text-names:sha-512", + "urn:xmpp:hash-function-text-names:sha3-256", + "urn:xmpp:hash-function-text-names:sha3-512", + "urn:xmpp:hash-function-text-names:id-blake2b256", + "urn:xmpp:hash-function-text-names:id-blake2b512", + "urn:xmpp:jingle:1", + "urn:xmpp:jingle:apps:file-transfer:5", + "urn:xmpp:jingle:security:xtls:0", + "urn:xmpp:jingle:transports:s5b:1", + "urn:xmpp:jingle:transports:ibb:1", + "urn:xmpp:avatar:metadata+notify", + "urn:xmpp:message-moderate:0", + "http://jabber.org/protocol/tune+notify", + "http://jabber.org/protocol/geoloc+notify", + "http://jabber.org/protocol/nick+notify", + "eu.siacs.conversations.axolotl.devicelist+notify", + ], + identities: [ + Identity( + category: "client", + type: "pc", + name: "Gajim" + ) + ] + ); - final hash = await calculateCapabilityHash(data, Sha1()); - expect(hash, "T7fOZrtBnV8sDA2fFTS59vyOyUs="); - */ + final hash = await calculateCapabilityHash(data, Sha1()); + expect(hash, "T7fOZrtBnV8sDA2fFTS59vyOyUs="); + */ }); test('Test Conversations hash computation', () async { - final data = DiscoInfo( - [ - 'eu.siacs.conversations.axolotl.devicelist+notify', - 'http://jabber.org/protocol/caps', - 'http://jabber.org/protocol/chatstates', - 'http://jabber.org/protocol/disco#info', - 'http://jabber.org/protocol/muc', - 'http://jabber.org/protocol/nick+notify', - 'jabber:iq:version', - 'jabber:x:conference', - 'jabber:x:oob', - 'storage:bookmarks+notify', - 'urn:xmpp:avatar:metadata+notify', - 'urn:xmpp:chat-markers:0', - 'urn:xmpp:jingle-message:0', - 'urn:xmpp:jingle:1', - 'urn:xmpp:jingle:apps:dtls:0', - 'urn:xmpp:jingle:apps:file-transfer:3', - 'urn:xmpp:jingle:apps:file-transfer:4', - 'urn:xmpp:jingle:apps:file-transfer:5', - 'urn:xmpp:jingle:apps:rtp:1', - 'urn:xmpp:jingle:apps:rtp:audio', - 'urn:xmpp:jingle:apps:rtp:video', - 'urn:xmpp:jingle:jet-omemo:0', - 'urn:xmpp:jingle:jet:0', - 'urn:xmpp:jingle:transports:ibb:1', - 'urn:xmpp:jingle:transports:ice-udp:1', - 'urn:xmpp:jingle:transports:s5b:1', - 'urn:xmpp:message-correct:0', - 'urn:xmpp:ping', - 'urn:xmpp:receipts', - 'urn:xmpp:time' - ], - [ - Identity( - category: 'client', - type: 'phone', - name: 'Conversations', - ) - ], - [], - JID.fromString('user@server.local/test'), - ); + final data = DiscoInfo( + [ + 'eu.siacs.conversations.axolotl.devicelist+notify', + 'http://jabber.org/protocol/caps', + 'http://jabber.org/protocol/chatstates', + 'http://jabber.org/protocol/disco#info', + 'http://jabber.org/protocol/muc', + 'http://jabber.org/protocol/nick+notify', + 'jabber:iq:version', + 'jabber:x:conference', + 'jabber:x:oob', + 'storage:bookmarks+notify', + 'urn:xmpp:avatar:metadata+notify', + 'urn:xmpp:chat-markers:0', + 'urn:xmpp:jingle-message:0', + 'urn:xmpp:jingle:1', + 'urn:xmpp:jingle:apps:dtls:0', + 'urn:xmpp:jingle:apps:file-transfer:3', + 'urn:xmpp:jingle:apps:file-transfer:4', + 'urn:xmpp:jingle:apps:file-transfer:5', + 'urn:xmpp:jingle:apps:rtp:1', + 'urn:xmpp:jingle:apps:rtp:audio', + 'urn:xmpp:jingle:apps:rtp:video', + 'urn:xmpp:jingle:jet-omemo:0', + 'urn:xmpp:jingle:jet:0', + 'urn:xmpp:jingle:transports:ibb:1', + 'urn:xmpp:jingle:transports:ice-udp:1', + 'urn:xmpp:jingle:transports:s5b:1', + 'urn:xmpp:message-correct:0', + 'urn:xmpp:ping', + 'urn:xmpp:receipts', + 'urn:xmpp:time' + ], + [ + Identity( + category: 'client', + type: 'phone', + name: 'Conversations', + ) + ], + [], + JID.fromString('user@server.local/test'), + ); - final hash = await calculateCapabilityHash(data, Sha1()); - expect(hash, 'zcIke+Rk13ah4d1pwDG7bEZsVwA='); + final hash = await calculateCapabilityHash(data, Sha1()); + expect(hash, 'zcIke+Rk13ah4d1pwDG7bEZsVwA='); }); } diff --git a/packages/moxxmpp/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index a468954..8ad8a23 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -34,7 +34,7 @@ XmppManagerAttributes mkAttributes(void Function(Stanza) callback) { isFeatureSupported: (_) => false, getFullJID: () => JID.fromString('hallo@example.server/uwu'), getSocket: () => StubTCPSocket(play: []), - getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), + getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])), getNegotiatorById: getNegotiatorNullStub, ); } @@ -233,7 +233,11 @@ void main() { ] ); - final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + final XmppConnection conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + fakeSocket, + ); conn.setConnectionSettings(ConnectionSettings( jid: JID.fromString('polynomdivision@test.server'), password: 'aaaa', @@ -355,7 +359,11 @@ void main() { ] ); - final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + final XmppConnection conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + fakeSocket, + ); conn.setConnectionSettings(ConnectionSettings( jid: JID.fromString('polynomdivision@test.server'), password: 'aaaa', @@ -510,7 +518,11 @@ void main() { ] ); - final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + final XmppConnection conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + fakeSocket, + ); conn.setConnectionSettings(ConnectionSettings( jid: JID.fromString('polynomdivision@test.server'), password: 'aaaa', @@ -602,7 +614,11 @@ void main() { ] ); - final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + final XmppConnection conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + fakeSocket, + ); conn.setConnectionSettings(ConnectionSettings( jid: JID.fromString('polynomdivision@test.server'), password: 'aaaa', @@ -694,7 +710,11 @@ void main() { ] ); - final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + final XmppConnection conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + fakeSocket, + ); conn.setConnectionSettings(ConnectionSettings( jid: JID.fromString('polynomdivision@test.server'), password: 'aaaa', diff --git a/packages/moxxmpp/test/xeps/xep_0280_test.dart b/packages/moxxmpp/test/xeps/xep_0280_test.dart index 3b5e932..8218c27 100644 --- a/packages/moxxmpp/test/xeps/xep_0280_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0280_test.dart @@ -22,7 +22,7 @@ void main() { isFeatureSupported: (_) => false, getFullJID: () => JID.fromString('bob@xmpp.example/uwu'), getSocket: () => StubTCPSocket(play: []), - getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), + getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])), getNegotiatorById: getNegotiatorNullStub, ); final manager = CarbonsManager(); diff --git a/packages/moxxmpp/test/xeps/xep_0352_test.dart b/packages/moxxmpp/test/xeps/xep_0352_test.dart index 592816f..49fd076 100644 --- a/packages/moxxmpp/test/xeps/xep_0352_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0352_test.dart @@ -50,7 +50,7 @@ void main() { isFeatureSupported: (_) => false, getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), getSocket: () => StubTCPSocket(play: []), - getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), + getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])), ), ); @@ -79,7 +79,7 @@ void main() { isFeatureSupported: (_) => false, getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), getSocket: () => StubTCPSocket(play: []), - getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), + getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])), ), ); diff --git a/packages/moxxmpp/test/xeps/xep_0363_test.dart b/packages/moxxmpp/test/xeps/xep_0363_test.dart index 9330968..836b6fa 100644 --- a/packages/moxxmpp/test/xeps/xep_0363_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0363_test.dart @@ -3,52 +3,52 @@ import 'package:test/test.dart'; void main() { group('Test the XEP-0363 header preparation', () { - test('invariance', () { - final headers = { - 'authorization': 'Basic Base64String==', - 'cookie': 'foo=bar; user=romeo' - }; - expect( - prepareHeaders(headers), - headers, - ); - }); - test('invariance through uppercase', () { - final headers = { - 'Authorization': 'Basic Base64String==', - 'Cookie': 'foo=bar; user=romeo' - }; - expect( - prepareHeaders(headers), - headers, - ); - }); - test('remove unspecified headers', () { - final headers = { - 'Authorization': 'Basic Base64String==', - 'Cookie': 'foo=bar; user=romeo', - 'X-Tracking': 'Base64String==' - }; - expect( - prepareHeaders(headers), - { - 'Authorization': 'Basic Base64String==', - 'Cookie': 'foo=bar; user=romeo', - } - ); - }); - test('remove newlines', () { - final headers = { - 'Authorization': '\n\nBasic Base64String==\n\n', - '\nCookie\r\n': 'foo=bar; user=romeo', - }; - expect( - prepareHeaders(headers), - { - 'Authorization': 'Basic Base64String==', - 'Cookie': 'foo=bar; user=romeo', - } - ); - }); + test('invariance', () { + final headers = { + 'authorization': 'Basic Base64String==', + 'cookie': 'foo=bar; user=romeo' + }; + expect( + prepareHeaders(headers), + headers, + ); + }); + test('invariance through uppercase', () { + final headers = { + 'Authorization': 'Basic Base64String==', + 'Cookie': 'foo=bar; user=romeo' + }; + expect( + prepareHeaders(headers), + headers, + ); + }); + test('remove unspecified headers', () { + final headers = { + 'Authorization': 'Basic Base64String==', + 'Cookie': 'foo=bar; user=romeo', + 'X-Tracking': 'Base64String==' + }; + expect( + prepareHeaders(headers), + { + 'Authorization': 'Basic Base64String==', + 'Cookie': 'foo=bar; user=romeo', + } + ); + }); + test('remove newlines', () { + final headers = { + 'Authorization': '\n\nBasic Base64String==\n\n', + '\nCookie\r\n': 'foo=bar; user=romeo', + }; + expect( + prepareHeaders(headers), + { + 'Authorization': 'Basic Base64String==', + 'Cookie': 'foo=bar; user=romeo', + } + ); + }); }); } diff --git a/packages/moxxmpp/test/xmpp_test.dart b/packages/moxxmpp/test/xmpp_test.dart index bbad56e..b552471 100644 --- a/packages/moxxmpp/test/xmpp_test.dart +++ b/packages/moxxmpp/test/xmpp_test.dart @@ -25,7 +25,7 @@ Future testRosterManager(String bareJid, String resource, String stanzaStr isFeatureSupported: (_) => false, getFullJID: () => JID.fromString('$bareJid/$resource'), getSocket: () => StubTCPSocket(play: []), - getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), + getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])), ),); final stanza = Stanza.fromXMLNode(XMLNode.fromString(stanzaString)); @@ -118,7 +118,10 @@ void main() { ], ); // TODO: This test is broken since we query the server and enable carbons - final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + final XmppConnection conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + fakeSocket); conn.setConnectionSettings(ConnectionSettings( jid: JID.fromString('polynomdivision@test.server'), password: 'aaaa', @@ -172,7 +175,11 @@ void main() { ], ); var receivedEvent = false; - final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + final XmppConnection conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + fakeSocket, + ); conn.setConnectionSettings(ConnectionSettings( jid: JID.fromString('polynomdivision@test.server'), password: 'aaaa', @@ -226,7 +233,11 @@ void main() { ], ); var receivedEvent = false; - final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + final XmppConnection conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + fakeSocket, + ); conn.setConnectionSettings(ConnectionSettings( jid: JID.fromString('polynomdivision@test.server'), password: 'aaaa', @@ -326,7 +337,7 @@ void main() { isFeatureSupported: (_) => false, getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), getSocket: () => StubTCPSocket(play: []), - getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), + getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])), ),); // NOTE: Based on https://gultsch.de/gajim_roster_push_and_message_interception.html