implemented dual Nextcloud push registration with separate general and talk apptypes to ensure reliable Talk notification delivery; introduced stacked MessagingStyle notifications for chat threads with support for circular conversation avatars and disk caching
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/push/chat_thread_store.dart';
|
||||
|
||||
ThreadMessage _msg(int nid, {String sender = 'Max', String text = 'hi'}) =>
|
||||
ThreadMessage(nid: nid, sender: sender, text: text, timestampMs: nid);
|
||||
|
||||
void main() {
|
||||
group('appendThreadMessage', () {
|
||||
test('appends in order', () {
|
||||
var messages = <ThreadMessage>[];
|
||||
messages = appendThreadMessage(messages, _msg(1));
|
||||
messages = appendThreadMessage(messages, _msg(2));
|
||||
expect(messages.map((m) => m.nid), [1, 2]);
|
||||
});
|
||||
|
||||
test('caps the history, dropping the oldest', () {
|
||||
var messages = <ThreadMessage>[];
|
||||
for (var nid = 1; nid <= kChatThreadCap + 3; nid++) {
|
||||
messages = appendThreadMessage(messages, _msg(nid));
|
||||
}
|
||||
expect(messages, hasLength(kChatThreadCap));
|
||||
expect(messages.first.nid, 4);
|
||||
expect(messages.last.nid, kChatThreadCap + 3);
|
||||
});
|
||||
|
||||
test('a redelivered nid replaces the old entry instead of duplicating', () {
|
||||
var messages = [_msg(1), _msg(2)];
|
||||
messages = appendThreadMessage(messages, _msg(1, text: 'edited'));
|
||||
expect(messages.map((m) => m.nid), [2, 1]);
|
||||
expect(messages.last.text, 'edited');
|
||||
});
|
||||
});
|
||||
|
||||
group('threadAfterIncoming (dismiss/read reset)', () {
|
||||
final existing = [_msg(1), _msg(2)];
|
||||
final incoming = _msg(3);
|
||||
|
||||
test('active notification → new message stacks onto history', () {
|
||||
final result = threadAfterIncoming(existing, incoming, true);
|
||||
expect(result.map((m) => m.nid), [1, 2, 3]);
|
||||
});
|
||||
|
||||
test('inactive notification → thread restarts with only the new one', () {
|
||||
final result = threadAfterIncoming(existing, incoming, false);
|
||||
expect(result.map((m) => m.nid), [3]);
|
||||
});
|
||||
|
||||
test('unknown active state (probe failed) → stacks defensively', () {
|
||||
final result = threadAfterIncoming(existing, incoming, null);
|
||||
expect(result.map((m) => m.nid), [1, 2, 3]);
|
||||
});
|
||||
|
||||
test('reset still honours the cap for a burst of messages', () {
|
||||
final result = threadAfterIncoming(existing, incoming, false, cap: 1);
|
||||
expect(result.map((m) => m.nid), [3]);
|
||||
});
|
||||
});
|
||||
|
||||
group('removeThreadNid', () {
|
||||
test('removes only the matching message', () {
|
||||
final remaining = removeThreadNid([_msg(1), _msg(2), _msg(3)], 2);
|
||||
expect(remaining.map((m) => m.nid), [1, 3]);
|
||||
});
|
||||
|
||||
test('deleting the last message empties the thread (caller cancels)', () {
|
||||
final remaining = removeThreadNid([_msg(7)], 7);
|
||||
expect(remaining, isEmpty);
|
||||
});
|
||||
|
||||
test('unknown nid leaves the thread untouched', () {
|
||||
final remaining = removeThreadNid([_msg(1)], 99);
|
||||
expect(remaining.map((m) => m.nid), [1]);
|
||||
});
|
||||
});
|
||||
|
||||
group('conversationHeader', () {
|
||||
test('1:1 chat (single sender, no room) gets no title', () {
|
||||
final header = conversationHeader([
|
||||
_msg(1, sender: 'Max'),
|
||||
_msg(2, sender: 'Max'),
|
||||
]);
|
||||
expect(header.conversationTitle, isNull);
|
||||
expect(header.groupConversation, isFalse);
|
||||
});
|
||||
|
||||
test('a known room name becomes the title exactly once', () {
|
||||
final header = conversationHeader([
|
||||
ThreadMessage(
|
||||
nid: 1,
|
||||
sender: 'Max',
|
||||
text: 'hi',
|
||||
timestampMs: 1,
|
||||
roomName: 'Projektraum',
|
||||
),
|
||||
_msg(2, sender: 'Max'),
|
||||
]);
|
||||
expect(header.conversationTitle, 'Projektraum');
|
||||
expect(header.groupConversation, isTrue);
|
||||
});
|
||||
|
||||
test('multiple senders without room name fall back to last sender', () {
|
||||
final header = conversationHeader([
|
||||
_msg(1, sender: 'Max'),
|
||||
_msg(2, sender: 'Anna'),
|
||||
]);
|
||||
expect(header.conversationTitle, 'Anna');
|
||||
expect(header.groupConversation, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('ThreadMessage roomName migration', () {
|
||||
test('entries without roomName field read as null', () {
|
||||
final restored = ThreadMessage.fromJson({
|
||||
'nid': 1,
|
||||
'sender': 'Max',
|
||||
'text': 'hi',
|
||||
'timestampMs': 5,
|
||||
});
|
||||
expect(restored.roomName, isNull);
|
||||
});
|
||||
|
||||
test('roomName round-trips when present', () {
|
||||
final restored = ThreadMessage.fromJson(
|
||||
const ThreadMessage(
|
||||
nid: 1,
|
||||
sender: 'Max',
|
||||
text: 'hi',
|
||||
timestampMs: 5,
|
||||
roomName: 'Raum',
|
||||
).toJson(),
|
||||
);
|
||||
expect(restored.roomName, 'Raum');
|
||||
});
|
||||
});
|
||||
|
||||
group('stable notification identity', () {
|
||||
test('same token always yields the same 31-bit id', () {
|
||||
final a = stableChatNotificationId('abc123');
|
||||
expect(a, stableChatNotificationId('abc123'));
|
||||
expect(a, greaterThanOrEqualTo(0));
|
||||
expect(a, lessThanOrEqualTo(0x7fffffff));
|
||||
});
|
||||
|
||||
test('different tokens yield different ids and tags', () {
|
||||
expect(
|
||||
stableChatNotificationId('abc123'),
|
||||
isNot(stableChatNotificationId('xyz789')),
|
||||
);
|
||||
expect(chatNotificationTag('abc123'), 'talk_abc123');
|
||||
});
|
||||
});
|
||||
|
||||
group('ThreadMessage json', () {
|
||||
test('round-trips', () {
|
||||
final restored = ThreadMessage.fromJson(
|
||||
ThreadMessage(
|
||||
nid: 5,
|
||||
sender: 'Max',
|
||||
text: 'Hallo',
|
||||
timestampMs: 1234,
|
||||
).toJson(),
|
||||
);
|
||||
expect(restored.nid, 5);
|
||||
expect(restored.sender, 'Max');
|
||||
expect(restored.text, 'Hallo');
|
||||
expect(restored.timestampMs, 1234);
|
||||
});
|
||||
});
|
||||
|
||||
group('ChatThreadStore.docIdForToken', () {
|
||||
test('url-safe tokens are used verbatim, others encoded', () {
|
||||
expect(ChatThreadStore.docIdForToken('abc_1-2'), 'abc_1-2');
|
||||
final encoded = ChatThreadStore.docIdForToken('a/b');
|
||||
expect(encoded, isNot(contains('/')));
|
||||
expect(encoded, ChatThreadStore.docIdForToken('a/b'));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user