refactor: Projektstruktur in app/ und backend/ aufgeteilt

This commit is contained in:
2026-03-24 15:06:44 +01:00
parent 5f5d5995c5
commit 2673ef658d
449 changed files with 28551 additions and 167 deletions

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