351 lines
10 KiB
Dart
351 lines
10 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:votianlt_app/services/message_handler.dart';
|
|
|
|
void main() {
|
|
group('MessageHandler', () {
|
|
late MessageHandler handler;
|
|
late List<String> ackCallbackCalls;
|
|
|
|
setUp(() {
|
|
ackCallbackCalls = [];
|
|
handler = MessageHandler(
|
|
maxProcessedIds: 100,
|
|
onAckRequired: (messageId) => ackCallbackCalls.add(messageId),
|
|
);
|
|
});
|
|
|
|
group('isEnvelopeMessage', () {
|
|
test('returns true for valid envelope with all required fields', () {
|
|
final data = {
|
|
'messageId': 'test-id',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/server/user/message',
|
|
'payload': {'key': 'value'},
|
|
};
|
|
|
|
expect(handler.isEnvelopeMessage(data), true);
|
|
});
|
|
|
|
test('returns false when messageId is missing', () {
|
|
final data = {
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/server/user/message',
|
|
'payload': {'key': 'value'},
|
|
};
|
|
|
|
expect(handler.isEnvelopeMessage(data), false);
|
|
});
|
|
|
|
test('returns false when timestamp is missing', () {
|
|
final data = {
|
|
'messageId': 'test-id',
|
|
'topic': '/server/user/message',
|
|
'payload': {'key': 'value'},
|
|
};
|
|
|
|
expect(handler.isEnvelopeMessage(data), false);
|
|
});
|
|
|
|
test('returns false when topic is missing', () {
|
|
final data = {
|
|
'messageId': 'test-id',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'payload': {'key': 'value'},
|
|
};
|
|
|
|
expect(handler.isEnvelopeMessage(data), false);
|
|
});
|
|
|
|
test('returns false when payload is missing', () {
|
|
final data = {
|
|
'messageId': 'test-id',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/server/user/message',
|
|
};
|
|
|
|
expect(handler.isEnvelopeMessage(data), false);
|
|
});
|
|
|
|
test('returns false for non-map data', () {
|
|
expect(handler.isEnvelopeMessage('string'), false);
|
|
expect(handler.isEnvelopeMessage(123), false);
|
|
expect(handler.isEnvelopeMessage(['list']), false);
|
|
expect(handler.isEnvelopeMessage(null), false);
|
|
});
|
|
|
|
test('returns true even if payload is null', () {
|
|
final data = {
|
|
'messageId': 'test-id',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/server/user/message',
|
|
'payload': null,
|
|
};
|
|
|
|
expect(handler.isEnvelopeMessage(data), true);
|
|
});
|
|
});
|
|
|
|
group('unwrapEnvelope', () {
|
|
test('extracts payload from valid envelope', () {
|
|
final data = {
|
|
'messageId': 'test-id-123',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/server/user/message',
|
|
'payload': {'content': 'Hello'},
|
|
'requiresAck': true,
|
|
};
|
|
|
|
final result = handler.unwrapEnvelope(data);
|
|
|
|
expect(result, isNotNull);
|
|
expect(result!.payload, {'content': 'Hello'});
|
|
expect(result.messageId, 'test-id-123');
|
|
expect(result.requiresAck, true);
|
|
});
|
|
|
|
test('returns non-envelope data as-is', () {
|
|
final plainData = {'content': 'Hello', 'sender': 'user1'};
|
|
|
|
final result = handler.unwrapEnvelope(plainData);
|
|
|
|
expect(result, isNotNull);
|
|
expect(result!.payload, plainData);
|
|
expect(result.messageId, isNull);
|
|
expect(result.requiresAck, false);
|
|
});
|
|
|
|
test('returns null for duplicate messageId', () {
|
|
final data = {
|
|
'messageId': 'duplicate-id',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/server/user/message',
|
|
'payload': {'content': 'First'},
|
|
'requiresAck': false,
|
|
};
|
|
|
|
// First call should succeed
|
|
final firstResult = handler.unwrapEnvelope(data);
|
|
expect(firstResult, isNotNull);
|
|
|
|
// Second call with same messageId should return null
|
|
final secondResult = handler.unwrapEnvelope(data);
|
|
expect(secondResult, isNull);
|
|
});
|
|
|
|
test('calls onAckRequired for duplicate when original required ACK', () {
|
|
final data = {
|
|
'messageId': 'ack-duplicate-id',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/server/user/message',
|
|
'payload': {'content': 'Message'},
|
|
'requiresAck': true,
|
|
};
|
|
|
|
// First call
|
|
handler.unwrapEnvelope(data);
|
|
expect(ackCallbackCalls, isEmpty); // No ACK callback on first process
|
|
|
|
// Second call (duplicate) - should trigger ACK
|
|
handler.unwrapEnvelope(data);
|
|
expect(ackCallbackCalls, ['ack-duplicate-id']);
|
|
});
|
|
|
|
test('does not call onAckRequired for duplicate when requiresAck is false', () {
|
|
final data = {
|
|
'messageId': 'no-ack-duplicate',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/server/user/message',
|
|
'payload': {'content': 'Message'},
|
|
'requiresAck': false,
|
|
};
|
|
|
|
handler.unwrapEnvelope(data);
|
|
handler.unwrapEnvelope(data);
|
|
|
|
expect(ackCallbackCalls, isEmpty);
|
|
});
|
|
|
|
test('defaults requiresAck to true when not specified', () {
|
|
final data = {
|
|
'messageId': 'default-ack-id',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/server/user/message',
|
|
'payload': {'content': 'Message'},
|
|
// requiresAck not specified
|
|
};
|
|
|
|
final result = handler.unwrapEnvelope(data);
|
|
|
|
expect(result!.requiresAck, true);
|
|
});
|
|
|
|
test('handles list payload', () {
|
|
final data = {
|
|
'messageId': 'list-payload-id',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/server/user/jobs',
|
|
'payload': [
|
|
{'id': '1'},
|
|
{'id': '2'}
|
|
],
|
|
};
|
|
|
|
final result = handler.unwrapEnvelope(data);
|
|
|
|
expect(result!.payload, isList);
|
|
expect(result.payload.length, 2);
|
|
});
|
|
|
|
test('handles null payload', () {
|
|
final data = {
|
|
'messageId': 'null-payload-id',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/server/user/ping',
|
|
'payload': null,
|
|
};
|
|
|
|
final result = handler.unwrapEnvelope(data);
|
|
|
|
expect(result!.payload, isNull);
|
|
});
|
|
});
|
|
|
|
group('deduplication memory management', () {
|
|
test('respects maxProcessedIds limit', () {
|
|
final smallHandler = MessageHandler(maxProcessedIds: 3);
|
|
|
|
// Add 4 messages
|
|
for (var i = 1; i <= 4; i++) {
|
|
smallHandler.unwrapEnvelope({
|
|
'messageId': 'msg-$i',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/test',
|
|
'payload': null,
|
|
});
|
|
}
|
|
|
|
// Should only have 3 IDs tracked
|
|
expect(smallHandler.processedCount, 3);
|
|
|
|
// First message should have been evicted (FIFO)
|
|
expect(smallHandler.wasProcessed('msg-1'), false);
|
|
expect(smallHandler.wasProcessed('msg-2'), true);
|
|
expect(smallHandler.wasProcessed('msg-3'), true);
|
|
expect(smallHandler.wasProcessed('msg-4'), true);
|
|
});
|
|
|
|
test('allows reprocessing after eviction', () {
|
|
final smallHandler = MessageHandler(maxProcessedIds: 2);
|
|
|
|
// Process msg-1
|
|
final first = smallHandler.unwrapEnvelope({
|
|
'messageId': 'msg-1',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/test',
|
|
'payload': {'first': true},
|
|
});
|
|
expect(first, isNotNull);
|
|
|
|
// Process msg-2 and msg-3 to evict msg-1
|
|
smallHandler.unwrapEnvelope({
|
|
'messageId': 'msg-2',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/test',
|
|
'payload': null,
|
|
});
|
|
smallHandler.unwrapEnvelope({
|
|
'messageId': 'msg-3',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/test',
|
|
'payload': null,
|
|
});
|
|
|
|
// msg-1 should be processable again
|
|
final reprocessed = smallHandler.unwrapEnvelope({
|
|
'messageId': 'msg-1',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/test',
|
|
'payload': {'reprocessed': true},
|
|
});
|
|
expect(reprocessed, isNotNull);
|
|
expect(reprocessed!.payload, {'reprocessed': true});
|
|
});
|
|
});
|
|
|
|
group('wasProcessed', () {
|
|
test('returns true for processed message', () {
|
|
handler.unwrapEnvelope({
|
|
'messageId': 'processed-id',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/test',
|
|
'payload': null,
|
|
});
|
|
|
|
expect(handler.wasProcessed('processed-id'), true);
|
|
});
|
|
|
|
test('returns false for unprocessed message', () {
|
|
expect(handler.wasProcessed('unknown-id'), false);
|
|
});
|
|
});
|
|
|
|
group('clearProcessedIds', () {
|
|
test('removes all tracked message IDs', () {
|
|
handler.unwrapEnvelope({
|
|
'messageId': 'msg-1',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/test',
|
|
'payload': null,
|
|
});
|
|
handler.unwrapEnvelope({
|
|
'messageId': 'msg-2',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/test',
|
|
'payload': null,
|
|
});
|
|
|
|
expect(handler.processedCount, 2);
|
|
|
|
handler.clearProcessedIds();
|
|
|
|
expect(handler.processedCount, 0);
|
|
expect(handler.wasProcessed('msg-1'), false);
|
|
expect(handler.wasProcessed('msg-2'), false);
|
|
});
|
|
|
|
test('allows reprocessing cleared messages', () {
|
|
final data = {
|
|
'messageId': 'cleared-msg',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/test',
|
|
'payload': {'value': 1},
|
|
};
|
|
|
|
handler.unwrapEnvelope(data);
|
|
handler.clearProcessedIds();
|
|
|
|
final result = handler.unwrapEnvelope(data);
|
|
expect(result, isNotNull);
|
|
});
|
|
});
|
|
|
|
group('without onAckRequired callback', () {
|
|
test('handles duplicate gracefully when no callback set', () {
|
|
final noCallbackHandler = MessageHandler();
|
|
|
|
final data = {
|
|
'messageId': 'no-callback-msg',
|
|
'timestamp': '2024-01-15T10:30:00.000Z',
|
|
'topic': '/test',
|
|
'payload': null,
|
|
'requiresAck': true,
|
|
};
|
|
|
|
noCallbackHandler.unwrapEnvelope(data);
|
|
// Should not throw when processing duplicate
|
|
expect(() => noCallbackHandler.unwrapEnvelope(data), returnsNormally);
|
|
});
|
|
});
|
|
});
|
|
}
|