tests: Add back tests

This commit is contained in:
PapaTutuWawa 2022-11-08 20:44:41 +01:00
parent 5dc4286e74
commit c850924dd3
20 changed files with 2480 additions and 16 deletions

View File

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

View File

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

View File

@ -0,0 +1,137 @@
import 'dart:async';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import 'xml.dart';
T? getNegotiatorNullStub<T extends XmppFeatureNegotiatorBase>(String id) {
return null;
}
T? getManagerNullStub<T extends XmppManagerBase>(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<ExpectationBase> play })
: _play = play,
_dataStream = StreamController<String>.broadcast(),
_eventStream = StreamController<XmppSocketEvent>.broadcast();
int _state = 0;
final StreamController<String> _dataStream;
final StreamController<XmppSocketEvent> _eventStream;
final List<ExpectationBase> _play;
String? lastId;
@override
bool isSecure() => true;
@override
Future<bool> secure(String domain) async => true;
@override
Future<bool> connect(String domain, { String? host, int? port }) async => true;
@override
Stream<String> getDataStream() => _dataStream.stream.asBroadcastStream();
@override
Stream<XmppSocketEvent> 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("<?xml version='1.0'?>")) {
str = str.substring(21);
}
if (str.endsWith('</stream:stream>')) {
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;
}

View File

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

View File

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

View File

