diff --git a/packages/moxxmpp/test/helpers/logging.dart b/packages/moxxmpp/test/helpers/logging.dart new file mode 100644 index 0000000..08c1e8d --- /dev/null +++ b/packages/moxxmpp/test/helpers/logging.dart @@ -0,0 +1,10 @@ +import 'package:logging/logging.dart'; + +/// Enable logging using logger. +void initLogger() { + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + // ignore: avoid_print + print('[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}'); + }); +} diff --git a/packages/moxxmpp/test/helpers/xml.dart b/packages/moxxmpp/test/helpers/xml.dart new file mode 100644 index 0000000..6749662 --- /dev/null +++ b/packages/moxxmpp/test/helpers/xml.dart @@ -0,0 +1,28 @@ +import 'package:moxxmpp/moxxmpp.dart'; + +bool compareXMLNodes(XMLNode actual, XMLNode expectation, { bool ignoreId = true}) { + // Compare attributes + if (expectation.tag != actual.tag) return false; + + final attributesEqual = expectation.attributes.keys.every((key) { + // Ignore the stanza ID + if (key == 'id' && ignoreId) return true; + + return actual.attributes[key] == expectation.attributes[key]; + }); + if (!attributesEqual) return false; + + final actualAttributeLength = !ignoreId ? actual.attributes.length : ( + actual.attributes.containsKey('id') ? actual.attributes.length - 1 : 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 (expectation.innerText() != '' && actual.innerText() != expectation.innerText()) return false; + + return expectation.children.every((childe) { + return actual.children.any((childa) => compareXMLNodes(childa, childe)); + }); +} diff --git a/packages/moxxmpp/test/helpers/xmpp.dart b/packages/moxxmpp/test/helpers/xmpp.dart new file mode 100644 index 0000000..2fdc538 --- /dev/null +++ b/packages/moxxmpp/test/helpers/xmpp.dart @@ -0,0 +1,137 @@ +import 'dart:async'; +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; + +import 'xml.dart'; + +T? getNegotiatorNullStub(String id) { + return null; +} + +T? getManagerNullStub(String id) { + return null; +} + +abstract class ExpectationBase { + + ExpectationBase(this.expectation, this.response); + final String expectation; + final String response; + + /// Return true if [input] matches the expectation + bool matches(String input); +} + +/// Literally compare the input with the expectation +class StringExpectation extends ExpectationBase { + StringExpectation(String expectation, String response) : super(expectation, response); + + @override + bool matches(String input) => input == expectation; +} + +/// +class StanzaExpectation extends ExpectationBase { + StanzaExpectation(String expectation, String response, {this.ignoreId = false, this.adjustId = false }) : super(expectation, response); + final bool ignoreId; + final bool adjustId; + + @override + bool matches(String input) { + final ex = XMLNode.fromString(expectation); + final recv = XMLNode.fromString(expectation); + + return compareXMLNodes(recv, ex, ignoreId: ignoreId); + } +} + +class StubTCPSocket extends BaseSocketWrapper { // Request -> Response(s) + + StubTCPSocket({ required List play }) + : _play = play, + _dataStream = StreamController.broadcast(), + _eventStream = StreamController.broadcast(); + int _state = 0; + final StreamController _dataStream; + final StreamController _eventStream; + final List _play; + String? lastId; + + @override + bool isSecure() => true; + + @override + Future secure(String domain) async => true; + + @override + Future connect(String domain, { String? host, int? port }) async => true; + + @override + Stream getDataStream() => _dataStream.stream.asBroadcastStream(); + @override + Stream getEventStream() => _eventStream.stream.asBroadcastStream(); + + /// Let the "connection" receive [data]. + void injectRawXml(String data) { + print('<== $data'); + _dataStream.add(data); + } + + @override + void write(Object? object, { String? redact }) { + var str = object as String; + // ignore: avoid_print + print('==> $str'); + + if (_state >= _play.length) { + _state++; + return; + } + + final expectation = _play[_state]; + + // TODO: Implement an XML matcher + if (str.startsWith("")) { + str = str.substring(21); + } + + if (str.endsWith('')) { + str = str.substring(0, str.length - 16); + } + + if (!expectation.matches(str)) { + expect(true, false, reason: 'Expected ${expectation.expectation}, got $str'); + } + + // Make sure to only progress if everything passed so far + _state++; + + var response = expectation.response; + if (expectation is StanzaExpectation) { + final inputNode = XMLNode.fromString(str); + lastId = inputNode.attributes['id']; + + if (expectation.adjustId) { + final outputNode = XMLNode.fromString(response); + + outputNode.attributes['id'] = inputNode.attributes['id']!; + response = outputNode.toXml(); + } + } + + print("<== $response"); + _dataStream.add(response); + } + + @override + void close() {} + + int getState() => _state; + void resetState() => _state = 0; + + @override + bool whitespacePingAllowed() => true; + + @override + bool managesKeepalives() => false; +} diff --git a/packages/moxxmpp/test/jid_test.dart b/packages/moxxmpp/test/jid_test.dart new file mode 100644 index 0000000..aefc963 --- /dev/null +++ b/packages/moxxmpp/test/jid_test.dart @@ -0,0 +1,41 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; + +void main() { + test('Parse a full JID', () { + final jid = JID.fromString('test@server/abc'); + + expect(jid.local, 'test'); + expect(jid.domain, 'server'); + expect(jid.resource, 'abc'); + expect(jid.toString(), 'test@server/abc'); + }); + + test('Parse a bare JID', () { + final jid = JID.fromString('test@server'); + + expect(jid.local, 'test'); + expect(jid.domain, 'server'); + expect(jid.resource, ''); + expect(jid.toString(), 'test@server'); + }); + + test('Parse a JID with no local part', () { + final jid = JID.fromString('server/abc'); + + expect(jid.local, ''); + expect(jid.domain, 'server'); + expect(jid.resource, 'abc'); + expect(jid.toString(), 'server/abc'); + }); + + test('Equality', () { + expect(JID.fromString('hallo@welt/abc') == JID('hallo', 'welt', 'abc'), true); + expect(JID.fromString('hallo@welt') == JID('hallo', 'welt', 'a'), false); + }); + + test('Whitespaces', () { + expect(JID.fromString('hallo@welt ') == JID('hallo', 'welt', ''), true); + expect(JID.fromString('hallo@welt/abc ') == JID('hallo', 'welt', 'abc'), true); + }); +} diff --git a/packages/moxxmpp/test/moxxmpp_test.dart b/packages/moxxmpp/test/moxxmpp_test.dart deleted file mode 100644 index 6ad5c46..0000000 --- a/packages/moxxmpp/test/moxxmpp_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:moxxmpp/moxxmpp.dart'; -import 'package:test/test.dart'; - -void main() { - group('A group of tests', () { - final awesome = Awesome(); - - setUp(() { - // Additional setup goes here. - }); - - test('First Test', () { - expect(awesome.isAwesome, isTrue); - }); - }); -} diff --git a/packages/moxxmpp/test/negotiator_test.dart b/packages/moxxmpp/test/negotiator_test.dart new file mode 100644 index 0000000..a8bf526 --- /dev/null +++ b/packages/moxxmpp/test/negotiator_test.dart @@ -0,0 +1,94 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; +import 'helpers/logging.dart'; +import 'helpers/xmpp.dart'; + +const exampleXmlns1 = 'im:moxxy:example1'; +const exampleNamespace1 = 'im.moxxy.test.example1'; +const exampleXmlns2 = 'im:moxxy:example2'; +const exampleNamespace2 = 'im.moxxy.test.example2'; + +class StubNegotiator1 extends XmppFeatureNegotiatorBase { + StubNegotiator1() : called = false, super(1, false, exampleXmlns1, exampleNamespace1); + + bool called; + + @override + Future negotiate(XMLNode nonza) async { + called = true; + state = NegotiatorState.done; + } +} + +class StubNegotiator2 extends XmppFeatureNegotiatorBase { + StubNegotiator2() : called = false, super(10, false, exampleXmlns2, exampleNamespace2); + + bool called; + + @override + Future negotiate(XMLNode nonza) async { + called = true; + state = NegotiatorState.done; + } +} + +void main() { + initLogger(); + + final stubSocket = StubTCPSocket( + play: [ + StringExpectation( + "", + ''' + + + + + ''', + ), + ], + ); + + final connection = XmppConnection(TestingReconnectionPolicy(), stubSocket) + ..registerFeatureNegotiators([ + StubNegotiator1(), + StubNegotiator2(), + ]) + ..registerManagers([ + PresenceManager(), + RosterManager(), + DiscoManager(), + PingManager(), + ]) + ..setConnectionSettings( + ConnectionSettings( + jid: JID.fromString('user@test.server'), + password: 'abc123', + useDirectTLS: true, + allowPlainAuth: false, + ), + ); + final features = [ + XMLNode.xmlns(tag: 'example1', xmlns: exampleXmlns1), + XMLNode.xmlns(tag: 'example2', xmlns: exampleXmlns2), + ]; + + test('Test the priority system', () { + expect(connection.getNextNegotiator(features)?.id, exampleNamespace2); + }); + + test('Test negotiating features with no stream restarts', () async { + await connection.connect(); + await Future.delayed(const Duration(seconds: 3), () { + final negotiator1 = connection.getNegotiatorById(exampleNamespace1); + final negotiator2 = connection.getNegotiatorById(exampleNamespace2); + expect(negotiator1?.called, true); + expect(negotiator2?.called, true); + }); + }); +} diff --git a/packages/moxxmpp/test/roster_test.dart b/packages/moxxmpp/test/roster_test.dart new file mode 100644 index 0000000..8842b11 --- /dev/null +++ b/packages/moxxmpp/test/roster_test.dart @@ -0,0 +1,322 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; + +// TODO(PapaTutuWawa): Fix tests + +typedef AddRosterItemFunction = Future Function( + String avatarUrl, + String avatarHash, + String jid, + String title, + String subscription, + String ask, + { + List groups, + } +); + +typedef UpdateRosterItemFunction = Future Function( + int id, { + String? avatarUrl, + String? avatarHash, + String? title, + String? subscription, + String? ask, + List? groups, + } +); + +AddRosterItemFunction mkAddRosterItem(void Function(String) callback) { + return ( + String avatarUrl, + String avatarHash, + String jid, + String title, + String subscription, + String ask, + { + List groups = const [], + } + ) async { + callback(jid); + return await addRosterItemFromData( + avatarUrl, + avatarHash, + jid, + title, + subscription, + ask, + groups: groups, + ); + }; +} + +Future addRosterItemFromData( + String avatarUrl, + String avatarHash, + String jid, + String title, + String subscription, + String ask, + { + List groups = const [], + } +) async => RosterItem( + 0, + avatarUrl, + avatarHash, + jid, + title, + subscription, + ask, + groups, +); + +UpdateRosterItemFunction mkRosterUpdate(List roster) { + return ( + int id, { + String? avatarUrl, + String? avatarHash, + String? title, + String? subscription, + String? ask, + List? groups, + } + ) async { + final item = firstWhereOrNull(roster, (RosterItem item) => item.id == id)!; + return item.copyWith( + avatarUrl: avatarUrl ?? item.avatarUrl, + avatarHash: avatarHash ?? item.avatarHash, + title: title ?? item.title, + subscription: subscription ?? item.subscription, + ask: ask ?? item.ask, + groups: groups ?? item.groups, + ); + }; +} + +void main() { + final localRosterSingle = [ + RosterItem( + 0, + '', + '', + 'hallo@server.example', + 'hallo', + 'none', + '', + [], + ) + ]; + final localRosterDouble = [ + RosterItem( + 0, + '', + '', + 'hallo@server.example', + 'hallo', + 'none', + '', + [], + ), + RosterItem( + 1, + '', + '', + 'welt@different.server.example', + 'welt', + 'from', + '', + [ 'Friends' ], + ) + ]; + + group('Test roster pushes', () { + test('Test removing an item', () async { + var removeCalled = false; + var addCalled = false; + final result = await processRosterDiff( + localRosterDouble, + [ + XmppRosterItem( + jid: 'hallo@server.example', subscription: 'remove', + ) + ], + true, + mkAddRosterItem((_) { addCalled = true; }), + mkRosterUpdate(localRosterDouble), + (jid) async { + if (jid == 'hallo@server.example') { + removeCalled = true; + } + }, + (_) async => null, + (_, { String? id }) async {}, + ); + + expect(result.removed, [ 'hallo@server.example' ]); + expect(result.modified.length, 0); + expect(result.added.length, 0); + expect(removeCalled, true); + expect(addCalled, false); + }); + + test('Test adding an item', () async { + var removeCalled = false; + var addCalled = false; + final result = await processRosterDiff( + localRosterSingle, + [ + XmppRosterItem( + jid: 'welt@different.server.example', + subscription: 'from', + ) + ], + true, + mkAddRosterItem( + (jid) { + if (jid == 'welt@different.server.example') { + addCalled = true; + } + } + ), + mkRosterUpdate(localRosterSingle), + (_) async { removeCalled = true; }, + (_) async => null, + (_, { String? id }) async {}, + ); + + expect(result.removed, [ ]); + expect(result.modified.length, 0); + expect(result.added.length, 1); + expect(result.added.first.subscription, 'from'); + expect(removeCalled, false); + expect(addCalled, true); + }); + + test('Test modifying an item', () async { + var removeCalled = false; + var addCalled = false; + final result = await processRosterDiff( + localRosterDouble, + [ + XmppRosterItem( + jid: 'welt@different.server.example', + subscription: 'both', + name: 'The World', + ) + ], + true, + mkAddRosterItem((_) { addCalled = false; }), + mkRosterUpdate(localRosterDouble), + (_) async { removeCalled = true; }, + (_) async => null, + (_, { String? id }) async {}, + ); + + expect(result.removed, [ ]); + expect(result.modified.length, 1); + expect(result.added.length, 0); + expect(result.modified.first.subscription, 'both'); + expect(result.modified.first.jid, 'welt@different.server.example'); + expect(result.modified.first.title, 'The World'); + expect(removeCalled, false); + expect(addCalled, false); + }); + }); + + group('Test roster requests', () { + test('Test removing an item', () async { + var removeCalled = false; + var addCalled = false; + final result = await processRosterDiff( + localRosterSingle, + [], + false, + mkAddRosterItem((_) { addCalled = true; }), + mkRosterUpdate(localRosterDouble), + (jid) async { + if (jid == 'hallo@server.example') { + removeCalled = true; + } + }, + (_) async => null, + (_, { String? id }) async {}, + ); + + expect(result.removed, [ 'hallo@server.example' ]); + expect(result.modified.length, 0); + expect(result.added.length, 0); + expect(removeCalled, true); + expect(addCalled, false); + }); + + test('Test adding an item', () async { + var removeCalled = false; + var addCalled = false; + final result = await processRosterDiff( + localRosterSingle, + [ + XmppRosterItem( + jid: 'hallo@server.example', + name: 'hallo', + subscription: 'none', + ), + XmppRosterItem( + jid: 'welt@different.server.example', + subscription: 'both', + ) + ], + false, + mkAddRosterItem( + (jid) { + if (jid == 'welt@different.server.example') { + addCalled = true; + } + } + ), + mkRosterUpdate(localRosterSingle), + (_) async { removeCalled = true; }, + (_) async => null, + (_, { String? id }) async {}, + ); + + expect(result.removed, [ ]); + expect(result.modified.length, 0); + expect(result.added.length, 1); + expect(result.added.first.subscription, 'both'); + expect(removeCalled, false); + expect(addCalled, true); + }); + + test('Test modifying an item', () async { + var removeCalled = false; + var addCalled = false; + final result = await processRosterDiff( + localRosterSingle, + [ + XmppRosterItem( + jid: 'hallo@server.example', + subscription: 'both', + name: 'Hallo Welt', + ) + ], + false, + mkAddRosterItem((_) { addCalled = false; }), + mkRosterUpdate(localRosterDouble), + (_) async { removeCalled = true; }, + (_) async => null, + (_, { String? id }) async {}, + ); + + expect(result.removed, [ ]); + expect(result.modified.length, 1); + expect(result.added.length, 0); + expect(result.modified.first.subscription, 'both'); + expect(result.modified.first.jid, 'hallo@server.example'); + expect(result.modified.first.title, 'Hallo Welt'); + expect(removeCalled, false); + expect(addCalled, false); + }); + }); +} diff --git a/packages/moxxmpp/test/stanza_test.dart b/packages/moxxmpp/test/stanza_test.dart new file mode 100644 index 0000000..e54b8a8 --- /dev/null +++ b/packages/moxxmpp/test/stanza_test.dart @@ -0,0 +1,50 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; + +void main() { + test('Make sure reply does not copy the children', () { + final stanza = Stanza.iq( + to: 'hallo', + from: 'world', + id: 'abc123', + type: 'get', + children: [ + XMLNode(tag: 'test-tag'), + XMLNode(tag: 'test-tag2') + ], + ); + + final reply = stanza.reply(); + + expect(reply.children, []); + expect(reply.type, 'result'); + expect(reply.from, stanza.to); + expect(reply.to, stanza.from); + expect(reply.id, stanza.id); + }); + + test('Make sure reply includes the new children', () { + final stanza = Stanza.iq( + to: 'hallo', + from: 'world', + id: 'abc123', + type: 'get', + children: [ + XMLNode(tag: 'test-tag'), + XMLNode(tag: 'test-tag2') + ], + ); + + final reply = stanza.reply( + children: [ + XMLNode.xmlns( + tag: 'test', + xmlns: 'test', + ) + ], + ); + + expect(reply.children.length, 1); + expect(reply.firstTag('test') != null, true); + }); +} diff --git a/packages/moxxmpp/test/stanzahandler_test.dart b/packages/moxxmpp/test/stanzahandler_test.dart new file mode 100644 index 0000000..d5f1302 --- /dev/null +++ b/packages/moxxmpp/test/stanzahandler_test.dart @@ -0,0 +1,89 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; + +final stanza1 = Stanza.iq(children: [ + XMLNode.xmlns(tag: 'tag', xmlns: 'owo') +],); +final stanza2 = Stanza.message(children: [ + XMLNode.xmlns(tag: 'some-other-tag', xmlns: 'owo') +],); + +void main() { + test('match all', () { + final handler = StanzaHandler(callback: (stanza, _) async => StanzaHandlerData(true, false, null, stanza)); + + expect(handler.matches(Stanza.iq()), true); + expect(handler.matches(Stanza.message()), true); + expect(handler.matches(Stanza.presence()), true); + expect(handler.matches(stanza1), true); + expect(handler.matches(stanza2), true); + }); + test('xmlns matching', () { + final handler = StanzaHandler( + callback: (stanza, _) async => StanzaHandlerData(true, false, null, stanza), + tagXmlns: 'owo', + ); + + expect(handler.matches(Stanza.iq()), false); + expect(handler.matches(Stanza.message()), false); + expect(handler.matches(Stanza.presence()), false); + expect(handler.matches(stanza1), true); + expect(handler.matches(stanza2), true); + }); + test('stanzaTag matching', () { + var run = false; + final handler = StanzaHandler(callback: (stanza, _) async { + run = true; + return StanzaHandlerData(true, false, null, stanza); + }, stanzaTag: 'iq',); + + expect(handler.matches(Stanza.iq()), true); + expect(handler.matches(Stanza.message()), false); + expect(handler.matches(Stanza.presence()), false); + expect(handler.matches(stanza1), true); + expect(handler.matches(stanza2), false); + + handler.callback(stanza2, StanzaHandlerData(false, false, null, stanza2)); + expect(run, true); + }); + test('tagName matching', () { + final handler = StanzaHandler( + callback: (stanza, _) async => StanzaHandlerData(true, false, null, stanza), + tagName: 'tag', + ); + + expect(handler.matches(Stanza.iq()), false); + expect(handler.matches(Stanza.message()), false); + expect(handler.matches(Stanza.presence()), false); + expect(handler.matches(stanza1), true); + expect(handler.matches(stanza2), false); + }); + test('combined matching', () { + final handler = StanzaHandler( + callback: (stanza, _) async => StanzaHandlerData(true, false, null, stanza), + tagName: 'tag', + stanzaTag: 'iq', + tagXmlns: 'owo', + ); + + expect(handler.matches(Stanza.iq()), false); + expect(handler.matches(Stanza.message()), false); + expect(handler.matches(Stanza.presence()), false); + expect(handler.matches(stanza1), true); + expect(handler.matches(stanza2), false); + }); + + test('sorting', () { + final handlerList = [ + StanzaHandler(callback: (stanza, _) async => StanzaHandlerData(true, false, null, stanza), tagName: '1', priority: 100), + StanzaHandler(callback: (stanza, _) async => StanzaHandlerData(true, false, null, stanza), tagName: '2'), + StanzaHandler(callback: (stanza, _) async => StanzaHandlerData(true, false, null, stanza), tagName: '3', priority: 50) + ]; + + handlerList.sort(stanzaHandlerSortComparator); + + expect(handlerList[0].tagName, '1'); + expect(handlerList[1].tagName, '3'); + expect(handlerList[2].tagName, '2'); + }); +} diff --git a/packages/moxxmpp/test/stringxml_test.dart b/packages/moxxmpp/test/stringxml_test.dart new file mode 100644 index 0000000..7d1efc4 --- /dev/null +++ b/packages/moxxmpp/test/stringxml_test.dart @@ -0,0 +1,32 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; +import 'package:xml/xml.dart'; +import 'helpers/xml.dart'; + +void main() { + test('Test stringxml', () { + final child = XMLNode(tag: 'uwu', attributes: { 'strength': 10 }); + final stanza = XMLNode.xmlns(tag: 'uwu-meter', xmlns: 'uwu', children: [ child ]); + expect(XMLNode(tag: 'iq', attributes: {'xmlns': 'uwu'}).toXml(), ""); + expect(XMLNode.xmlns(tag: 'iq', xmlns: 'uwu', attributes: {'how': 'uwu'}).toXml(), ""); + expect(stanza.toXml(), ""); + + expect(StreamHeaderNonza('uwu.server').toXml(), ""); + + expect(XMLNode(tag: 'text', attributes: {}, text: 'hallo').toXml(), 'hallo'); + expect(XMLNode(tag: 'text', attributes: { 'world': 'no' }, text: 'hallo').toXml(), "hallo"); + expect(XMLNode(tag: 'text', attributes: {}, text: 'hallo').toXml(), 'hallo'); + expect(XMLNode(tag: 'text', attributes: {}, text: 'test').innerText(), 'test'); + }); + + test('Test XmlElement', () { + expect(XMLNode.fromXmlElement(XmlDocument.parse("").firstElementChild!).toXml(), ""); + }); + + test('Test the find functions', () { + final node1 = XMLNode.fromString('Hallo'); + + expect(compareXMLNodes(node1.firstTag('body')!, XMLNode.fromString('Hallo')), true); + expect(compareXMLNodes(node1.firstTagByXmlns('a')!, XMLNode.fromString('')), true); + }); +} diff --git a/packages/moxxmpp/test/xeps/xep_0004_test.dart b/packages/moxxmpp/test/xeps/xep_0004_test.dart new file mode 100644 index 0000000..57cf823 --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0004_test.dart @@ -0,0 +1,16 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; + +void main() { + test('Parsing', () { + 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'); + }); +} diff --git a/packages/moxxmpp/test/xeps/xep_0030.dart b/packages/moxxmpp/test/xeps/xep_0030.dart new file mode 100644 index 0000000..1e50b31 --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0030.dart @@ -0,0 +1,111 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; + +import '../helpers/xmpp.dart'; + +void main() { + test('Test having multiple disco requests for the same JID', () async { + final fakeSocket = StubTCPSocket( + play: [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '' + ), + StringExpectation( + "", + ''' + + + + + + + + + + + +''', + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + ignoreId: true, + ), + StringExpectation( + "chat", + '', + ), + StanzaExpectation( + "", + '', + ignoreId: true, + adjustId: false, + ), + + ], + ); + final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), socket: fakeSocket); + conn.setConnectionSettings(ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + useDirectTLS: true, + allowPlainAuth: true, + ),); + conn.registerManagers([ + PresenceManager(), + RosterManager(), + DiscoManager(), + PingManager(), + ]); + conn.registerFeatureNegotiators( + [ + SaslPlainNegotiator(), + SaslScramNegotiator(10, '', '', ScramHashType.sha512), + ResourceBindingNegotiator(), + ] + ); + + final disco = conn.getManagerById(discoManager)!; + + await conn.connect(); + await Future.delayed(const Duration(seconds: 3)); + + final jid = JID.fromString('romeo@montague.lit/orchard'); + final result1 = disco.discoInfoQuery(jid.toString()); + final result2 = disco.discoInfoQuery(jid.toString()); + + await Future.delayed(const Duration(seconds: 1)); + expect( + disco.getRunningInfoQueries(DiscoCacheKey(jid.toString(), null)).length, + 1, + ); + fakeSocket.injectRawXml(""); + + await Future.delayed(const Duration(seconds: 2)); + + expect(fakeSocket.getState(), 6); + expect(await result1, await result2); + expect(disco.hasInfoQueriesRunning(), false); + }); +} diff --git a/packages/moxxmpp/test/xeps/xep_0115_test.dart b/packages/moxxmpp/test/xeps/xep_0115_test.dart new file mode 100644 index 0000000..244ebb7 --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0115_test.dart @@ -0,0 +1,167 @@ +import 'package:cryptography/cryptography.dart'; +import 'package:moxxmpp/moxxmpp.dart'; +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 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'), + ); + + 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" + ) + ] + ); + + 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 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 new file mode 100644 index 0000000..e099f35 --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -0,0 +1,737 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; +import '../helpers/logging.dart'; +import '../helpers/xmpp.dart'; + +Future runIncomingStanzaHandlers(StreamManagementManager man, Stanza stanza) async { + for (final handler in man.getIncomingStanzaHandlers()) { + if (handler.matches(stanza)) await handler.callback(stanza, StanzaHandlerData(false, false, null, stanza)); + } +} + +Future runOutgoingStanzaHandlers(StreamManagementManager man, Stanza stanza) async { + for (final handler in man.getOutgoingPostStanzaHandlers()) { + if (handler.matches(stanza)) await handler.callback(stanza, StanzaHandlerData(false, false, null, stanza)); + } +} + +XmppManagerAttributes mkAttributes(void Function(Stanza) callback) { + return XmppManagerAttributes( + sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false }) async { + callback(stanza); + + return Stanza.message(); + }, + sendNonza: (nonza) {}, + sendEvent: (event) {}, + getManagerById: getManagerNullStub, + 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(), StubTCPSocket(play: [])), + getNegotiatorById: getNegotiatorNullStub, + ); +} + +XMLNode mkAck(int h) => XMLNode.xmlns(tag: 'a', xmlns: 'urn:xmpp:sm:3', attributes: { 'h': h.toString() }); + +void main() { + initLogger(); + + final stanza = Stanza( + to: 'some.user@server.example', + tag: 'message', + ); + + test('Test stream with SM enablement', () async { + final attributes = mkAttributes((_) {}); + final manager = StreamManagementManager(); + manager.register(attributes); + + // [...] + // // + await manager.onXmppEvent(StreamManagementEnabledEvent(resource: 'hallo')); + expect(manager.state.c2s, 0); + expect(manager.state.s2c, 0); + + expect(manager.isStreamManagementEnabled(), true); + + // Send a stanza 5 times + for (var i = 0; i < 5; i++) { + await runOutgoingStanzaHandlers(manager, stanza); + } + expect(manager.state.c2s, 5); + + // Receive 3 stanzas + for (var i = 0; i < 3; i++) { + await runIncomingStanzaHandlers(manager, stanza); + } + expect(manager.state.s2c, 3); + }); + + group('Acking', () { + test('Test completely clearing the queue', () async { + final attributes = mkAttributes((_) {}); + final manager = StreamManagementManager(); + manager.register(attributes); + + await manager.onXmppEvent(StreamManagementEnabledEvent(resource: 'hallo')); + + // Send a stanza 5 times + for (var i = 0; i < 5; i++) { + await runOutgoingStanzaHandlers(manager, stanza); + } + + // + await manager.runNonzaHandlers(mkAck(5)); + expect(manager.getUnackedStanzas().length, 0); + }); + test('Test partially clearing the queue', () async { + final attributes = mkAttributes((_) {}); + final manager = StreamManagementManager(); + manager.register(attributes); + + await manager.onXmppEvent(StreamManagementEnabledEvent(resource: 'hallo')); + + // Send a stanza 5 times + for (var i = 0; i < 5; i++) { + await runOutgoingStanzaHandlers(manager, stanza); + } + + // + await manager.runNonzaHandlers(mkAck(3)); + expect(manager.getUnackedStanzas().length, 2); + }); + test('Send an ack with h > c2s', () async { + final attributes = mkAttributes((_) {}); + final manager = StreamManagementManager(); + manager.register(attributes); + + await manager.onXmppEvent(StreamManagementEnabledEvent(resource: 'hallo')); + + // Send a stanza 5 times + for (var i = 0; i < 5; i++) { + await runOutgoingStanzaHandlers(manager, stanza); + } + + // + await manager.runNonzaHandlers(mkAck(6)); + expect(manager.getUnackedStanzas().length, 0); + expect(manager.state.c2s, 6); + }); + test('Send an ack with h < c2s', () async { + final attributes = mkAttributes((_) {}); + final manager = StreamManagementManager(); + manager.register(attributes); + + await manager.onXmppEvent(StreamManagementEnabledEvent(resource: 'hallo')); + + // Send a stanza 5 times + for (var i = 0; i < 5; i++) { + await runOutgoingStanzaHandlers(manager, stanza); + } + + // + await manager.runNonzaHandlers(mkAck(3)); + expect(manager.getUnackedStanzas().length, 2); + expect(manager.state.c2s, 5); + }); + }); + + group('Counting acks', () { + test('Sending all pending acks at once', () async { + final attributes = mkAttributes((_) {}); + final manager = StreamManagementManager(); + manager.register(attributes); + await manager.onXmppEvent(StreamManagementEnabledEvent(resource: 'hallo')); + + // Send a stanza 5 times + for (var i = 0; i < 5; i++) { + await runOutgoingStanzaHandlers(manager, stanza); + } + expect(await manager.getPendingAcks(), 5); + + // Ack all of them at once + await manager.runNonzaHandlers(mkAck(5)); + expect(await manager.getPendingAcks(), 0); + }); + test('Sending partial pending acks at once', () async { + final attributes = mkAttributes((_) {}); + final manager = StreamManagementManager(); + manager.register(attributes); + await manager.onXmppEvent(StreamManagementEnabledEvent(resource: 'hallo')); + + // Send a stanza 5 times + for (var i = 0; i < 5; i++) { + await runOutgoingStanzaHandlers(manager, stanza); + } + expect(await manager.getPendingAcks(), 5); + + // Ack only 3 of them at once + await manager.runNonzaHandlers(mkAck(3)); + expect(await manager.getPendingAcks(), 2); + }); + + test('Test counting incoming stanzas for which handlers end early', () async { + final fakeSocket = StubTCPSocket( + play: [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '' + ), + StringExpectation( + "", + ''' + + + + + + + + + + + +''', + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + ignoreId: true, + ), + StringExpectation( + "", + '', + ), + ] + ); + + final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + conn.setConnectionSettings(ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + useDirectTLS: true, + allowPlainAuth: true, + ),); + final sm = StreamManagementManager(); + conn.registerManagers([ + PresenceManager(), + RosterManager(), + DiscoManager(), + PingManager(), + sm, + CarbonsManager()..forceEnable(), + ]); + conn.registerFeatureNegotiators( + [ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + StreamManagementNegotiator(), + ] + ); + + await conn.connect(); + await Future.delayed(const Duration(seconds: 3)); + expect(fakeSocket.getState(), 6); + expect(await conn.getConnectionState(), XmppConnectionState.connected); + expect( + conn.getManagerById(smManager)!.isStreamManagementEnabled(), + true, + ); + + // Send an invalid carbon + fakeSocket.injectRawXml(''' + + + + + What man art thou that, thus bescreen'd in night, so stumblest on my counsel? + 0e3141cd80894871a68e6fe6b1ec56fa + + + + + '''); + + await Future.delayed(const Duration(seconds: 2)); + expect(sm.state.s2c, 1); + }); + + test('Test counting incoming stanzas that are awaited', () async { + final fakeSocket = StubTCPSocket( + play: [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '' + ), + StringExpectation( + "", + ''' + + + + + + + + + + + +''', + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + ignoreId: true, + ), + StringExpectation( + "", + '', + ), + StringExpectation( + "chat", + '', + ), + StanzaExpectation( + "", + "", + ignoreId: true, + adjustId: true, + ), + ] + ); + + final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + conn.setConnectionSettings(ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + useDirectTLS: true, + allowPlainAuth: true, + ),); + final sm = StreamManagementManager(); + conn.registerManagers([ + PresenceManager(), + RosterManager(), + DiscoManager(), + PingManager(), + sm, + CarbonsManager()..forceEnable(), + ]); + conn.registerFeatureNegotiators( + [ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + StreamManagementNegotiator(), + ] + ); + + await conn.connect(); + await Future.delayed(const Duration(seconds: 3)); + expect(fakeSocket.getState(), 6); + expect(await conn.getConnectionState(), XmppConnectionState.connected); + expect( + conn.getManagerById(smManager)!.isStreamManagementEnabled(), + true, + ); + + // Await an iq + await conn.sendStanza( + Stanza.iq( + to: 'user@example.com', + type: 'get', + ), + addFrom: StanzaFromType.none, + ); + + expect(sm.state.s2c, 2); + }); + }); + + group('Stream resumption', () { + test('Stanza retransmission', () async { + var stanzaCount = 0; + final attributes = mkAttributes((_) { + stanzaCount++; + }); + final manager = StreamManagementManager(); + manager.register(attributes); + + await manager.onXmppEvent(StreamManagementEnabledEvent(resource: 'hallo')); + + // Send 5 stanzas + for (var i = 0; i < 5; i++) { + await runOutgoingStanzaHandlers(manager, stanza); + } + + // Only ack 3 + // + await manager.runNonzaHandlers(mkAck(3)); + expect(manager.getUnackedStanzas().length, 2); + + // Lose connection + // [ Reconnect ] + await manager.onXmppEvent(StreamResumedEvent(h: 3)); + + expect(stanzaCount, 2); + }); + test('Resumption with prior state', () async { + var stanzaCount = 0; + final attributes = mkAttributes((_) { + stanzaCount++; + }); + final manager = StreamManagementManager(); + manager.register(attributes); + + // [ ... ] + await manager.onXmppEvent(StreamManagementEnabledEvent(resource: 'hallo')); + manager.setState(manager.state.copyWith(c2s: 150, s2c: 70)); + + // Send some stanzas but don't ack them + for (var i = 0; i < 5; i++) { + await runOutgoingStanzaHandlers(manager, stanza); + } + expect(manager.getUnackedStanzas().length, 5); + + // Lose connection + // [ Reconnect ] + await manager.onXmppEvent(StreamResumedEvent(h: 150)); + expect(manager.getUnackedStanzas().length, 0); + expect(stanzaCount, 5); + }); + }); + + group('Test the negotiator', () { + test('Test successful stream enablement', () async { + final fakeSocket = StubTCPSocket( + play: [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '' + ), + StringExpectation( + "", + ''' + + + + + + + + + + + +''', + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + ignoreId: true + ), + StringExpectation( + "", + '' + ) + ] + ); + + final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + conn.setConnectionSettings(ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + useDirectTLS: true, + allowPlainAuth: true, + ),); + conn.registerManagers([ + PresenceManager(), + RosterManager(), + DiscoManager(), + PingManager(), + StreamManagementManager(), + ]); + conn.registerFeatureNegotiators( + [ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + StreamManagementNegotiator(), + ] + ); + + await conn.connect(); + await Future.delayed(const Duration(seconds: 3)); + + expect(fakeSocket.getState(), 6); + expect(await conn.getConnectionState(), XmppConnectionState.connected); + expect( + conn.getManagerById(smManager)!.isStreamManagementEnabled(), + true, + ); + }); + + test('Test a failed stream resumption', () async { + final fakeSocket = StubTCPSocket( + play: [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '' + ), + StringExpectation( + "", + ''' + + + + + + + + + + + +''', + ), + StringExpectation( + "", + "", + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + ignoreId: true + ), + StringExpectation( + "", + '' + ) + ] + ); + + final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + conn.setConnectionSettings(ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + useDirectTLS: true, + allowPlainAuth: true, + ),); + conn.registerManagers([ + PresenceManager(), + RosterManager(), + DiscoManager(), + PingManager(), + StreamManagementManager(), + ]); + conn.registerFeatureNegotiators( + [ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + StreamManagementNegotiator(), + ] + ); + conn.getManagerById(smManager)! + .setState( + StreamManagementState( + 10, + 10, + streamResumptionId: 'id-1', + ), + ); + + await conn.connect(); + await Future.delayed(const Duration(seconds: 3)); + expect(fakeSocket.getState(), 7); + expect(await conn.getConnectionState(), XmppConnectionState.connected); + expect( + conn + .getManagerById(smManager)! + .isStreamManagementEnabled(), + true, + ); + }); + + test('Test a successful stream resumption', () async { + final fakeSocket = StubTCPSocket( + play: [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '' + ), + StringExpectation( + "", + ''' + + + + + + + + + + + +''', + ), + StringExpectation( + "", + "", + ), + ] + ); + + final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + conn.setConnectionSettings(ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + useDirectTLS: true, + allowPlainAuth: true, + ),); + conn.registerManagers([ + PresenceManager(), + RosterManager(), + DiscoManager(), + PingManager(), + StreamManagementManager(), + ]); + conn.registerFeatureNegotiators( + [ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + StreamManagementNegotiator(), + ] + ); + conn.getManagerById(smManager)! + .setState( + StreamManagementState( + 10, + 10, + streamResumptionId: 'id-1', + ), + ); + + await conn.connect(lastResource: 'abc123'); + await Future.delayed(const Duration(seconds: 3), () async { + expect(fakeSocket.getState(), 5); + expect(await conn.getConnectionState(), XmppConnectionState.connected); + final sm = conn.getManagerById(smManager)!; + expect(sm.isStreamManagementEnabled(), true); + expect(sm.streamResumed, true); + }); + }); + }); +} diff --git a/packages/moxxmpp/test/xeps/xep_0280_test.dart b/packages/moxxmpp/test/xeps/xep_0280_test.dart new file mode 100644 index 0000000..c021db3 --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0280_test.dart @@ -0,0 +1,36 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; +import '../helpers/xmpp.dart'; + +void main() { + test("Test if we're vulnerable against CVE-2020-26547 style vulnerabilities", () async { + final attributes = XmppManagerAttributes( + sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async { + // ignore: avoid_print + print('==> ${stanza.toXml()}'); + return XMLNode(tag: 'iq', attributes: { 'type': 'result' }); + }, + sendNonza: (nonza) {}, + sendEvent: (event) {}, + getManagerById: getManagerNullStub, + getConnectionSettings: () => ConnectionSettings( + jid: JID.fromString('bob@xmpp.example'), + password: 'password', + useDirectTLS: true, + allowPlainAuth: false, + ), + isFeatureSupported: (_) => false, + getFullJID: () => JID.fromString('bob@xmpp.example/uwu'), + getSocket: () => StubTCPSocket(play: []), + getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), + getNegotiatorById: getNegotiatorNullStub, + ); + final manager = CarbonsManager(); + manager.register(attributes); + await manager.enableCarbons(); + + expect(manager.isCarbonValid(JID.fromString('mallory@evil.example')), false); + expect(manager.isCarbonValid(JID.fromString('bob@xmpp.example')), true); + expect(manager.isCarbonValid(JID.fromString('bob@xmpp.example/abc')), false); + }); +} diff --git a/packages/moxxmpp/test/xeps/xep_0352_test.dart b/packages/moxxmpp/test/xeps/xep_0352_test.dart new file mode 100644 index 0000000..24e0a12 --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0352_test.dart @@ -0,0 +1,87 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; +import '../helpers/xmpp.dart'; + +class MockedCSINegotiator extends CSINegotiator { + MockedCSINegotiator(this._isSupported); + + final bool _isSupported; + + @override + bool get isSupported => _isSupported; +} + +T? getSupportedCSINegotiator(String id) { + if (id == csiNegotiator) { + return MockedCSINegotiator(true) as T; + } + + return null; +} + +T? getUnsupportedCSINegotiator(String id) { + if (id == csiNegotiator) { + return MockedCSINegotiator(false) as T; + } + + return null; +} + +void main() { + group('Test the XEP-0352 implementation', () { + test('Test setting the CSI state when CSI is unsupported', () { + var nonzaSent = false; + final csi = CSIManager(); + csi.register(XmppManagerAttributes( + sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'), + sendEvent: (event) {}, + sendNonza: (nonza) { + nonzaSent = true; + }, + getConnectionSettings: () => ConnectionSettings( + jid: JID.fromString('some.user@example.server'), + password: 'password', + useDirectTLS: true, + allowPlainAuth: false, + ), + getManagerById: getManagerNullStub, + getNegotiatorById: getUnsupportedCSINegotiator, + isFeatureSupported: (_) => false, + getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), + getSocket: () => StubTCPSocket(play: []), + getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), + ), + ); + + csi.setActive(); + csi.setInactive(); + + expect(nonzaSent, false, reason: 'Expected that no nonza is sent'); + }); + test('Test setting the CSI state when CSI is supported', () { + final csi = CSIManager(); + csi.register(XmppManagerAttributes( + sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'), + sendEvent: (event) {}, + sendNonza: (nonza) { + expect(nonza.attributes['xmlns'] == csiXmlns, true, reason: "Expected only nonzas with XMLNS '$csiXmlns'"); + }, + getConnectionSettings: () => ConnectionSettings( + jid: JID.fromString('some.user@example.server'), + password: 'password', + useDirectTLS: true, + allowPlainAuth: false, + ), + getManagerById: getManagerNullStub, + getNegotiatorById: getSupportedCSINegotiator, + isFeatureSupported: (_) => false, + getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), + getSocket: () => StubTCPSocket(play: []), + getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), + ),); + + csi.setActive(); + csi.setInactive(); + }); + }); +} diff --git a/packages/moxxmpp/test/xeps/xep_0363_test.dart b/packages/moxxmpp/test/xeps/xep_0363_test.dart new file mode 100644 index 0000000..9330968 --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0363_test.dart @@ -0,0 +1,54 @@ +import 'package:moxxmpp/moxxmpp.dart'; +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', + } + ); + }); + }); +} diff --git a/packages/moxxmpp/test/xeps/xep_0447.dart b/packages/moxxmpp/test/xeps/xep_0447.dart new file mode 100644 index 0000000..4a266c7 --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0447.dart @@ -0,0 +1,33 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; + +void main() { + test('Test correct SFS parsing', () { + final sfs = StatelessFileSharingData.fromXML( + // Taken from https://xmpp.org/extensions/xep-0447.html#file-sharing + XMLNode.fromString(''' + + + image/jpeg + summit.jpg + 3032449 + 4096x2160 + 2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU= + 2AfMGH8O7UNPTvUVAM9aK13mpCY= + Photo from the summit. + + + + + + + + + + '''), + ); + + expect(sfs.metadata.hashes['sha3-256'], '2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU='); + expect(sfs.metadata.hashes['id-blake2b256'], '2AfMGH8O7UNPTvUVAM9aK13mpCY='); + }); +} diff --git a/packages/moxxmpp/test/xmlstreambuffer_test.dart b/packages/moxxmpp/test/xmlstreambuffer_test.dart new file mode 100644 index 0000000..406dd68 --- /dev/null +++ b/packages/moxxmpp/test/xmlstreambuffer_test.dart @@ -0,0 +1,82 @@ +import 'dart:async'; +import 'package:moxxmpp/src/buffer.dart'; +import 'package:test/test.dart'; + +void main() { + test('Test non-broken up Xml data', () async { + var childa = false; + var childb = false; + + final buffer = XmlStreamBuffer(); + final controller = StreamController(); + + controller + .stream + .transform(buffer) + .forEach((node) { + if (node.tag == 'childa') { + childa = true; + } else if (node.tag == 'childb') { + childb = true; + } + }); + controller.add(''); + + await Future.delayed(const Duration(seconds: 2), () { + expect(childa, true); + expect(childb, true); + }); + }); + test('Test broken up Xml data', () async { + var childa = false; + var childb = false; + + final buffer = XmlStreamBuffer(); + final controller = StreamController(); + + controller + .stream + .transform(buffer) + .forEach((node) { + if (node.tag == 'childa') { + childa = true; + } else if (node.tag == 'childb') { + childb = true; + } + }); + controller.add(''); + + await Future.delayed(const Duration(seconds: 2), () { + expect(childa, true); + expect(childb, true); + }); + }); + + test('Test closing the stream', () async { + var childa = false; + var childb = false; + + final buffer = XmlStreamBuffer(); + final controller = StreamController(); + + controller + .stream + .transform(buffer) + .forEach((node) { + if (node.tag == 'childa') { + childa = true; + } else if (node.tag == 'childb') { + childb = true; + } + }); + controller.add(''); + controller.add(''); + + await Future.delayed(const Duration(seconds: 2), () { + expect(childa, true); + expect(childb, true); + }); + }); +} diff --git a/packages/moxxmpp/test/xmpp_test.dart b/packages/moxxmpp/test/xmpp_test.dart new file mode 100644 index 0000000..c18c448 --- /dev/null +++ b/packages/moxxmpp/test/xmpp_test.dart @@ -0,0 +1,354 @@ +import 'dart:async'; +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; +import 'helpers/logging.dart'; +import 'helpers/xmpp.dart'; + +/// Returns true if the roster manager triggeres an event for a given stanza +Future testRosterManager(String bareJid, String resource, String stanzaString) async { + var eventTriggered = false; + final roster = RosterManager(); + roster.register(XmppManagerAttributes( + sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'), + sendEvent: (event) { + eventTriggered = true; + }, + sendNonza: (_) {}, + getConnectionSettings: () => ConnectionSettings( + jid: JID.fromString(bareJid), + password: 'password', + useDirectTLS: true, + allowPlainAuth: false, + ), + getManagerById: getManagerNullStub, + getNegotiatorById: getNegotiatorNullStub, + isFeatureSupported: (_) => false, + getFullJID: () => JID.fromString('$bareJid/$resource'), + getSocket: () => StubTCPSocket(play: []), + getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), + ),); + + final stanza = Stanza.fromXMLNode(XMLNode.fromString(stanzaString)); + for (final handler in roster.getIncomingStanzaHandlers()) { + if (handler.matches(stanza)) await handler.callback(stanza, StanzaHandlerData(false, false, null, stanza)); + } + + return eventTriggered; +} + +void main() { + initLogger(); + + test('Test a successful login attempt with no SM', () async { + final fakeSocket = StubTCPSocket( + play: [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '' + ), + StringExpectation( + "", + ''' + + + + + + + + + + + +''', + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + ignoreId: true, + ), + /* + Expectation( + XMLNode.xmlns( + tag: 'presence', + xmlns: 'jabber:client', + attributes: { 'from': 'polynomdivision@test.server/MU29eEZn' }, + children: [ + XMLNode( + tag: 'show', + text: 'chat', + ), + XMLNode.xmlns( + tag: 'c', + xmlns: 'http://jabber.org/protocol/caps', + attributes: { + // TODO: Somehow make the test ignore this attribute + 'ver': 'QRTBC5cg/oYd+UOTYazSQR4zb/I=', + 'node': 'http://moxxy.im', + 'hash': 'sha-1' + }, + ) + ], + ), + XMLNode( + tag: 'presence', + ), + ), + */ + ], + ); + // TODO: This test is broken since we query the server and enable carbons + final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + conn.setConnectionSettings(ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + useDirectTLS: true, + allowPlainAuth: true, + ),); + conn.registerManagers([ + PresenceManager(), + RosterManager(), + DiscoManager(), + PingManager(), + StreamManagementManager(), + ]); + conn.registerFeatureNegotiators( + [ + SaslPlainNegotiator(), + SaslScramNegotiator(10, '', '', ScramHashType.sha512), + ResourceBindingNegotiator(), + StreamManagementNegotiator(), + ] + ); + + await conn.connect(); + await Future.delayed(const Duration(seconds: 3), () { + expect(fakeSocket.getState(), /*6*/ 5); + }); + }); + + test('Test a failed SASL auth', () async { + final fakeSocket = StubTCPSocket( + play: [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '' + ), + ], + ); + var receivedEvent = false; + final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + conn.setConnectionSettings(ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + useDirectTLS: true, + allowPlainAuth: true, + ),); + conn.registerManagers([ + PresenceManager(), + RosterManager(), + DiscoManager(), + PingManager(), + ]); + conn.registerFeatureNegotiators([ + SaslPlainNegotiator() + ]); + + conn.asBroadcastStream().listen((event) { + if (event is AuthenticationFailedEvent && event.saslError == 'not-authorized') { + receivedEvent = true; + } + }); + + await conn.connect(); + await Future.delayed(const Duration(seconds: 3), () { + expect(receivedEvent, true); + }); + }); + + test('Test another failed SASL auth', () async { + final fakeSocket = StubTCPSocket( + play: [ + StringExpectation( + "", + ''' + + + + PLAIN + + ''', + ), + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + '', + ), + ], + ); + var receivedEvent = false; + final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + conn.setConnectionSettings(ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + useDirectTLS: true, + allowPlainAuth: true, + ),); + conn.registerManagers([ + PresenceManager(), + RosterManager(), + DiscoManager(), + PingManager(), + ]); + conn.registerFeatureNegotiators([ + SaslPlainNegotiator() + ]); + + conn.asBroadcastStream().listen((event) { + if (event is AuthenticationFailedEvent && event.saslError == 'mechanism-too-weak') { + receivedEvent = true; + } + }); + + await conn.connect(); + await Future.delayed(const Duration(seconds: 3), () { + expect(receivedEvent, true); + }); + }); + + /*test('Test choosing SCRAM-SHA-1', () async { + final fakeSocket = StubTCPSocket( + play: [ + StringExpectation( + "", + ''' + + + + PLAIN + SCRAM-SHA-1 + + ''', + ), + // TODO(Unknown): This test is currently broken + StringExpectation( + "AHBvbHlub21kaXZpc2lvbgBhYWFh", + "..." + ) + ], + ); + final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); + conn.setConnectionSettings(ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + useDirectTLS: true, + allowPlainAuth: false, + ),); + conn.registerManagers([ + PresenceManager(), + RosterManager(), + DiscoManager(), + PingManager(), + ]); + conn.registerFeatureNegotiators([ + SaslPlainNegotiator(), + SaslScramNegotiator(10, '', '', ScramHashType.sha1), + ]); + + await conn.connect(); + await Future.delayed(const Duration(seconds: 3), () { + expect(fakeSocket.getState(), 2); + }); + });*/ + + group('Test roster pushes', () { + test('Test for a CVE-2015-8688 style vulnerability', () async { + var eventTriggered = false; + final roster = RosterManager(); + roster.register(XmppManagerAttributes( + sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'), + sendEvent: (event) { + eventTriggered = true; + }, + sendNonza: (_) {}, + getConnectionSettings: () => ConnectionSettings( + jid: JID.fromString('some.user@example.server'), + password: 'password', + useDirectTLS: true, + allowPlainAuth: false, + ), + getManagerById: getManagerNullStub, + getNegotiatorById: getNegotiatorNullStub, + isFeatureSupported: (_) => false, + getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), + getSocket: () => StubTCPSocket(play: []), + getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), + ),); + + // NOTE: Based on https://gultsch.de/gajim_roster_push_and_message_interception.html + // NOTE: Added a from attribute as a server would add it itself. + final maliciousStanza = Stanza.fromXMLNode(XMLNode.fromString("")); + + for (final handler in roster.getIncomingStanzaHandlers()) { + if (handler.matches(maliciousStanza)) await handler.callback(maliciousStanza, StanzaHandlerData(false, false, null, maliciousStanza)); + } + + expect(eventTriggered, false, reason: 'Was able to inject a malicious roster push'); + }); + test('The manager should accept pushes from our bare jid', () async { + final result = await testRosterManager('test.user@server.example', 'aaaaa', ""); + expect(result, true, reason: 'Roster pushes from our bare JID should be accepted'); + }); + test('The manager should accept pushes from a jid that, if the resource is stripped, is our bare jid', () async { + final result1 = await testRosterManager('test.user@server.example', 'aaaaa', ""); + expect(result1, true, reason: 'Roster pushes should be accepted if the bare JIDs are the same'); + + final result2 = await testRosterManager('test.user@server.example', 'aaaaa', ""); + expect(result2, true, reason: 'Roster pushes should be accepted if the bare JIDs are the same'); + }); + }); +}