feat: Add a management class for roster state

This commit is contained in:
PapaTutuWawa 2023-01-07 15:51:10 +01:00
parent 763c93857d
commit e2c8f79429
5 changed files with 239 additions and 324 deletions

View File

@ -29,6 +29,7 @@ export 'package:moxxmpp/src/rfcs/rfc_2782.dart';
export 'package:moxxmpp/src/rfcs/rfc_4790.dart';
export 'package:moxxmpp/src/roster/errors.dart';
export 'package:moxxmpp/src/roster/roster.dart';
export 'package:moxxmpp/src/roster/state.dart';
export 'package:moxxmpp/src/settings.dart';
export 'package:moxxmpp/src/socket.dart';
export 'package:moxxmpp/src/stanza.dart';

View File

@ -65,7 +65,6 @@ class RosterFeatureNegotiator extends XmppFeatureNegotiatorBase {
/// This manager requires a RosterFeatureNegotiator to be registered.
class RosterManager extends XmppManagerBase {
RosterManager() : _rosterVersion = null, super();
String? _rosterVersion;
@ -88,8 +87,10 @@ class RosterManager extends XmppManagerBase {
@override
Future<bool> isSupported() async => true;
/// Override-able functions
/// Commit the current roster to storage.
Future<void> commitLastRosterVersion(String version) async {}
/// Load the last roster data
Future<void> loadLastRosterVersion() async {}
void setRosterVersion(String ver) {

View File

@ -0,0 +1,142 @@
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/roster/roster.dart';
class _RosterProcessTriple {
const _RosterProcessTriple(this.removed, this.modified, this.added);
final String? removed;
final XmppRosterItem? modified;
final XmppRosterItem? added;
}
class RosterCacheLoadResult {
const RosterCacheLoadResult(this.version, this.roster);
final String? version;
final List<XmppRosterItem> roster;
}
abstract class BaseRosterStateManager {
List<XmppRosterItem>? currentRoster;
String? currentVersion;
Future<RosterCacheLoadResult> loadRosterCache();
Future<void> commitRoster(String? version, List<String> removed, List<XmppRosterItem> modified, List<XmppRosterItem> added);
Future<void> _loadRosterCache() async {
if (currentRoster == null) {
final result = await loadRosterCache();
currentRoster = result.roster;
currentVersion = result.version;
}
}
_RosterProcessTriple _handleRosterItem(XmppRosterItem item) {
if (item.subscription == 'remove') {
// The item has been removed
currentRoster!.removeWhere((i) => i.jid == item.jid);
return _RosterProcessTriple(
item.jid,
null,
null,
);
}
final index = currentRoster!.indexWhere((i) => i.jid == item.jid);
if (index == -1) {
// The item does not exist
currentRoster!.add(item);
return _RosterProcessTriple(
null,
null,
item,
);
} else {
// The item is updated
currentRoster![index] = item;
return _RosterProcessTriple(
null,
item,
null,
);
}
}
Future<void> handleRosterPush(RosterPushEvent event) async {
await _loadRosterCache();
currentVersion = event.ver;
final result = _handleRosterItem(event.item);
if (result.removed != null) {
return commitRoster(
currentVersion,
[result.removed!],
[],
[],
);
} else if (result.modified != null) {
return commitRoster(
currentVersion,
[],
[result.modified!],
[],
);
} else if (result.added != null) {
return commitRoster(
currentVersion,
[],
[],
[result.added!],
);
}
}
Future<void> handleRosterFetch(RosterRequestResult result) async {
final removed = List<String>.empty(growable: true);
final modified = List<XmppRosterItem>.empty(growable: true);
final added = List<XmppRosterItem>.empty(growable: true);
await _loadRosterCache();
currentVersion = result.ver;
for (final item in result.items) {
final result = _handleRosterItem(item);
if (result.removed != null) removed.add(result.removed!);
if (result.modified != null) modified.add(result.modified!);
if (result.added != null) added.add(result.added!);
}
await commitRoster(
currentVersion,
removed,
modified,
added,
);
}
}
@visibleForTesting
class TestingRosterStateManager extends BaseRosterStateManager {
TestingRosterStateManager(
this.initialRosterVersion,
this.initialRoster,
);
final String? initialRosterVersion;
final List<XmppRosterItem> initialRoster;
int loadCount = 0;
@override
Future<RosterCacheLoadResult> loadRosterCache() async {
loadCount++;
return RosterCacheLoadResult(
initialRosterVersion,
initialRoster,
);
}
@override
Future<void> commitRoster(String? version, List<String> removed, List<XmppRosterItem> modified, List<XmppRosterItem> added) async {}
}

View File

@ -0,0 +1,93 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
void main() {
test('Test receiving a roster push', () async {
final rs = TestingRosterStateManager(null, []);
await rs.handleRosterPush(
RosterPushEvent(
item: XmppRosterItem(
jid: 'testuser@server.example',
subscription: 'both',
),
),
);
expect(
rs.currentRoster!.indexWhere((item) => item.jid == 'testuser@server.example') != -1,
true,
);
expect(rs.loadCount, 1);
expect(rs.currentRoster!.length, 1);
// Receive another roster push
await rs.handleRosterPush(
RosterPushEvent(
item: XmppRosterItem(
jid: 'testuser2@server2.example',
subscription: 'to',
),
),
);
expect(
rs.currentRoster!.indexWhere((item) => item.jid == 'testuser2@server2.example') != -1,
true,
);
expect(rs.loadCount, 1);
expect(rs.currentRoster!.length, 2);
// Remove one of the items
await rs.handleRosterPush(
RosterPushEvent(
item: XmppRosterItem(
jid: 'testuser2@server2.example',
subscription: 'remove',
),
),
);
expect(
rs.currentRoster!.indexWhere((item) => item.jid == 'testuser2@server2.example') == -1,
true,
);
expect(
rs.currentRoster!.indexWhere((item) => item.jid == 'testuser@server.example') != 1,
true,
);
expect(rs.loadCount, 1);
expect(rs.currentRoster!.length, 1);
});
test('Test a roster fetch', () async {
final rs = TestingRosterStateManager(null, []);
// Fetch the roster
await rs.handleRosterFetch(
RosterRequestResult(
items: [
XmppRosterItem(
jid: 'testuser@server.example',
subscription: 'both',
),
XmppRosterItem(
jid: 'testuser2@server2.example',
subscription: 'to',
),
XmppRosterItem(
jid: 'testuser3@server3.example',
subscription: 'from',
),
],
ver: 'aaaaaaaa',
),
);
expect(rs.loadCount, 1);
expect(rs.currentRoster!.length, 3);
expect(rs.currentRoster!.indexWhere((item) => item.jid == 'testuser@server.example') != -1, true);
expect(rs.currentRoster!.indexWhere((item) => item.jid == 'testuser2@server2.example') != -1, true);
expect(rs.currentRoster!.indexWhere((item) => item.jid == 'testuser3@server3.example') != -1, true);
});
}

View File

@ -1,322 +0,0 @@
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);
});
});
}