@ -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<void> 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<void> negotiate(XMLNode nonza) async {
called = true;
state = NegotiatorState.done;
}
}
void main() {
initLogger();
final stubSocket = StubTCPSocket(
play: [
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<example1 xmlns="im:moxxy:example1" />
<example2 xmlns="im:moxxy:example2" />
</stream:features>''',
),
],
);
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<StubNegotiator1>(exampleNamespace1);
final negotiator2 = connection.getNegotiatorById<StubNegotiator2>(exampleNamespace2);
expect(negotiator1?.called, true);
expect(negotiator2?.called, true);
});
});
}

View File

@ -0,0 +1,322 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
// TODO(PapaTutuWawa): Fix tests
typedef AddRosterItemFunction = Future<RosterItem> Function(
String avatarUrl,
String avatarHash,
String jid,
String title,
String subscription,
String ask,
{
List<String> groups,
}
);
typedef UpdateRosterItemFunction = Future<RosterItem> Function(
int id, {
String? avatarUrl,
String? avatarHash,
String? title,
String? subscription,
String? ask,
List<String>? groups,
}
);
AddRosterItemFunction mkAddRosterItem(void Function(String) callback) {
return (
String avatarUrl,
String avatarHash,
String jid,
String title,
String subscription,
String ask,
{
List<String> groups = const [],
}
) async {
callback(jid);
return await addRosterItemFromData(
avatarUrl,
avatarHash,
jid,
title,
subscription,
ask,
groups: groups,
);
};
}
Future<RosterItem> addRosterItemFromData(
String avatarUrl,
String avatarHash,
String jid,
String title,
String subscription,
String ask,
{
List<String> groups = const [],
}
) async => RosterItem(
0,
avatarUrl,
avatarHash,
jid,
title,
subscription,
ask,
groups,
);
UpdateRosterItemFunction mkRosterUpdate(List<RosterItem> roster) {
return (
int id, {
String? avatarUrl,
String? avatarHash,
String? title,
String? subscription,
String? ask,
List<String>? 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);
});
});
}

View File

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

View File

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

View File

@ -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(), "<iq xmlns='uwu' />");
expect(XMLNode.xmlns(tag: 'iq', xmlns: 'uwu', attributes: {'how': 'uwu'}).toXml(), "<iq xmlns='uwu' how='uwu' />");
expect(stanza.toXml(), "<uwu-meter xmlns='uwu'><uwu strength=10 /></uwu-meter>");
expect(StreamHeaderNonza('uwu.server').toXml(), "<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='uwu.server' xml:lang='en'>");
expect(XMLNode(tag: 'text', attributes: {}, text: 'hallo').toXml(), '<text>hallo</text>');
expect(XMLNode(tag: 'text', attributes: { 'world': 'no' }, text: 'hallo').toXml(), "<text world='no'>hallo</text>");
expect(XMLNode(tag: 'text', attributes: {}, text: 'hallo').toXml(), '<text>hallo</text>');
expect(XMLNode(tag: 'text', attributes: {}, text: 'test').innerText(), 'test');
});
test('Test XmlElement', () {
expect(XMLNode.fromXmlElement(XmlDocument.parse("<root owo='uwu' />").firstElementChild!).toXml(), "<root owo='uwu' />");
});
test('Test the find functions', () {
final node1 = XMLNode.fromString('<message><a xmlns="a" /><body>Hallo</body></message>');
expect(compareXMLNodes(node1.firstTag('body')!, XMLNode.fromString('<body>Hallo</body>')), true);
expect(compareXMLNodes(node1.firstTagByXmlns('a')!, XMLNode.fromString('<a xmlns="a" />')), true);
});
}

View File

@ -0,0 +1,16 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
void main() {
test('Parsing', () {
const testData = "<x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>urn:xmpp:dataforms:softwareinfo</value></field><field var='ip_version' type='text-multi' ><value>ipv4</value><value>ipv6</value></field><field var='os'><value>Mac</value></field><field var='os_version'><value>10.5.1</value></field><field var='software'><value>Psi</value></field><field var='software_version'><value>0.11</value></field></x>";
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');
});
}

View File

@ -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(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />'
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true,
),
StringExpectation(
"<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://moxxy.im' ver='QRTBC5cg/oYd+UOTYazSQR4zb/I=' /></presence>",
'',
),
StanzaExpectation(
"<iq type='get' id='ec325efc-9924-4c48-93f8-ed34a2b0e5fc' to='romeo@montague.lit/orchard' from='polynomdivision@test.server/MU29eEZn' xmlns='jabber:client'><query xmlns='http://jabber.org/protocol/disco#info' /></iq>",
'',
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>(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("<iq type='result' id='${fakeSocket.lastId!}' from='romeo@montague.lit/orchard' to='polynomdivision@test.server/MU29eEZn' xmlns='jabber:client'><query xmlns='http://jabber.org/protocol/disco#info' /></iq>");
await Future.delayed(const Duration(seconds: 2));
expect(fakeSocket.getState(), 6);
expect(await result1, await result2);
expect(disco.hasInfoQueriesRunning(), false);
});
}

View File

@ -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 = "<x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>urn:xmpp:dataforms:softwareinfo</value></field><field var='ip_version' type='text-multi' ><value>ipv4</value><value>ipv6</value></field><field var='os'><value>Mac</value></field><field var='os_version'><value>10.5.1</value></field><field var='software'><value>Psi</value></field><field var='software_version'><value>0.11</value></field></x>";
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=');
});
}

View File

@ -0,0 +1,737 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import '../helpers/logging.dart';
import '../helpers/xmpp.dart';
Future<void> 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<void> 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);
// [...]
// <enable /> // <enabled />
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);
}
// <a h='5'/>
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);
}
// <a h='3'/>
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);
}
// <a h='3'/>
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);
}
// <a h='3'/>
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(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />'
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true,
),
StringExpectation(
"<enable xmlns='urn:xmpp:sm:3' resume='true' />",
'<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="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<StreamManagementManager>(smManager)!.isStreamManagementEnabled(),
true,
);
// Send an invalid carbon
fakeSocket.injectRawXml('''
<message xmlns='jabber:client'
from='romeo@montague.example'
to='romeo@montague.example/home'
type='chat'>
<received xmlns='urn:xmpp:carbons:2'>
<forwarded xmlns='urn:xmpp:forward:0'>
<message xmlns='jabber:client'
from='juliet@capulet.example/balcony'
to='romeo@montague.example/garden'
type='chat'>
<body>What man art thou that, thus bescreen'd in night, so stumblest on my counsel?</body>
<thread>0e3141cd80894871a68e6fe6b1ec56fa</thread>
</message>
</forwarded>
</received>
</message>
''');
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(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />'
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true,
),
StringExpectation(
"<enable xmlns='urn:xmpp:sm:3' resume='true' />",
'<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true" />',
),
StringExpectation(
"<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://moxxy.im' ver='QRTBC5cg/oYd+UOTYazSQR4zb/I=' /></presence>",
'<iq type="result" />',
),
StanzaExpectation(
"<iq to='user@example.com' type='get' id='a' xmlns='jabber:client' />",
"<iq to='user@example.com' type='result' id='a' />",
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<StreamManagementManager>(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
// <a h='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(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />'
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true
),
StringExpectation(
"<enable xmlns='urn:xmpp:sm:3' resume='true' />",
'<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true" />'
)
]
);
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<StreamManagementManager>(smManager)!.isStreamManagementEnabled(),
true,
);
});
test('Test a failed stream resumption', () async {
final fakeSocket = StubTCPSocket(
play: [
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />'
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StringExpectation(
"<resume xmlns='urn:xmpp:sm:3' previd='id-1' h='10' />",
"<failed xmlns='urn:xmpp:sm:3' h='another-sequence-number'><item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></failed>",
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true
),
StringExpectation(
"<enable xmlns='urn:xmpp:sm:3' resume='true' />",
'<enabled xmlns="urn:xmpp:sm:3" id="id-2" resume="true" />'
)
]
);
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<StreamManagementManager>(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<StreamManagementManager>(smManager)!
.isStreamManagementEnabled(),
true,
);
});
test('Test a successful stream resumption', () async {
final fakeSocket = StubTCPSocket(
play: [
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />'
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StringExpectation(
"<resume xmlns='urn:xmpp:sm:3' previd='id-1' h='10' />",
"<resumed xmlns='urn:xmpp:sm:3' h='id-1' h='12' />",
),
]
);
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<StreamManagementManager>(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<StreamManagementManager>(smManager)!;
expect(sm.isStreamManagementEnabled(), true);
expect(sm.streamResumed, true);
});
});
});
}

View File

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

View File

@ -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<T extends XmppFeatureNegotiatorBase>(String id) {
if (id == csiNegotiator) {
return MockedCSINegotiator(true) as T;
}
return null;
}
T? getUnsupportedCSINegotiator<T extends XmppFeatureNegotiatorBase>(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();
});
});
}

View File

@ -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',
}
);
});
});
}

View File

@ -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('''
<file-sharing xmlns='urn:xmpp:sfs:0' disposition='inline'>
<file xmlns='urn:xmpp:file:metadata:0'>
<media-type>image/jpeg</media-type>
<name>summit.jpg</name>
<size>3032449</size>
<dimensions>4096x2160</dimensions>
<hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=</hash>
<hash xmlns='urn:xmpp:hashes:2' algo='id-blake2b256'>2AfMGH8O7UNPTvUVAM9aK13mpCY=</hash>
<desc>Photo from the summit.</desc>
<thumbnail xmlns='urn:xmpp:thumbs:1' uri='cid:sha1+ffd7c8d28e9c5e82afea41f97108c6b4@bob.xmpp.org' media-type='image/png' width='128' height='96'/>
</file>
<sources>
<url-data xmlns='http://jabber.org/protocol/url-data' target='https://download.montague.lit/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/summit.jpg' />
<jinglepub xmlns='urn:xmpp:jinglepub:1' from='romeo@montague.lit/resource' id='9559976B-3FBF-4E7E-B457-2DAA225972BB'>
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5' />
</jinglepub>
</sources>
</file-sharing>
'''),
);
expect(sfs.metadata.hashes['sha3-256'], '2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=');
expect(sfs.metadata.hashes['id-blake2b256'], '2AfMGH8O7UNPTvUVAM9aK13mpCY=');
});
}

View File

@ -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<String>();
controller
.stream
.transform(buffer)
.forEach((node) {
if (node.tag == 'childa') {
childa = true;
} else if (node.tag == 'childb') {
childb = true;
}
});
controller.add('<childa /><childb />');
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<String>();
controller
.stream
.transform(buffer)
.forEach((node) {
if (node.tag == 'childa') {
childa = true;
} else if (node.tag == 'childb') {
childb = true;
}
});
controller.add('<childa');
controller.add(' /><childb />');
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<String>();
controller
.stream
.transform(buffer)
.forEach((node) {
if (node.tag == 'childa') {
childa = true;
} else if (node.tag == 'childb') {
childb = true;
}
});
controller.add('<childa');
controller.add(' /><childb />');
controller.add('</stream:stream>');
await Future.delayed(const Duration(seconds: 2), () {
expect(childa, true);
expect(childb, true);
});
});
}

View File

@ -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<bool> 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(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />'
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
<optional/>
</session>
<csi xmlns="urn:xmpp:csi:0"/>
<sm xmlns="urn:xmpp:sm:3"/>
</stream:features>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
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(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><not-authorized /></failure>'
),
],
);
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(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>''',
),
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
'<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism-too-weak /></failure>',
),
],
);
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(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
<mechanism>SCRAM-SHA-1</mechanism>
</mechanisms>
</stream:features>''',
),
// TODO(Unknown): This test is currently broken
StringExpectation(
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='SCRAM-SHA-1'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
"..."
)
],
);
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("<iq type=\"set\" from=\"eve@siacs.eu/bbbbb\" to=\"some.user@example.server/aaaaa\"><query xmlns='jabber:iq:roster'><item subscription=\"both\" jid=\"eve@siacs.eu\" name=\"Bob\" /></query></iq>"));
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', "<iq from='test.user@server.example' type='result' id='82c2aa1e-cac3-4f62-9e1f-bbe6b057daf3' to='test.user@server.example/aaaaa' xmlns='jabber:client'><query ver='64' xmlns='jabber:iq:roster'><item jid='some.other.user@server.example' subscription='to' /></query></iq>");
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', "<iq from='test.user@server.example/aaaaa' type='result' id='82c2aa1e-cac3-4f62-9e1f-bbe6b057daf3' to='test.user@server.example/aaaaa' xmlns='jabber:client'><query ver='64' xmlns='jabber:iq:roster'><item jid='some.other.user@server.example' subscription='to' /></query></iq>");
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', "<iq from='test.user@server.example/bbbbb' type='result' id='82c2aa1e-cac3-4f62-9e1f-bbe6b057daf3' to='test.user@server.example/aaaaa' xmlns='jabber:client'><query ver='64' xmlns='jabber:iq:roster'><item jid='some.other.user@server.example' subscription='to' /></query></iq>");
expect(result2, true, reason: 'Roster pushes should be accepted if the bare JIDs are the same');
});
});
}