refactor: Projektstruktur in app/ und backend/ aufgeteilt
This commit is contained in:
350
app/test/services/message_handler_test.dart
Normal file
350
app/test/services/message_handler_test.dart
Normal file
@@ -0,0 +1,350 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user