From 2534d321cfb63840da25030509ff10ed32ac1f28 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Thu, 26 Mar 2026 08:40:51 +0100 Subject: [PATCH] refactor: remove obsolete test files and clean up manifest --- app/android/app/src/main/AndroidManifest.xml | 1 + .../models/acknowledgment_message_test.dart | 201 ---- app/test/models/job_parsing_test.dart | 883 ------------------ app/test/models/message_envelope_test.dart | 190 ---- app/test/services/ack_tracker_test.dart | 312 ------- app/test/services/message_handler_test.dart | 350 ------- app/test/services/mqtt_integration_test.dart | 401 -------- app/test/views/cargo_items_view_test.dart | 86 -- .../votianlt/service/DemoModeService.java | 1 - 9 files changed, 1 insertion(+), 2424 deletions(-) delete mode 100644 app/test/models/acknowledgment_message_test.dart delete mode 100644 app/test/models/job_parsing_test.dart delete mode 100644 app/test/models/message_envelope_test.dart delete mode 100644 app/test/services/ack_tracker_test.dart delete mode 100644 app/test/services/message_handler_test.dart delete mode 100644 app/test/services/mqtt_integration_test.dart delete mode 100644 app/test/views/cargo_items_view_test.dart diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 32393e5..ada706b 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + completeJobJson = { - 'job': { - 'id': {'timestamp': 1705312200, '\$oid': '65a4b5c8d4e5f6a7b8c9d0e1'}, - 'jobNumber': 'JOB-2024-001', - 'status': 'ASSIGNED', - 'createdAt': '2024-01-15T10:30:00.000Z', - 'updatedAt': '2024-01-15T14:45:00.000Z', - 'createdBy': 'admin@example.com', - 'customerSelection': 'Kunde ABC GmbH', - 'pickupCompany': 'Absender GmbH', - 'pickupSalutation': 'Herr', - 'pickupFirstName': 'Max', - 'pickupLastName': 'Mustermann', - 'pickupPhone': '+49 123 456789', - 'pickupStreet': 'Hauptstraße', - 'pickupHouseNumber': '42', - 'pickupAddressAddition': 'Hinterhaus', - 'pickupZip': '10115', - 'pickupCity': 'Berlin', - 'deliveryCompany': 'Empfänger AG', - 'deliverySalutation': 'Frau', - 'deliveryFirstName': 'Erika', - 'deliveryLastName': 'Musterfrau', - 'deliveryPhone': '+49 987 654321', - 'deliveryStreet': 'Nebenstraße', - 'deliveryHouseNumber': '7a', - 'deliveryAddressAddition': null, - 'deliveryZip': '80331', - 'deliveryCity': 'München', - 'digitalProcessing': true, - 'appUser': 'driver@example.com', - 'pickupDate': '2024-01-16', - 'deliveryDate': '2024-01-17', - 'remark': 'Bitte vorsichtig behandeln', - 'price': 149.99, - 'draft': false, - }, - 'cargoItems': [ - { - 'id': {'timestamp': 1705312201}, - 'jobId': {'timestamp': 1705312200}, - 'description': 'Palette mit Elektronik', - 'quantity': 2, - 'weightKg': 150.5, - 'lengthMm': 1200.0, - 'widthMm': 800.0, - 'heightMm': 1000.0, - }, - { - 'id': {'timestamp': 1705312202}, - 'jobId': {'timestamp': 1705312200}, - 'description': 'Karton mit Dokumenten', - 'quantity': 5, - 'weightKg': 12.0, - 'lengthMm': 400.0, - 'widthMm': 300.0, - 'heightMm': 200.0, - }, - ], - 'tasks': [ - { - 'id': {'timestamp': 1705312210}, - 'jobId': {'timestamp': 1705312200}, - 'completed': false, - 'completedAt': null, - 'completedBy': null, - 'taskOrder': 1, - 'taskSpecificData': { - 'taskType': 'CONFIRMATION', - 'buttonText': 'Abholung bestätigen', - }, - }, - { - 'id': {'timestamp': 1705312211}, - 'jobId': {'timestamp': 1705312200}, - 'completed': true, - 'completedAt': '2024-01-16T09:15:00.000Z', - 'completedBy': 'driver@example.com', - 'taskOrder': 2, - 'taskSpecificData': {'taskType': 'SIGNATURE'}, - }, - { - 'id': {'timestamp': 1705312212}, - 'jobId': {'timestamp': 1705312200}, - 'completed': false, - 'completedAt': null, - 'completedBy': null, - 'taskOrder': 3, - 'taskSpecificData': { - 'taskType': 'PHOTO', - 'minPhotoCount': 2, - 'maxPhotoCount': 10, - }, - }, - { - 'id': {'timestamp': 1705312213}, - 'jobId': {'timestamp': 1705312200}, - 'completed': false, - 'completedAt': null, - 'completedBy': null, - 'taskOrder': 4, - 'taskSpecificData': { - 'taskType': 'BARCODE', - 'minBarcodeCount': 1, - 'maxBarcodeCount': 5, - }, - }, - { - 'id': {'timestamp': 1705312214}, - 'jobId': {'timestamp': 1705312200}, - 'completed': false, - 'completedAt': null, - 'completedBy': null, - 'taskOrder': 5, - 'taskSpecificData': { - 'taskType': 'TODOLIST', - 'todoItems': [ - 'Ladung sichern', - 'Dokumente prüfen', - 'Unterschrift einholen', - ], - }, - }, - { - 'id': {'timestamp': 1705312215}, - 'jobId': {'timestamp': 1705312200}, - 'completed': false, - 'completedAt': null, - 'completedBy': null, - 'taskOrder': 6, - 'taskSpecificData': { - 'taskType': 'COMMENT', - 'commentText': '', - 'required': true, - }, - }, - ], - }; - - group('Job Parsing', () { - late Job job; - - setUp(() { - job = Job.fromJson(completeJobJson); - }); - - test('parses job basic fields correctly', () { - expect(job.id, '1705312200'); - expect(job.jobNumber, 'JOB-2024-001'); - expect(job.status, 'ASSIGNED'); - expect(job.createdBy, 'admin@example.com'); - expect(job.customerSelection, 'Kunde ABC GmbH'); - expect(job.appUser, 'driver@example.com'); - expect(job.remark, 'Bitte vorsichtig behandeln'); - }); - - test('parses pickup address correctly', () { - expect(job.pickupCompany, 'Absender GmbH'); - expect(job.pickupSalutation, 'Herr'); - expect(job.pickupFirstName, 'Max'); - expect(job.pickupLastName, 'Mustermann'); - expect(job.pickupPhone, '+49 123 456789'); - expect(job.pickupStreet, 'Hauptstraße'); - expect(job.pickupHouseNumber, '42'); - expect(job.pickupAddressAddition, 'Hinterhaus'); - expect(job.pickupZip, '10115'); - expect(job.pickupCity, 'Berlin'); - }); - - test('parses delivery address correctly', () { - expect(job.deliveryCompany, 'Empfänger AG'); - expect(job.deliverySalutation, 'Frau'); - expect(job.deliveryFirstName, 'Erika'); - expect(job.deliveryLastName, 'Musterfrau'); - expect(job.deliveryPhone, '+49 987 654321'); - expect(job.deliveryStreet, 'Nebenstraße'); - expect(job.deliveryHouseNumber, '7a'); - expect(job.deliveryAddressAddition, ''); - expect(job.deliveryZip, '80331'); - expect(job.deliveryCity, 'München'); - }); - - test('parses date strings correctly', () { - expect(job.pickupDate, '2024-01-16'); - expect(job.deliveryDate, '2024-01-17'); - }); - - test('parses DateTime from ISO string', () { - expect(job.createdAt, DateTime.utc(2024, 1, 15, 10, 30, 0)); - expect(job.updatedAt, DateTime.utc(2024, 1, 15, 14, 45, 0)); - }); - - test('parses DateTime from array format', () { - final jsonWithArrayDate = Map.from(completeJobJson); - final jobData = Map.from(jsonWithArrayDate['job']); - jobData['createdAt'] = [2024, 1, 15, 10, 30, 0, 0]; - jobData['updatedAt'] = [2024, 1, 15, 14, 45, 0, 500000000]; - jsonWithArrayDate['job'] = jobData; - - final jobWithArrayDate = Job.fromJson(jsonWithArrayDate); - - expect(jobWithArrayDate.createdAt, DateTime(2024, 1, 15, 10, 30, 0, 0)); - expect(jobWithArrayDate.updatedAt.year, 2024); - expect(jobWithArrayDate.updatedAt.month, 1); - expect(jobWithArrayDate.updatedAt.day, 15); - }); - - test('parses price as double', () { - expect(job.price, 149.99); - }); - - test('parses boolean fields correctly', () { - expect(job.digitalProcessing, true); - expect(job.draft, false); - }); - - test('parses cargoItems array', () { - expect(job.cargoItems.length, 2); - }); - - test('parses tasks array', () { - expect(job.tasks.length, 6); - }); - - test('parses delivery stations and flattens station tasks', () { - final jsonWithStations = { - 'job': { - 'id': 'station-job-1', - 'jobNumber': 'JOB-STATION-001', - 'status': 'CREATED', - 'deliveryCompany': 'Legacy Delivery', - 'deliveryStreet': 'Legacy Street', - 'deliveryHouseNumber': '1', - 'deliveryZip': '12345', - 'deliveryCity': 'Legacy City', - 'deliveryCitiesDisplay': 'Boostedt -> Geesthacht', - 'firstDeliveryCity': 'Boostedt', - 'lastDeliveryCity': 'Geesthacht', - 'deliveryStations': [ - { - 'stationOrder': 0, - 'company': 'Volker Hinst', - 'street': 'Vossbarg', - 'houseNumber': '24', - 'zip': '24893', - 'city': 'Boostedt', - 'tasks': [ - { - 'id': 'station-task-1', - 'jobId': 'station-job-1', - 'taskOrder': 0, - 'description': 'Erste Station', - 'displayName': 'Bestätigung', - 'taskSpecificData': { - 'taskType': 'CONFIRMATION', - 'buttonText': 'Blubb', - }, - }, - ], - }, - { - 'stationOrder': 1, - 'company': 'Timm GmbH', - 'street': 'Gerhart-Hauptmann-Weg', - 'houseNumber': '14', - 'zip': '21502', - 'city': 'Geesthacht', - 'tasks': [ - { - 'id': 'station-task-2', - 'jobId': 'station-job-1', - 'taskOrder': 0, - 'description': 'Zweite Station', - 'displayName': 'Bestätigung', - 'taskSpecificData': { - 'taskType': 'CONFIRMATION', - 'buttonText': 'Blubb', - }, - }, - ], - }, - ], - }, - 'cargoItems': [], - 'tasks': [ - { - 'id': 'legacy-task', - 'jobId': 'station-job-1', - 'taskOrder': 0, - 'description': 'Legacy', - 'taskSpecificData': { - 'taskType': 'CONFIRMATION', - 'buttonText': 'Legacy', - }, - }, - ], - }; - - final jobWithStations = Job.fromJson(jsonWithStations); - - expect(jobWithStations.deliveryStations.length, 2); - expect(jobWithStations.tasks.length, 2); - expect(jobWithStations.tasks[0].stationOrder, 0); - expect(jobWithStations.tasks[1].stationOrder, 1); - expect( - jobWithStations.deliveryStations[0].tasks.first.id, - 'station-task-1', - ); - expect(jobWithStations.deliveryCitiesDisplay, 'Boostedt -> Geesthacht'); - }); - - test('extracts ID from Map object with timestamp', () { - expect(job.id, '1705312200'); - }); - - test('extracts ID from Map object with \$oid fallback', () { - final jsonWithOidOnly = { - 'job': { - 'id': {'\$oid': '65a4b5c8d4e5f6a7b8c9d0e1'}, - 'jobNumber': 'JOB-TEST', - 'status': 'CREATED', - }, - 'cargoItems': [], - 'tasks': [], - }; - - final jobWithOid = Job.fromJson(jsonWithOidOnly); - expect(jobWithOid.id, '65a4b5c8d4e5f6a7b8c9d0e1'); - }); - - test('handles flat JSON structure (without nested job object)', () { - final flatJson = { - 'id': 'flat-job-id-123', - 'jobNumber': 'JOB-FLAT-001', - 'status': 'CREATED', - 'createdAt': '2024-01-15T10:30:00.000Z', - 'updatedAt': '2024-01-15T10:30:00.000Z', - 'createdBy': 'test@test.de', - 'customerSelection': 'Test Kunde', - 'pickupCompany': 'Test Firma', - 'pickupFirstName': 'Test', - 'pickupLastName': 'User', - 'pickupPhone': '12345', - 'pickupStreet': 'Teststr', - 'pickupHouseNumber': '1', - 'pickupAddressAddition': '', - 'pickupZip': '12345', - 'pickupCity': 'Teststadt', - 'deliveryCompany': 'Ziel Firma', - 'deliveryFirstName': 'Ziel', - 'deliveryLastName': 'Person', - 'deliveryPhone': '54321', - 'deliveryStreet': 'Zielstr', - 'deliveryHouseNumber': '2', - 'deliveryAddressAddition': '', - 'deliveryZip': '54321', - 'deliveryCity': 'Zielstadt', - 'digitalProcessing': false, - 'appUser': 'user@test.de', - 'pickupDate': '2024-01-20', - 'deliveryDate': '2024-01-21', - 'remark': '', - 'price': 0.0, - 'draft': true, - }; - - final flatJob = Job.fromJson(flatJson); - - expect(flatJob.id, 'flat-job-id-123'); - expect(flatJob.jobNumber, 'JOB-FLAT-001'); - expect(flatJob.draft, true); - }); - }); - - group('CargoItem Parsing', () { - late Job job; - - setUp(() { - job = Job.fromJson(completeJobJson); - }); - - test('parses CargoItem fields correctly', () { - final cargoItem = job.cargoItems[0]; - - expect(cargoItem.id, '1705312201'); - expect(cargoItem.jobId, '1705312200'); - expect(cargoItem.description, 'Palette mit Elektronik'); - expect(cargoItem.quantity, 2); - expect(cargoItem.weightKg, 150.5); - expect(cargoItem.lengthCm, 1200.0); - expect(cargoItem.widthCm, 800.0); - expect(cargoItem.heightCm, 1000.0); - }); - - test('parses multiple CargoItems', () { - expect(job.cargoItems.length, 2); - expect(job.cargoItems[0].description, 'Palette mit Elektronik'); - expect(job.cargoItems[1].description, 'Karton mit Dokumenten'); - }); - - test('extracts CargoItem ID from Map object', () { - expect(job.cargoItems[0].id, '1705312201'); - expect(job.cargoItems[1].id, '1705312202'); - }); - - test('handles CargoItem with simple string ID', () { - final cargoJson = { - 'id': 'simple-string-id', - 'jobId': 'simple-job-id', - 'description': 'Test Item', - 'quantity': 1, - 'weightKg': 10.0, - 'lengthMm': 100.0, - 'widthMm': 100.0, - 'heightMm': 100.0, - }; - - final cargoItem = CargoItem.fromJson(cargoJson); - - expect(cargoItem.id, 'simple-string-id'); - expect(cargoItem.jobId, 'simple-job-id'); - }); - }); - - group('Task Parsing', () { - late Job job; - - setUp(() { - job = Job.fromJson(completeJobJson); - }); - - test('creates ConfirmationTask with buttonText', () { - final task = job.tasks[0]; - - expect(task, isA()); - final confirmationTask = task as ConfirmationTask; - expect(confirmationTask.taskOrder, 1); - expect(confirmationTask.buttonText, 'Abholung bestätigen'); - expect(confirmationTask.completed, false); - }); - - test('creates SignatureTask', () { - final task = job.tasks[1]; - - expect(task, isA()); - expect(task.taskOrder, 2); - expect(task.completed, true); - expect(task.completedBy, 'driver@example.com'); - }); - - test('creates PhotoTask with min/maxPhotoCount', () { - final task = job.tasks[2]; - - expect(task, isA()); - final photoTask = task as PhotoTask; - expect(photoTask.taskOrder, 3); - expect(photoTask.minPhotoCount, 2); - expect(photoTask.maxPhotoCount, 10); - }); - - test('creates BarcodeTask with min/maxBarcodeCount', () { - final task = job.tasks[3]; - - expect(task, isA()); - final barcodeTask = task as BarcodeTask; - expect(barcodeTask.taskOrder, 4); - expect(barcodeTask.minBarcodeCount, 1); - expect(barcodeTask.maxBarcodeCount, 5); - }); - - test('creates TodoListTask with todoItems', () { - final task = job.tasks[4]; - - expect(task, isA()); - final todoListTask = task as TodoListTask; - expect(todoListTask.taskOrder, 5); - expect(todoListTask.todoItems.length, 3); - expect(todoListTask.todoItems[0], 'Ladung sichern'); - expect(todoListTask.todoItems[1], 'Dokumente prüfen'); - expect(todoListTask.todoItems[2], 'Unterschrift einholen'); - }); - - test('creates CommentTask with commentText and required', () { - final task = job.tasks[5]; - - expect(task, isA()); - final commentTask = task as CommentTask; - expect(commentTask.taskOrder, 6); - expect(commentTask.commentText, ''); - expect(commentTask.required, true); - }); - - test('falls back to GenericTask for unknown task type', () { - final unknownTaskJson = { - 'id': {'timestamp': 1705312299}, - 'jobId': {'timestamp': 1705312200}, - 'completed': false, - 'taskOrder': 99, - 'taskSpecificData': {'taskType': 'UNKNOWN_TYPE'}, - }; - - final task = Task.fromJson(unknownTaskJson); - - expect(task, isA()); - }); - - test('falls back to GenericTask when taskType is missing', () { - final noTypeTaskJson = { - 'id': {'timestamp': 1705312298}, - 'jobId': {'timestamp': 1705312200}, - 'completed': false, - 'taskOrder': 98, - 'taskSpecificData': {}, - }; - - final task = Task.fromJson(noTypeTaskJson); - - expect(task, isA()); - }); - - test('parses completedAt from ISO string', () { - final task = job.tasks[1]; - - expect(task.completedAt, DateTime.utc(2024, 1, 16, 9, 15, 0)); - }); - - test('parses completedAt from array format', () { - final taskJsonWithArrayDate = { - 'id': {'timestamp': 1705312220}, - 'jobId': {'timestamp': 1705312200}, - 'completed': true, - 'completedAt': [2024, 1, 16, 9, 15, 0, 0], - 'completedBy': 'driver@example.com', - 'taskOrder': 10, - 'taskSpecificData': {'taskType': 'SIGNATURE'}, - }; - - final task = Task.fromJson(taskJsonWithArrayDate); - - expect(task.completedAt, DateTime(2024, 1, 16, 9, 15, 0, 0)); - }); - - test('extracts task ID from Map object', () { - final task = job.tasks[0]; - expect(task.id, '1705312210'); - }); - - test('extracts task jobId from Map object', () { - final task = job.tasks[0]; - expect(task.jobId, '1705312200'); - }); - }); - - group('Task Defaults', () { - test('ConfirmationTask uses default buttonText', () { - final taskJson = { - 'id': 'task-1', - 'jobId': 'job-1', - 'taskOrder': 1, - 'taskSpecificData': {'taskType': 'CONFIRMATION'}, - }; - - final task = Task.fromJson(taskJson) as ConfirmationTask; - expect(task.buttonText, 'Bestätigen'); - }); - - test('PhotoTask uses default min/max counts', () { - final taskJson = { - 'id': 'task-2', - 'jobId': 'job-1', - 'taskOrder': 2, - 'taskSpecificData': {'taskType': 'PHOTO'}, - }; - - final task = Task.fromJson(taskJson) as PhotoTask; - expect(task.minPhotoCount, 1); - expect(task.maxPhotoCount, 5); - }); - - test('BarcodeTask uses default min/max counts', () { - final taskJson = { - 'id': 'task-3', - 'jobId': 'job-1', - 'taskOrder': 3, - 'taskSpecificData': {'taskType': 'BARCODE'}, - }; - - final task = Task.fromJson(taskJson) as BarcodeTask; - expect(task.minBarcodeCount, 1); - expect(task.maxBarcodeCount, 10); - }); - - test('CommentTask uses default values', () { - final taskJson = { - 'id': 'task-4', - 'jobId': 'job-1', - 'taskOrder': 4, - 'taskSpecificData': {'taskType': 'COMMENT'}, - }; - - final task = Task.fromJson(taskJson) as CommentTask; - expect(task.commentText, ''); - expect(task.required, false); - }); - - test('TodoListTask handles empty todoItems', () { - final taskJson = { - 'id': 'task-5', - 'jobId': 'job-1', - 'taskOrder': 5, - 'taskSpecificData': {'taskType': 'TODOLIST'}, - }; - - final task = Task.fromJson(taskJson) as TodoListTask; - expect(task.todoItems, isEmpty); - }); - }); - - group('Edge Cases', () { - test('handles empty cargoItems array', () { - final jsonWithEmptyCargoItems = Map.from( - completeJobJson, - ); - jsonWithEmptyCargoItems['cargoItems'] = []; - - final job = Job.fromJson(jsonWithEmptyCargoItems); - - expect(job.cargoItems, isEmpty); - }); - - test('handles empty tasks array', () { - final jsonWithEmptyTasks = Map.from(completeJobJson); - jsonWithEmptyTasks['tasks'] = []; - - final job = Job.fromJson(jsonWithEmptyTasks); - - expect(job.tasks, isEmpty); - }); - - test('handles missing optional fields with defaults', () { - final minimalJson = { - 'job': {'jobNumber': 'JOB-MIN-001'}, - 'cargoItems': [], - 'tasks': [], - }; - - final job = Job.fromJson(minimalJson); - - expect(job.jobNumber, 'JOB-MIN-001'); - expect(job.status, 'UNKNOWN'); - expect(job.pickupCompany, ''); - expect(job.deliveryCompany, ''); - expect(job.digitalProcessing, false); - expect(job.price, 0.0); - expect(job.draft, false); - }); - - test('handles null values in optional fields', () { - final jsonWithNulls = { - 'job': { - 'id': 'null-test-id', - 'jobNumber': 'JOB-NULL-001', - 'status': 'CREATED', - 'pickupSalutation': null, - 'deliverySalutation': null, - 'pickupAddressAddition': null, - 'deliveryAddressAddition': null, - 'remark': null, - }, - 'cargoItems': [], - 'tasks': [], - }; - - final job = Job.fromJson(jsonWithNulls); - - expect(job.pickupSalutation, isNull); - expect(job.deliverySalutation, isNull); - expect(job.pickupAddressAddition, ''); - expect(job.deliveryAddressAddition, ''); - expect(job.remark, ''); - }); - - test('generates ID from jobNumber when ID is missing', () { - final jsonWithoutId = { - 'job': {'jobNumber': 'JOB-NOID-001'}, - 'cargoItems': [], - 'tasks': [], - }; - - final job = Job.fromJson(jsonWithoutId); - - expect(job.id, 'jobnum:JOB-NOID-001'); - }); - }); - - group('Roundtrip (fromJson -> toJson -> fromJson)', () { - test('Job preserves data through serialization', () { - final original = Job.fromJson(completeJobJson); - final json = original.toJson(); - final restored = Job.fromJson(json); - - expect(restored.id, original.id); - expect(restored.jobNumber, original.jobNumber); - expect(restored.status, original.status); - expect(restored.pickupCompany, original.pickupCompany); - expect(restored.pickupCity, original.pickupCity); - expect(restored.deliveryCompany, original.deliveryCompany); - expect(restored.deliveryCity, original.deliveryCity); - expect(restored.price, original.price); - expect(restored.digitalProcessing, original.digitalProcessing); - }); - - test('CargoItem preserves data through serialization', () { - final original = Job.fromJson(completeJobJson).cargoItems[0]; - final json = original.toJson(); - final restored = CargoItem.fromJson(json); - - expect(restored.id, original.id); - expect(restored.jobId, original.jobId); - expect(restored.description, original.description); - expect(restored.quantity, original.quantity); - expect(restored.weightKg, original.weightKg); - expect(restored.lengthCm, original.lengthCm); - }); - - test('ConfirmationTask preserves data through serialization', () { - final original = - Job.fromJson(completeJobJson).tasks[0] as ConfirmationTask; - final json = original.toJson(); - final restored = Task.fromJson(json) as ConfirmationTask; - - expect(restored.id, original.id); - expect(restored.jobId, original.jobId); - expect(restored.buttonText, original.buttonText); - expect(restored.taskOrder, original.taskOrder); - }); - - test('PhotoTask preserves data through serialization', () { - final original = Job.fromJson(completeJobJson).tasks[2] as PhotoTask; - final json = original.toJson(); - final restored = Task.fromJson(json) as PhotoTask; - - expect(restored.minPhotoCount, original.minPhotoCount); - expect(restored.maxPhotoCount, original.maxPhotoCount); - }); - - test('TodoListTask preserves data through serialization', () { - final original = Job.fromJson(completeJobJson).tasks[4] as TodoListTask; - final json = original.toJson(); - final restored = Task.fromJson(json) as TodoListTask; - - expect(restored.todoItems, original.todoItems); - }); - - test('CommentTask preserves data through serialization', () { - final original = Job.fromJson(completeJobJson).tasks[5] as CommentTask; - final json = original.toJson(); - final restored = Task.fromJson(json) as CommentTask; - - expect(restored.commentText, original.commentText); - expect(restored.required, original.required); - }); - }); - - group('Job Status', () { - test('statusDisplayText returns German text for known statuses', () { - final statuses = { - 'CREATED': 'Erstellt', - 'PENDING': 'Wartend', - 'ASSIGNED': 'Zugewiesen', - 'IN_PROGRESS': 'In Bearbeitung', - 'STARTED': 'In Bearbeitung', - 'COMPLETED': 'Abgeschlossen', - 'DONE': 'Abgeschlossen', - 'CANCELLED': 'Abgebrochen', - 'FAILED': 'Fehlgeschlagen', - }; - - for (final entry in statuses.entries) { - final json = { - 'job': { - 'id': 'status-test', - 'jobNumber': 'JOB-STATUS', - 'status': entry.key, - }, - 'cargoItems': [], - 'tasks': [], - }; - - final job = Job.fromJson(json); - expect( - job.statusDisplayText, - entry.value, - reason: 'Status ${entry.key} should display as ${entry.value}', - ); - } - }); - - test('statusColor returns correct color for statuses', () { - final statusColors = { - 'CREATED': 'orange', - 'ASSIGNED': 'orange', - 'IN_PROGRESS': 'blue', - 'COMPLETED': 'green', - 'CANCELLED': 'red', - }; - - for (final entry in statusColors.entries) { - final json = { - 'job': { - 'id': 'color-test', - 'jobNumber': 'JOB-COLOR', - 'status': entry.key, - }, - 'cargoItems': [], - 'tasks': [], - }; - - final job = Job.fromJson(json); - expect( - job.statusColor, - entry.value, - reason: 'Status ${entry.key} should have color ${entry.value}', - ); - } - }); - }); - - group('Job normalized()', () { - test('trims string fields', () { - final json = { - 'job': { - 'id': 'normalize-test', - 'jobNumber': ' JOB-TRIM ', - 'status': ' ASSIGNED ', - 'pickupCompany': ' Test Company ', - 'pickupCity': ' Berlin ', - }, - 'cargoItems': [], - 'tasks': [], - }; - - final job = Job.fromJson(json).normalized(); - - expect(job.jobNumber, 'JOB-TRIM'); - expect(job.status, 'ASSIGNED'); - expect(job.pickupCompany, 'Test Company'); - expect(job.pickupCity, 'Berlin'); - }); - - test('converts null strings to empty strings', () { - final json = { - 'job': { - 'id': 'null-normalize-test', - 'jobNumber': 'JOB-NULL', - 'pickupSalutation': null, - }, - 'cargoItems': [], - 'tasks': [], - }; - - final job = Job.fromJson(json).normalized(); - - expect(job.pickupSalutation, ''); - }); - }); -} diff --git a/app/test/models/message_envelope_test.dart b/app/test/models/message_envelope_test.dart deleted file mode 100644 index aed4915..0000000 --- a/app/test/models/message_envelope_test.dart +++ /dev/null @@ -1,190 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:votianlt_app/models/message_envelope.dart'; - -void main() { - group('MessageEnvelope', () { - group('fromJson', () { - test('parses all required fields correctly', () { - final json = { - 'messageId': 'test-uuid-123', - 'timestamp': '2024-01-15T10:30:00.000Z', - 'topic': '/server/user123/message', - 'payload': {'key': 'value'}, - }; - - final envelope = MessageEnvelope.fromJson(json); - - expect(envelope.messageId, 'test-uuid-123'); - expect(envelope.timestamp, DateTime.utc(2024, 1, 15, 10, 30, 0)); - expect(envelope.topic, '/server/user123/message'); - expect(envelope.payload, {'key': 'value'}); - expect(envelope.requiresAck, true); // default - expect(envelope.retryCount, 0); // default - expect(envelope.expiresAt, isNull); - }); - - test('parses optional fields when present', () { - final json = { - 'messageId': 'test-uuid-456', - 'timestamp': '2024-01-15T10:30:00.000Z', - 'topic': '/server/user123/message', - 'payload': {'data': 123}, - 'requiresAck': false, - 'retryCount': 3, - 'expiresAt': '2024-01-15T11:30:00.000Z', - }; - - final envelope = MessageEnvelope.fromJson(json); - - expect(envelope.requiresAck, false); - expect(envelope.retryCount, 3); - expect(envelope.expiresAt, DateTime.utc(2024, 1, 15, 11, 30, 0)); - }); - - test('handles list payload', () { - final json = { - 'messageId': 'test-uuid-789', - 'timestamp': '2024-01-15T10:30:00.000Z', - 'topic': '/server/user123/jobs', - 'payload': [ - {'id': '1'}, - {'id': '2'} - ], - }; - - final envelope = MessageEnvelope.fromJson(json); - - expect(envelope.payload, isList); - expect(envelope.payload.length, 2); - }); - - test('handles null payload', () { - final json = { - 'messageId': 'test-uuid-null', - 'timestamp': '2024-01-15T10:30:00.000Z', - 'topic': '/server/user123/ping', - 'payload': null, - }; - - final envelope = MessageEnvelope.fromJson(json); - - expect(envelope.payload, isNull); - }); - }); - - group('toJson', () { - test('serializes all fields correctly', () { - final envelope = MessageEnvelope( - messageId: 'test-uuid-123', - timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0), - topic: '/server/user123/message', - payload: {'key': 'value'}, - requiresAck: true, - retryCount: 2, - expiresAt: DateTime.utc(2024, 1, 15, 11, 30, 0), - ); - - final json = envelope.toJson(); - - expect(json['messageId'], 'test-uuid-123'); - expect(json['timestamp'], '2024-01-15T10:30:00.000Z'); - expect(json['topic'], '/server/user123/message'); - expect(json['payload'], {'key': 'value'}); - expect(json['requiresAck'], true); - expect(json['retryCount'], 2); - expect(json['expiresAt'], '2024-01-15T11:30:00.000Z'); - }); - - test('omits expiresAt when null', () { - final envelope = MessageEnvelope( - messageId: 'test-uuid-123', - timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0), - topic: '/server/user123/message', - payload: {'key': 'value'}, - ); - - final json = envelope.toJson(); - - expect(json.containsKey('expiresAt'), false); - }); - }); - - group('fromJson/toJson roundtrip', () { - test('preserves all data through serialization', () { - final original = MessageEnvelope( - messageId: 'roundtrip-uuid', - timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0), - topic: '/server/user123/message', - payload: {'nested': {'key': 'value'}, 'list': [1, 2, 3]}, - requiresAck: false, - retryCount: 5, - expiresAt: DateTime.utc(2024, 1, 16, 10, 30, 0), - ); - - final json = original.toJson(); - final restored = MessageEnvelope.fromJson(json); - - expect(restored.messageId, original.messageId); - expect(restored.timestamp, original.timestamp); - expect(restored.topic, original.topic); - expect(restored.payload, original.payload); - expect(restored.requiresAck, original.requiresAck); - expect(restored.retryCount, original.retryCount); - expect(restored.expiresAt, original.expiresAt); - }); - }); - - group('copyWith', () { - test('creates copy with updated messageId', () { - final original = MessageEnvelope( - messageId: 'original-id', - timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0), - topic: '/server/user123/message', - payload: {'key': 'value'}, - ); - - final copy = original.copyWith(messageId: 'new-id'); - - expect(copy.messageId, 'new-id'); - expect(copy.timestamp, original.timestamp); - expect(copy.topic, original.topic); - expect(copy.payload, original.payload); - }); - - test('creates copy with updated retryCount', () { - final original = MessageEnvelope( - messageId: 'test-id', - timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0), - topic: '/server/user123/message', - payload: {'key': 'value'}, - retryCount: 0, - ); - - final copy = original.copyWith(retryCount: 3); - - expect(copy.retryCount, 3); - expect(copy.messageId, original.messageId); - }); - }); - - group('toString', () { - test('returns readable representation', () { - final envelope = MessageEnvelope( - messageId: 'test-id', - timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0), - topic: '/server/user123/message', - payload: {'key': 'value'}, - requiresAck: true, - retryCount: 2, - ); - - final str = envelope.toString(); - - expect(str, contains('test-id')); - expect(str, contains('/server/user123/message')); - expect(str, contains('requiresAck: true')); - expect(str, contains('retryCount: 2')); - }); - }); - }); -} diff --git a/app/test/services/ack_tracker_test.dart b/app/test/services/ack_tracker_test.dart deleted file mode 100644 index f9852cd..0000000 --- a/app/test/services/ack_tracker_test.dart +++ /dev/null @@ -1,312 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:votianlt_app/services/ack_tracker.dart'; - -void main() { - group('AckTracker', () { - late AckTracker tracker; - late List> retryCalls; - late List> timeoutCalls; - - setUp(() { - retryCalls = []; - timeoutCalls = []; - tracker = AckTracker( - maxRetries: 4, - onRetry: (topic, payload) async { - retryCalls.add({'topic': topic, 'payload': payload}); - return true; - }, - onTimeout: (messageId, topic) { - timeoutCalls.add({'messageId': messageId, 'topic': topic}); - }, - ); - }); - - group('track', () { - test('adds message to pending', () { - tracker.track('msg-1', '/server/user/message', '{"data": "test"}'); - - expect(tracker.isPending('msg-1'), true); - expect(tracker.pendingCount, 1); - }); - - test('can track multiple messages', () { - tracker.track('msg-1', '/topic1', 'payload1'); - tracker.track('msg-2', '/topic2', 'payload2'); - tracker.track('msg-3', '/topic3', 'payload3'); - - expect(tracker.pendingCount, 3); - expect(tracker.isPending('msg-1'), true); - expect(tracker.isPending('msg-2'), true); - expect(tracker.isPending('msg-3'), true); - }); - - test('stores correct topic and payload', () { - tracker.track('msg-1', '/server/user/message', '{"key": "value"}'); - - final pending = tracker.getPendingMessage('msg-1'); - expect(pending, isNotNull); - expect(pending!.topic, '/server/user/message'); - expect(pending.jsonPayload, '{"key": "value"}'); - expect(pending.retryCount, 0); - }); - - test('overwrites existing message with same ID', () { - tracker.track('msg-1', '/old/topic', 'old payload'); - tracker.track('msg-1', '/new/topic', 'new payload'); - - final pending = tracker.getPendingMessage('msg-1'); - expect(pending!.topic, '/new/topic'); - expect(pending.jsonPayload, 'new payload'); - expect(tracker.pendingCount, 1); - }); - }); - - group('acknowledge', () { - test('removes message from pending', () { - tracker.track('msg-1', '/topic', 'payload'); - expect(tracker.isPending('msg-1'), true); - - tracker.acknowledge('msg-1'); - - expect(tracker.isPending('msg-1'), false); - expect(tracker.pendingCount, 0); - }); - - test('does nothing for unknown message ID', () { - tracker.track('msg-1', '/topic', 'payload'); - - tracker.acknowledge('unknown-msg'); - - expect(tracker.pendingCount, 1); - expect(tracker.isPending('msg-1'), true); - }); - - test('only removes specified message', () { - tracker.track('msg-1', '/topic1', 'payload1'); - tracker.track('msg-2', '/topic2', 'payload2'); - - tracker.acknowledge('msg-1'); - - expect(tracker.isPending('msg-1'), false); - expect(tracker.isPending('msg-2'), true); - expect(tracker.pendingCount, 1); - }); - }); - - group('isPending', () { - test('returns true for tracked message', () { - tracker.track('msg-1', '/topic', 'payload'); - expect(tracker.isPending('msg-1'), true); - }); - - test('returns false for untracked message', () { - expect(tracker.isPending('unknown'), false); - }); - - test('returns false after acknowledge', () { - tracker.track('msg-1', '/topic', 'payload'); - tracker.acknowledge('msg-1'); - expect(tracker.isPending('msg-1'), false); - }); - }); - - group('pendingMessageIds', () { - test('returns empty list when no messages pending', () { - expect(tracker.pendingMessageIds, isEmpty); - }); - - test('returns all pending message IDs', () { - tracker.track('msg-1', '/topic1', 'payload1'); - tracker.track('msg-2', '/topic2', 'payload2'); - - final ids = tracker.pendingMessageIds; - - expect(ids, containsAll(['msg-1', 'msg-2'])); - expect(ids.length, 2); - }); - - test('returns unmodifiable list', () { - tracker.track('msg-1', '/topic', 'payload'); - - final ids = tracker.pendingMessageIds; - - expect(() => ids.add('new-id'), throwsUnsupportedError); - }); - }); - - group('processRetries', () { - test('increments retryCount on each call', () async { - tracker.track('msg-1', '/topic', 'payload'); - - await tracker.processRetries(); - expect(tracker.getPendingMessage('msg-1')!.retryCount, 1); - - await tracker.processRetries(); - expect(tracker.getPendingMessage('msg-1')!.retryCount, 2); - - await tracker.processRetries(); - expect(tracker.getPendingMessage('msg-1')!.retryCount, 3); - }); - - test('calls onRetry callback with correct parameters', () async { - tracker.track('msg-1', '/server/user/message', '{"data": "test"}'); - - await tracker.processRetries(); - - expect(retryCalls.length, 1); - expect(retryCalls[0]['topic'], '/server/user/message'); - expect(retryCalls[0]['payload'], '{"data": "test"}'); - }); - - test('calls onRetry for each pending message', () async { - tracker.track('msg-1', '/topic1', 'payload1'); - tracker.track('msg-2', '/topic2', 'payload2'); - - await tracker.processRetries(); - - expect(retryCalls.length, 2); - }); - - test('calls onTimeout after maxRetries exceeded', () async { - tracker.track('msg-1', '/timeout/topic', 'payload'); - - // Process until maxRetries reached - for (var i = 0; i < 4; i++) { - await tracker.processRetries(); - } - - expect(timeoutCalls, isEmpty); - expect(tracker.isPending('msg-1'), true); - - // One more retry should trigger timeout - await tracker.processRetries(); - - expect(timeoutCalls.length, 1); - expect(timeoutCalls[0]['messageId'], 'msg-1'); - expect(timeoutCalls[0]['topic'], '/timeout/topic'); - }); - - test('removes message after timeout', () async { - tracker.track('msg-1', '/topic', 'payload'); - - // Process until timeout - for (var i = 0; i <= 4; i++) { - await tracker.processRetries(); - } - - expect(tracker.isPending('msg-1'), false); - expect(tracker.pendingCount, 0); - }); - - test('does not retry when isConnected is false', () async { - tracker.track('msg-1', '/topic', 'payload'); - - await tracker.processRetries(isConnected: false); - - expect(retryCalls, isEmpty); - expect(tracker.getPendingMessage('msg-1')!.retryCount, 0); - }); - - test('still times out when disconnected after max retries', () async { - tracker.track('msg-1', '/topic', 'payload'); - - // Manually set retry count to max (simulating previous retries) - final pending = tracker.getPendingMessage('msg-1')!; - pending.retryCount = 4; - - await tracker.processRetries(isConnected: false); - - expect(timeoutCalls.length, 1); - expect(tracker.isPending('msg-1'), false); - }); - - test('does nothing when no messages pending', () async { - await tracker.processRetries(); - - expect(retryCalls, isEmpty); - expect(timeoutCalls, isEmpty); - }); - }); - - group('clearAll', () { - test('removes all pending messages', () { - tracker.track('msg-1', '/topic1', 'payload1'); - tracker.track('msg-2', '/topic2', 'payload2'); - - tracker.clearAll(); - - expect(tracker.pendingCount, 0); - expect(tracker.isPending('msg-1'), false); - expect(tracker.isPending('msg-2'), false); - }); - }); - - group('clearForTopic', () { - test('removes only messages for matching topic', () { - tracker.track('msg-1', '/server/login', 'login1'); - tracker.track('msg-2', '/server/login', 'login2'); - tracker.track('msg-3', '/server/user/message', 'message'); - - tracker.clearForTopic('/server/login'); - - expect(tracker.isPending('msg-1'), false); - expect(tracker.isPending('msg-2'), false); - expect(tracker.isPending('msg-3'), true); - expect(tracker.pendingCount, 1); - }); - - test('does nothing when no matching topic', () { - tracker.track('msg-1', '/server/user/message', 'payload'); - - tracker.clearForTopic('/server/login'); - - expect(tracker.pendingCount, 1); - expect(tracker.isPending('msg-1'), true); - }); - }); - - group('without callbacks', () { - test('processRetries works without onRetry callback', () async { - final noCallbackTracker = AckTracker(maxRetries: 2); - noCallbackTracker.track('msg-1', '/topic', 'payload'); - - // Should not throw - await noCallbackTracker.processRetries(); - - expect(noCallbackTracker.getPendingMessage('msg-1')!.retryCount, 1); - }); - - test('processRetries works without onTimeout callback', () async { - final noCallbackTracker = AckTracker(maxRetries: 1); - noCallbackTracker.track('msg-1', '/topic', 'payload'); - - // Process until timeout - await noCallbackTracker.processRetries(); - await noCallbackTracker.processRetries(); - - // Should be removed even without callback - expect(noCallbackTracker.isPending('msg-1'), false); - }); - }); - - group('PendingMessage', () { - test('stores sentAt timestamp', () { - final before = DateTime.now(); - tracker.track('msg-1', '/topic', 'payload'); - final after = DateTime.now(); - - final pending = tracker.getPendingMessage('msg-1')!; - - expect(pending.sentAt.isAfter(before) || pending.sentAt == before, true); - expect(pending.sentAt.isBefore(after) || pending.sentAt == after, true); - }); - - test('initializes retryCount to 0', () { - tracker.track('msg-1', '/topic', 'payload'); - - expect(tracker.getPendingMessage('msg-1')!.retryCount, 0); - }); - }); - }); -} diff --git a/app/test/services/message_handler_test.dart b/app/test/services/message_handler_test.dart deleted file mode 100644 index f7c8443..0000000 --- a/app/test/services/message_handler_test.dart +++ /dev/null @@ -1,350 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:votianlt_app/services/message_handler.dart'; - -void main() { - group('MessageHandler', () { - late MessageHandler handler; - late List 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); - }); - }); - }); -} diff --git a/app/test/services/mqtt_integration_test.dart b/app/test/services/mqtt_integration_test.dart deleted file mode 100644 index cf6dfad..0000000 --- a/app/test/services/mqtt_integration_test.dart +++ /dev/null @@ -1,401 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:votianlt_app/services/message_handler.dart'; -import 'package:votianlt_app/services/ack_tracker.dart'; - -/// Integration tests simulating the full MQTT message flow -/// based on real app behavior from login through job loading. -void main() { - group('MQTT Integration Scenarios', () { - group('Login Flow', () { - test('complete login flow with message tracking and ACK handling', () async { - // === SETUP === - final acksSent = []; - final retriedMessages = []; - - final messageHandler = MessageHandler(maxProcessedIds: 100, onAckRequired: (messageId) => acksSent.add(messageId)); - - final ackTracker = AckTracker( - maxRetries: 4, - onRetry: (topic, payload) async { - retriedMessages.add(topic); - return true; - }, - onTimeout: (messageId, topic) {}, - ); - - const appId = '410cea21-a3cf-47c3-97ad-b1c01f0bacbb'; - const userId = '693fdcc757853e744d2ab0d5'; - - // === STEP 1: Send Login Request === - // App sends login message with envelope - const loginMessageId = '5f4907de-684e-4381-a771-bd4d7ecdddbd'; - const loginTopic = '/server/login'; - final loginPayload = '''{ - "messageId": "$loginMessageId", - "timestamp": "2026-01-13T11:13:10.275836", - "topic": "$loginTopic", - "payload": { - "email": "mail@svencarstensen.de", - "password": "secret" - }, - "requiresAck": true, - "retryCount": 0 - }'''; - - // Track the sent login message for ACK - ackTracker.track(loginMessageId, loginTopic, loginPayload); - - expect(ackTracker.isPending(loginMessageId), true); - expect(ackTracker.pendingCount, 1); - - // === STEP 2: Receive Auth Response === - // Server sends auth response with envelope - const authResponseMessageId = '9d867b3a-fe87-40d7-80bf-56b031beb76d'; - final authResponseEnvelope = { - 'messageId': authResponseMessageId, - 'timestamp': '2026-01-13T11:13:10.607327', - 'topic': '/client/$appId/auth', - 'payload': {'success': true, 'message': 'Anmeldung erfolgreich', 'token': null, 'userId': null, 'appUserId': userId}, - 'requiresAck': true, - 'retryCount': 0, - }; - - // Unwrap and process the auth response - final authResult = messageHandler.unwrapEnvelope(authResponseEnvelope); - - expect(authResult, isNotNull); - expect(authResult!.messageId, authResponseMessageId); - expect(authResult.requiresAck, true); - expect(authResult.payload['success'], true); - expect(authResult.payload['appUserId'], userId); - - // Message was processed, now marked as seen - expect(messageHandler.wasProcessed(authResponseMessageId), true); - - // === STEP 3: Auth Response acts as implicit ACK for login === - // The auth response itself confirms the login was received - ackTracker.clearForTopic('/server/login'); - - expect(ackTracker.isPending(loginMessageId), false); - expect(ackTracker.pendingCount, 0); - - // === STEP 4: Send ACK for auth response === - // Simulate sending ACK (callback would be called after processing) - acksSent.add(authResponseMessageId); - - expect(acksSent, contains(authResponseMessageId)); - - // === STEP 5: Verify duplicate auth response is ignored === - final duplicateResult = messageHandler.unwrapEnvelope(authResponseEnvelope); - - expect(duplicateResult, isNull); // Duplicate returns null - // But ACK is still triggered for duplicate (via callback) - expect(acksSent.where((id) => id == authResponseMessageId).length, 2); - }); - - test('login request retry when no ACK received', () async { - final retriedTopics = []; - final timedOutMessages = []; - - final ackTracker = AckTracker( - maxRetries: 4, - onRetry: (topic, payload) async { - retriedTopics.add(topic); - return true; - }, - onTimeout: (messageId, topic) { - timedOutMessages.add(messageId); - }, - ); - - const loginMessageId = 'login-no-ack-test'; - const loginTopic = '/server/login'; - - // Track login message - ackTracker.track(loginMessageId, loginTopic, '{"test": true}'); - - // Simulate 4 retry cycles (5 seconds each in real app) - for (var i = 1; i <= 4; i++) { - await ackTracker.processRetries(); - expect(retriedTopics.length, i); - expect(ackTracker.isPending(loginMessageId), true); - } - - // 5th cycle should timeout - await ackTracker.processRetries(); - - expect(timedOutMessages, contains(loginMessageId)); - expect(ackTracker.isPending(loginMessageId), false); - }); - }); - - group('Job Loading Flow', () { - test('complete job loading flow with request, ACK, and response', () async { - // === SETUP === - final acksSent = []; - - final messageHandler = MessageHandler(maxProcessedIds: 100, onAckRequired: (messageId) => acksSent.add(messageId)); - - final ackTracker = AckTracker(maxRetries: 4, onRetry: (topic, payload) async => true, onTimeout: (messageId, topic) {}); - - const userId = '693fdcc757853e744d2ab0d5'; - - // === STEP 1: Send Jobs Request === - const jobsRequestMessageId = 'eb678b54-2b5e-47f5-9c3e-823d7092c49a'; - const jobsRequestTopic = '/server/$userId/jobs/assigned'; - - ackTracker.track(jobsRequestMessageId, jobsRequestTopic, '{"messageId": "$jobsRequestMessageId"}'); - - expect(ackTracker.isPending(jobsRequestMessageId), true); - - // === STEP 2: Receive ACK for jobs request === - // Server acknowledges our request - final jobsRequestAckEnvelope = { - 'messageId': 'ack-envelope-id-1', - 'timestamp': '2026-01-13T11:13:10.700000', - 'topic': '/client/$userId/ack', - 'payload': {'messageId': jobsRequestMessageId, 'status': 'RECEIVED', 'timestamp': '2026-01-13T11:13:10.700000', 'clientId': 'server'}, - 'requiresAck': false, - 'retryCount': 0, - }; - - // Process ACK envelope - final ackResult = messageHandler.unwrapEnvelope(jobsRequestAckEnvelope); - expect(ackResult, isNotNull); - expect(ackResult!.requiresAck, false); // ACKs don't need ACKs - - // Remove from pending based on ACK content - final ackPayload = ackResult.payload as Map; - final acknowledgedMessageId = ackPayload['messageId'] as String; - ackTracker.acknowledge(acknowledgedMessageId); - - expect(ackTracker.isPending(jobsRequestMessageId), false); - - // === STEP 3: Receive Jobs Response === - const jobsResponseMessageId = 'c92ef207-65a5-4204-802c-99e097f833f5'; - final jobsResponseEnvelope = { - 'messageId': jobsResponseMessageId, - 'timestamp': '2026-01-13T11:13:10.800000', - 'topic': '/client/$userId/jobs', - 'payload': [ - { - 'job': {'jobNumber': 'JOB20260106001', 'status': 'CREATED', 'id': '695ce8faf3fbbd0c2acfdb17', 'pickupCompany': 'cAPPacity GmbH', 'deliveryCompany': 'cAPPacity GmbH', 'pickupCity': 'Taarstedt', 'deliveryCity': 'Taarstedt'}, - 'cargoItems': [ - {'description': 'Europalette', 'quantity': 1, 'id': '695ce8faf3fbbd0c2acfdb18'}, - ], - 'tasks': [ - {'taskType': 'CONFIRMATION', 'taskOrder': 0, 'completed': false, 'buttonText': 'TEST', 'displayName': 'Bestätigung', 'id': '695ce8faf3fbbd0c2acfdb19'}, - ], - }, - ], - 'requiresAck': true, - 'retryCount': 0, - }; - - // Process jobs response - final jobsResult = messageHandler.unwrapEnvelope(jobsResponseEnvelope); - - expect(jobsResult, isNotNull); - expect(jobsResult!.messageId, jobsResponseMessageId); - expect(jobsResult.requiresAck, true); - expect(jobsResult.payload, isList); - expect((jobsResult.payload as List).length, 1); - - // Verify job data - final jobData = (jobsResult.payload as List)[0] as Map; - expect(jobData['job']['jobNumber'], 'JOB20260106001'); - expect(jobData['cargoItems'].length, 1); - expect(jobData['tasks'].length, 1); - - // === STEP 4: Send ACK for jobs response === - acksSent.add(jobsResponseMessageId); - - expect(acksSent, contains(jobsResponseMessageId)); - expect(messageHandler.wasProcessed(jobsResponseMessageId), true); - - // === STEP 5: Verify duplicate jobs response is handled === - final duplicateJobsResult = messageHandler.unwrapEnvelope(jobsResponseEnvelope); - - expect(duplicateJobsResult, isNull); - // Duplicate triggers ACK callback - expect(acksSent.where((id) => id == jobsResponseMessageId).length, 2); - }); - - test('multiple concurrent job requests with independent ACK tracking', () async { - final ackTracker = AckTracker(maxRetries: 4); - - const userId = '693fdcc757853e744d2ab0d5'; - - // Two JobsView instances send requests (as seen in log) - const request1Id = 'eb678b54-2b5e-47f5-9c3e-823d7092c49a'; - const request2Id = '936161ce-6535-4098-9e12-0e7bfb9393ed'; - const topic = '/server/$userId/jobs/assigned'; - - ackTracker.track(request1Id, topic, '{}'); - ackTracker.track(request2Id, topic, '{}'); - - expect(ackTracker.pendingCount, 2); - expect(ackTracker.isPending(request1Id), true); - expect(ackTracker.isPending(request2Id), true); - - // Both ACKs arrive - ackTracker.acknowledge(request1Id); - expect(ackTracker.pendingCount, 1); - expect(ackTracker.isPending(request1Id), false); - expect(ackTracker.isPending(request2Id), true); - - ackTracker.acknowledge(request2Id); - expect(ackTracker.pendingCount, 0); - }); - - test('ping messages without envelope are handled correctly', () { - final messageHandler = MessageHandler(); - - // Ping messages come without envelope wrapper - final pingData = {'type': 'ping', 'timestamp': '2026-01-13T11:13:15.000000'}; - - final result = messageHandler.unwrapEnvelope(pingData); - - expect(result, isNotNull); - expect(result!.messageId, isNull); // No messageId for non-envelope - expect(result.requiresAck, false); // No ACK needed - expect(result.payload, pingData); // Returns data as-is - }); - }); - - group('Message Deduplication Across Sessions', () { - test('prevents reprocessing of already-seen messages', () { - final processedPayloads = []; - final acksSent = []; - - final messageHandler = MessageHandler(maxProcessedIds: 100, onAckRequired: (messageId) => acksSent.add(messageId)); - - const messageId = 'duplicate-test-id'; - final envelope = { - 'messageId': messageId, - 'timestamp': '2026-01-13T11:13:10.000000', - 'topic': '/client/user/jobs', - 'payload': {'jobNumber': 'JOB001'}, - 'requiresAck': true, - }; - - // First processing - final result1 = messageHandler.unwrapEnvelope(envelope); - if (result1 != null) { - processedPayloads.add(result1.payload['jobNumber']); - } - - // Simulate server retry (same message arrives again) - final result2 = messageHandler.unwrapEnvelope(envelope); - if (result2 != null) { - processedPayloads.add(result2.payload['jobNumber']); - } - - // Third arrival - final result3 = messageHandler.unwrapEnvelope(envelope); - if (result3 != null) { - processedPayloads.add(result3.payload['jobNumber']); - } - - // Payload should only be processed once - expect(processedPayloads.length, 1); - expect(processedPayloads.first, 'JOB001'); - - // But ACK should be sent for each duplicate - expect(acksSent.length, 2); // Only duplicates trigger callback - }); - }); - - group('Full Session Flow', () { - test('simulates complete app session from login to job receipt', () async { - // === SETUP === - final processedMessages = {}; - final acksSent = []; - final pendingAcks = []; - - final messageHandler = MessageHandler(maxProcessedIds: 100, onAckRequired: (messageId) => acksSent.add(messageId)); - - final ackTracker = AckTracker(maxRetries: 4, onRetry: (topic, payload) async => true); - - const appId = '410cea21-a3cf-47c3-97ad-b1c01f0bacbb'; - const userId = '693fdcc757853e744d2ab0d5'; - - // === PHASE 1: LOGIN === - const loginMsgId = '5f4907de-684e-4381-a771-bd4d7ecdddbd'; - ackTracker.track(loginMsgId, '/server/login', '{}'); - pendingAcks.add(loginMsgId); - - expect(ackTracker.pendingCount, 1); - - // Auth response arrives - const authMsgId = '9d867b3a-fe87-40d7-80bf-56b031beb76d'; - final authResult = messageHandler.unwrapEnvelope({ - 'messageId': authMsgId, - 'timestamp': '2026-01-13T11:13:10.607327', - 'topic': '/client/$appId/auth', - 'payload': {'success': true, 'appUserId': userId}, - 'requiresAck': true, - }); - - processedMessages['auth'] = authResult!.payload; - acksSent.add(authMsgId); // Send ACK for auth - - // Login implicitly ACKed by auth response - ackTracker.clearForTopic('/server/login'); - pendingAcks.remove(loginMsgId); - - expect(ackTracker.pendingCount, 0); - expect(processedMessages['auth']['success'], true); - - // === PHASE 2: REQUEST JOBS === - const jobsReqMsgId = 'eb678b54-2b5e-47f5-9c3e-823d7092c49a'; - ackTracker.track(jobsReqMsgId, '/server/$userId/jobs/assigned', '{}'); - pendingAcks.add(jobsReqMsgId); - - expect(ackTracker.pendingCount, 1); - - // ACK for jobs request arrives - ackTracker.acknowledge(jobsReqMsgId); - pendingAcks.remove(jobsReqMsgId); - - expect(ackTracker.pendingCount, 0); - - // === PHASE 3: RECEIVE JOBS === - const jobsMsgId = 'c92ef207-65a5-4204-802c-99e097f833f5'; - final jobsResult = messageHandler.unwrapEnvelope({ - 'messageId': jobsMsgId, - 'timestamp': '2026-01-13T11:13:10.800000', - 'topic': '/client/$userId/jobs', - 'payload': [ - { - 'job': {'jobNumber': 'JOB20260106001', 'id': '695ce8faf3fbbd0c2acfdb17'}, - 'tasks': [ - {'taskType': 'CONFIRMATION', 'id': '695ce8faf3fbbd0c2acfdb19'}, - ], - }, - ], - 'requiresAck': true, - }); - - processedMessages['jobs'] = jobsResult!.payload; - acksSent.add(jobsMsgId); // Send ACK for jobs - - // === VERIFY FINAL STATE === - expect(processedMessages.length, 2); - expect(acksSent.length, 2); - expect(pendingAcks, isEmpty); - expect(ackTracker.pendingCount, 0); - - // Verify processed data - expect(processedMessages['auth']['appUserId'], userId); - expect((processedMessages['jobs'] as List).length, 1); - expect((processedMessages['jobs'] as List)[0]['job']['jobNumber'], 'JOB20260106001'); - - // Verify deduplication state - expect(messageHandler.wasProcessed(authMsgId), true); - expect(messageHandler.wasProcessed(jobsMsgId), true); - expect(messageHandler.processedCount, 2); - }); - }); - }); -} diff --git a/app/test/views/cargo_items_view_test.dart b/app/test/views/cargo_items_view_test.dart deleted file mode 100644 index bfa8b67..0000000 --- a/app/test/views/cargo_items_view_test.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:votianlt_app/cargo_items_view.dart'; -import 'package:votianlt_app/models/delivery_station.dart'; -import 'package:votianlt_app/models/tasks/confirmation_task.dart'; - -void main() { - group('deliveryStationCardBackgroundColor', () { - DeliveryStation buildStation(List tasks) { - return DeliveryStation( - stationOrder: 0, - company: 'ACME', - salutation: null, - firstName: 'Max', - lastName: 'Mustermann', - phone: '12345', - street: 'Musterstrasse', - houseNumber: '1', - addressAddition: '', - zip: '12345', - city: 'Berlin', - deliveryDate: '2026-03-10', - deliveryTime: '10:00', - tasks: tasks, - ); - } - - ConfirmationTask buildTask(String id, {bool completed = false}) { - return ConfirmationTask( - id: id, - jobId: 'job-1', - buttonText: 'Bestaetigen', - completed: completed, - ); - } - - test('returns light green when all station tasks are completed', () { - final station = buildStation([ - buildTask('task-1', completed: true), - buildTask('task-2', completed: true), - ]); - - final color = deliveryStationCardBackgroundColor(station, const {}); - - expect(color, Colors.green[50]); - }); - - test('returns null when only some tasks are completed', () { - final station = buildStation([ - buildTask('task-1', completed: true), - buildTask('task-2'), - ]); - - final color = deliveryStationCardBackgroundColor(station, const {}); - - expect(color, isNull); - }); - - test('returns null when no tasks are completed', () { - final station = buildStation([buildTask('task-1'), buildTask('task-2')]); - - final color = deliveryStationCardBackgroundColor(station, const {}); - - expect(color, isNull); - }); - - test('returns null when station has no tasks', () { - final station = buildStation(const []); - - final color = deliveryStationCardBackgroundColor(station, const {}); - - expect(color, isNull); - }); - - test('prefers local task status over incomplete task payload', () { - final station = buildStation([buildTask('task-1'), buildTask('task-2')]); - - final color = deliveryStationCardBackgroundColor(station, const { - 'task-1': true, - 'task-2': true, - }); - - expect(color, Colors.green[50]); - }); - }); -} diff --git a/backend/src/main/java/de/assecutor/votianlt/service/DemoModeService.java b/backend/src/main/java/de/assecutor/votianlt/service/DemoModeService.java index 4b70962..f432a16 100644 --- a/backend/src/main/java/de/assecutor/votianlt/service/DemoModeService.java +++ b/backend/src/main/java/de/assecutor/votianlt/service/DemoModeService.java @@ -11,7 +11,6 @@ import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.JobServiceSelection; import de.assecutor.votianlt.model.JobStatus; import de.assecutor.votianlt.model.Language; -import de.assecutor.votianlt.model.LocationPosition; import de.assecutor.votianlt.model.Photo; import de.assecutor.votianlt.model.Service; import de.assecutor.votianlt.model.Signature;