refactor: remove obsolete test files and clean up manifest
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
<!-- GPS Location permissions -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||
<application
|
||||
android:label="votianlt_app"
|
||||
android:name="${applicationName}"
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:votianlt_app/models/acknowledgment_message.dart';
|
||||
|
||||
void main() {
|
||||
group('AcknowledgmentMessage', () {
|
||||
group('fromJson', () {
|
||||
test('parses all required fields correctly', () {
|
||||
final json = {
|
||||
'messageId': 'msg-123',
|
||||
'status': 'RECEIVED',
|
||||
'timestamp': '2024-01-15T10:30:00.000Z',
|
||||
};
|
||||
|
||||
final ack = AcknowledgmentMessage.fromJson(json);
|
||||
|
||||
expect(ack.messageId, 'msg-123');
|
||||
expect(ack.status, AcknowledgmentStatus.received);
|
||||
expect(ack.timestamp, DateTime.utc(2024, 1, 15, 10, 30, 0));
|
||||
expect(ack.errorMessage, isNull);
|
||||
});
|
||||
|
||||
test('parses errorMessage when present', () {
|
||||
final json = {
|
||||
'messageId': 'msg-123',
|
||||
'status': 'FAILED',
|
||||
'timestamp': '2024-01-15T10:30:00.000Z',
|
||||
'errorMessage': 'Connection timeout',
|
||||
};
|
||||
|
||||
final ack = AcknowledgmentMessage.fromJson(json);
|
||||
|
||||
expect(ack.status, AcknowledgmentStatus.failed);
|
||||
expect(ack.errorMessage, 'Connection timeout');
|
||||
});
|
||||
|
||||
test('parses PROCESSED status', () {
|
||||
final json = {
|
||||
'messageId': 'msg-123',
|
||||
'status': 'PROCESSED',
|
||||
'timestamp': '2024-01-15T10:30:00.000Z',
|
||||
};
|
||||
|
||||
final ack = AcknowledgmentMessage.fromJson(json);
|
||||
|
||||
expect(ack.status, AcknowledgmentStatus.processed);
|
||||
});
|
||||
});
|
||||
|
||||
group('toJson', () {
|
||||
test('serializes all fields correctly', () {
|
||||
final ack = AcknowledgmentMessage(
|
||||
messageId: 'msg-123',
|
||||
status: AcknowledgmentStatus.received,
|
||||
timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0),
|
||||
);
|
||||
|
||||
final json = ack.toJson();
|
||||
|
||||
expect(json['messageId'], 'msg-123');
|
||||
expect(json['status'], 'RECEIVED');
|
||||
expect(json['timestamp'], '2024-01-15T10:30:00.000Z');
|
||||
expect(json.containsKey('errorMessage'), false);
|
||||
});
|
||||
|
||||
test('includes errorMessage when present', () {
|
||||
final ack = AcknowledgmentMessage(
|
||||
messageId: 'msg-123',
|
||||
status: AcknowledgmentStatus.failed,
|
||||
timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0),
|
||||
errorMessage: 'Processing error',
|
||||
);
|
||||
|
||||
final json = ack.toJson();
|
||||
|
||||
expect(json['errorMessage'], 'Processing error');
|
||||
});
|
||||
});
|
||||
|
||||
group('fromJson/toJson roundtrip', () {
|
||||
test('preserves all data through serialization', () {
|
||||
final original = AcknowledgmentMessage(
|
||||
messageId: 'roundtrip-msg',
|
||||
status: AcknowledgmentStatus.processed,
|
||||
timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0),
|
||||
);
|
||||
|
||||
final json = original.toJson();
|
||||
final restored = AcknowledgmentMessage.fromJson(json);
|
||||
|
||||
expect(restored.messageId, original.messageId);
|
||||
expect(restored.status, original.status);
|
||||
expect(restored.timestamp, original.timestamp);
|
||||
expect(restored.errorMessage, original.errorMessage);
|
||||
});
|
||||
|
||||
test('preserves errorMessage through serialization', () {
|
||||
final original = AcknowledgmentMessage(
|
||||
messageId: 'error-msg',
|
||||
status: AcknowledgmentStatus.failed,
|
||||
timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0),
|
||||
errorMessage: 'Something went wrong',
|
||||
);
|
||||
|
||||
final json = original.toJson();
|
||||
final restored = AcknowledgmentMessage.fromJson(json);
|
||||
|
||||
expect(restored.errorMessage, 'Something went wrong');
|
||||
});
|
||||
});
|
||||
|
||||
group('toString', () {
|
||||
test('returns readable representation', () {
|
||||
final ack = AcknowledgmentMessage(
|
||||
messageId: 'msg-123',
|
||||
status: AcknowledgmentStatus.received,
|
||||
timestamp: DateTime.utc(2024, 1, 15, 10, 30, 0),
|
||||
);
|
||||
|
||||
final str = ack.toString();
|
||||
|
||||
expect(str, contains('msg-123'));
|
||||
expect(str, contains('RECEIVED'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('AcknowledgmentStatus', () {
|
||||
group('fromString', () {
|
||||
test('parses RECEIVED', () {
|
||||
expect(
|
||||
AcknowledgmentStatus.fromString('RECEIVED'),
|
||||
AcknowledgmentStatus.received,
|
||||
);
|
||||
});
|
||||
|
||||
test('parses PROCESSED', () {
|
||||
expect(
|
||||
AcknowledgmentStatus.fromString('PROCESSED'),
|
||||
AcknowledgmentStatus.processed,
|
||||
);
|
||||
});
|
||||
|
||||
test('parses FAILED', () {
|
||||
expect(
|
||||
AcknowledgmentStatus.fromString('FAILED'),
|
||||
AcknowledgmentStatus.failed,
|
||||
);
|
||||
});
|
||||
|
||||
test('handles lowercase input', () {
|
||||
expect(
|
||||
AcknowledgmentStatus.fromString('received'),
|
||||
AcknowledgmentStatus.received,
|
||||
);
|
||||
expect(
|
||||
AcknowledgmentStatus.fromString('processed'),
|
||||
AcknowledgmentStatus.processed,
|
||||
);
|
||||
expect(
|
||||
AcknowledgmentStatus.fromString('failed'),
|
||||
AcknowledgmentStatus.failed,
|
||||
);
|
||||
});
|
||||
|
||||
test('defaults to received for unknown values', () {
|
||||
expect(
|
||||
AcknowledgmentStatus.fromString('UNKNOWN'),
|
||||
AcknowledgmentStatus.received,
|
||||
);
|
||||
expect(
|
||||
AcknowledgmentStatus.fromString(''),
|
||||
AcknowledgmentStatus.received,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('toString', () {
|
||||
test('returns RECEIVED for received', () {
|
||||
expect(AcknowledgmentStatus.received.toString(), 'RECEIVED');
|
||||
});
|
||||
|
||||
test('returns PROCESSED for processed', () {
|
||||
expect(AcknowledgmentStatus.processed.toString(), 'PROCESSED');
|
||||
});
|
||||
|
||||
test('returns FAILED for failed', () {
|
||||
expect(AcknowledgmentStatus.failed.toString(), 'FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
group('fromString/toString roundtrip', () {
|
||||
test('preserves status through conversion', () {
|
||||
for (final status in AcknowledgmentStatus.values) {
|
||||
final str = status.toString();
|
||||
final restored = AcknowledgmentStatus.fromString(str);
|
||||
expect(restored, status);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,883 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:votianlt_app/models/job.dart';
|
||||
import 'package:votianlt_app/models/cargo_item.dart';
|
||||
import 'package:votianlt_app/models/task.dart';
|
||||
import 'package:votianlt_app/models/tasks/confirmation_task.dart';
|
||||
import 'package:votianlt_app/models/tasks/photo_task.dart';
|
||||
import 'package:votianlt_app/models/tasks/signature_task.dart';
|
||||
import 'package:votianlt_app/models/tasks/barcode_task.dart';
|
||||
import 'package:votianlt_app/models/tasks/todolist_task.dart';
|
||||
import 'package:votianlt_app/models/tasks/comment_task.dart';
|
||||
import 'package:votianlt_app/models/tasks/generic_task.dart';
|
||||
|
||||
/// Test data based on job_json.md documentation from
|
||||
/// https://www.appcreation.de/download/job_json.md
|
||||
void main() {
|
||||
// Complete job JSON according to documentation
|
||||
final Map<String, dynamic> 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<String, dynamic>.from(completeJobJson);
|
||||
final jobData = Map<String, dynamic>.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': <dynamic>[],
|
||||
'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': <dynamic>[],
|
||||
'tasks': <dynamic>[],
|
||||
};
|
||||
|
||||
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<ConfirmationTask>());
|
||||
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<SignatureTask>());
|
||||
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<PhotoTask>());
|
||||
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<BarcodeTask>());
|
||||
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<TodoListTask>());
|
||||
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<CommentTask>());
|
||||
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<GenericTask>());
|
||||
});
|
||||
|
||||
test('falls back to GenericTask when taskType is missing', () {
|
||||
final noTypeTaskJson = {
|
||||
'id': {'timestamp': 1705312298},
|
||||
'jobId': {'timestamp': 1705312200},
|
||||
'completed': false,
|
||||
'taskOrder': 98,
|
||||
'taskSpecificData': <String, dynamic>{},
|
||||
};
|
||||
|
||||
final task = Task.fromJson(noTypeTaskJson);
|
||||
|
||||
expect(task, isA<GenericTask>());
|
||||
});
|
||||
|
||||
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<String, dynamic>.from(
|
||||
completeJobJson,
|
||||
);
|
||||
jsonWithEmptyCargoItems['cargoItems'] = <dynamic>[];
|
||||
|
||||
final job = Job.fromJson(jsonWithEmptyCargoItems);
|
||||
|
||||
expect(job.cargoItems, isEmpty);
|
||||
});
|
||||
|
||||
test('handles empty tasks array', () {
|
||||
final jsonWithEmptyTasks = Map<String, dynamic>.from(completeJobJson);
|
||||
jsonWithEmptyTasks['tasks'] = <dynamic>[];
|
||||
|
||||
final job = Job.fromJson(jsonWithEmptyTasks);
|
||||
|
||||
expect(job.tasks, isEmpty);
|
||||
});
|
||||
|
||||
test('handles missing optional fields with defaults', () {
|
||||
final minimalJson = {
|
||||
'job': {'jobNumber': 'JOB-MIN-001'},
|
||||
'cargoItems': <dynamic>[],
|
||||
'tasks': <dynamic>[],
|
||||
};
|
||||
|
||||
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': <dynamic>[],
|
||||
'tasks': <dynamic>[],
|
||||
};
|
||||
|
||||
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': <dynamic>[],
|
||||
'tasks': <dynamic>[],
|
||||
};
|
||||
|
||||
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': <dynamic>[],
|
||||
'tasks': <dynamic>[],
|
||||
};
|
||||
|
||||
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': <dynamic>[],
|
||||
'tasks': <dynamic>[],
|
||||
};
|
||||
|
||||
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': <dynamic>[],
|
||||
'tasks': <dynamic>[],
|
||||
};
|
||||
|
||||
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': <dynamic>[],
|
||||
'tasks': <dynamic>[],
|
||||
};
|
||||
|
||||
final job = Job.fromJson(json).normalized();
|
||||
|
||||
expect(job.pickupSalutation, '');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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<Map<String, String>> retryCalls;
|
||||
late List<Map<String, String>> 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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<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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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 = <String>[];
|
||||
final retriedMessages = <String>[];
|
||||
|
||||
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 = <String>[];
|
||||
final timedOutMessages = <String>[];
|
||||
|
||||
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 = <String>[];
|
||||
|
||||
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<String, dynamic>;
|
||||
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<String, dynamic>;
|
||||
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 = <String>[];
|
||||
final acksSent = <String>[];
|
||||
|
||||
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 = <String, dynamic>{};
|
||||
final acksSent = <String>[];
|
||||
final pendingAcks = <String>[];
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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<ConfirmationTask> 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]);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user