mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-10-09 19:55:05 +00:00
chore: Initial commit
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPovRHpejR4zRxuhjY4Qs3
This commit is contained in:
35
test/models/channel_test.dart
Normal file
35
test/models/channel_test.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/models/channel.dart';
|
||||
|
||||
void main() {
|
||||
test('hash channels expose deterministic psk_base64', () {
|
||||
final channel = Channel.create(index: 3, name: '#ops');
|
||||
|
||||
expect(channel.isHashChannel, isTrue);
|
||||
expect(channel.pskBase64, 'O2RN43fDLHh5NgWiWqkVvw==');
|
||||
expect(
|
||||
Channel.pskBase64ForHashChannelName('#ops'),
|
||||
'O2RN43fDLHh5NgWiWqkVvw==',
|
||||
);
|
||||
});
|
||||
|
||||
test('normal channels reject derived hashtag psk export helper', () {
|
||||
expect(
|
||||
() => Channel.pskBase64ForHashChannelName('ops'),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
|
||||
test('normal channels still expose their stored psk_base64', () {
|
||||
final channel = Channel.create(
|
||||
index: 4,
|
||||
name: 'ops',
|
||||
explicitSecret: Uint8List.fromList(List<int>.generate(16, (i) => i)),
|
||||
);
|
||||
|
||||
expect(channel.isHashChannel, isFalse);
|
||||
expect(channel.pskBase64, 'AAECAwQFBgcICQoLDA0ODw==');
|
||||
});
|
||||
}
|
||||
172
test/models/contact_route_codec_test.dart
Normal file
172
test/models/contact_route_codec_test.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
|
||||
void main() {
|
||||
Contact buildContact({
|
||||
required int signedPathLen,
|
||||
required Uint8List outPath,
|
||||
}) {
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(List<int>.generate(32, (index) => index)),
|
||||
type: ContactType.chat,
|
||||
flags: 0,
|
||||
outPathLen: signedPathLen,
|
||||
outPath: outPath,
|
||||
advName: 'Route Contact',
|
||||
lastAdvert: 0,
|
||||
advLat: 0,
|
||||
advLon: 0,
|
||||
lastMod: 0,
|
||||
);
|
||||
}
|
||||
|
||||
group('ContactRouteCodec.parse', () {
|
||||
test('parses 1-byte hop routes', () {
|
||||
final route = ContactRouteCodec.parse('AA,BB,CC');
|
||||
|
||||
expect(route.hashSize, 1);
|
||||
expect(route.hopCount, 3);
|
||||
expect(route.encodedPathLen, 0x03);
|
||||
expect(route.canonicalText, 'AA,BB,CC');
|
||||
expect(route.pathBytes, [0xAA, 0xBB, 0xCC]);
|
||||
});
|
||||
|
||||
test('parses 2-byte hop routes', () {
|
||||
final route = ContactRouteCodec.parse('AABB,CCDD');
|
||||
|
||||
expect(route.hashSize, 2);
|
||||
expect(route.hopCount, 2);
|
||||
expect(route.encodedPathLen, 0x42);
|
||||
expect(route.canonicalText, 'AABB,CCDD');
|
||||
expect(route.pathBytes, [0xAA, 0xBB, 0xCC, 0xDD]);
|
||||
});
|
||||
|
||||
test('parses 3-byte hop routes', () {
|
||||
final route = ContactRouteCodec.parse('AABBCC,DDEEFF');
|
||||
|
||||
expect(route.hashSize, 3);
|
||||
expect(route.hopCount, 2);
|
||||
expect(route.encodedPathLen, 0x82);
|
||||
expect(route.signedEncodedPathLen, -126);
|
||||
expect(route.pathBytes, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
||||
});
|
||||
|
||||
test('accepts colon-separated hops and normalizes output', () {
|
||||
final route = ContactRouteCodec.parse('AA:BB,CC:DD');
|
||||
|
||||
expect(route.hashSize, 2);
|
||||
expect(route.canonicalText, 'AABB,CCDD');
|
||||
});
|
||||
|
||||
test('rejects mixed hop widths', () {
|
||||
expect(
|
||||
() => ContactRouteCodec.parse('AA,AABB'),
|
||||
throwsA(isA<ContactRouteFormatException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects routes that do not match the configured hash size', () {
|
||||
expect(
|
||||
() => ContactRouteCodec.parse('AABB,CCDD', expectedHashSize: 1),
|
||||
throwsA(isA<ContactRouteFormatException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects invalid tokens', () {
|
||||
expect(
|
||||
() => ContactRouteCodec.parse('AA,XYZ'),
|
||||
throwsA(isA<ContactRouteFormatException>()),
|
||||
);
|
||||
expect(
|
||||
() => ContactRouteCodec.parse('AAA'),
|
||||
throwsA(isA<ContactRouteFormatException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects routes over 64 bytes', () {
|
||||
final tooLong = List.filled(22, 'AABBCC').join(',');
|
||||
expect(
|
||||
() => ContactRouteCodec.parse(tooLong),
|
||||
throwsA(isA<ContactRouteFormatException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Contact route helpers', () {
|
||||
test('interprets signed 3-byte descriptors as valid routes', () {
|
||||
final outPath = Uint8List(64)
|
||||
..setRange(0, 6, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
||||
final contact = buildContact(signedPathLen: -126, outPath: outPath);
|
||||
|
||||
expect(contact.routeHasPath, isTrue);
|
||||
expect(contact.routeHashSize, 3);
|
||||
expect(contact.routeHopCount, 2);
|
||||
expect(contact.routeCanonicalText, 'AABBCC,DDEEFF');
|
||||
expect(contact.routeSupportsLegacyRawTransport, isTrue);
|
||||
});
|
||||
|
||||
test('treats -1 as unknown route', () {
|
||||
final contact = buildContact(signedPathLen: -1, outPath: Uint8List(0));
|
||||
|
||||
expect(contact.routeHasPath, isFalse);
|
||||
expect(contact.routeSummary, 'Flood/Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
group('ContactRouteCodec.toLegacyRawPath', () {
|
||||
test('zero-hop direct (0x40) → empty legacy path', () {
|
||||
final legacy = ContactRouteCodec.toLegacyRawPath(0x40, Uint8List(0));
|
||||
|
||||
expect(legacy.length, 0);
|
||||
expect(legacy.path, isEmpty);
|
||||
});
|
||||
|
||||
test('1-hop 2-byte hash (0x41) → first byte of the hash', () {
|
||||
final legacy = ContactRouteCodec.toLegacyRawPath(
|
||||
0x41,
|
||||
Uint8List.fromList([0x02, 0x62]),
|
||||
);
|
||||
|
||||
expect(legacy.length, 1);
|
||||
expect(legacy.path, [0x02]);
|
||||
});
|
||||
|
||||
test('2-hop 2-byte hash (0x42) → first byte of each hash', () {
|
||||
final legacy = ContactRouteCodec.toLegacyRawPath(
|
||||
0x42,
|
||||
Uint8List.fromList([0xAA, 0xBB, 0xCC, 0xDD]),
|
||||
);
|
||||
|
||||
expect(legacy.length, 2);
|
||||
expect(legacy.path, [0xAA, 0xCC]);
|
||||
});
|
||||
|
||||
test('signed 3-byte descriptor (-126 / 0x82) down-samples each hop', () {
|
||||
final outPath = Uint8List(64)
|
||||
..setRange(0, 6, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
||||
final legacy = ContactRouteCodec.toLegacyRawPath(-126, outPath);
|
||||
|
||||
expect(legacy.length, 2);
|
||||
expect(legacy.path, [0xAA, 0xDD]);
|
||||
});
|
||||
|
||||
test('legacy 1-byte hops (< 0x40) pass through trimmed', () {
|
||||
final legacy = ContactRouteCodec.toLegacyRawPath(
|
||||
0x03,
|
||||
Uint8List.fromList([0xAA, 0xBB, 0xCC, 0x00, 0x00]),
|
||||
);
|
||||
|
||||
expect(legacy.length, 3);
|
||||
expect(legacy.path, [0xAA, 0xBB, 0xCC]);
|
||||
});
|
||||
|
||||
test('unknown descriptor (0xFF) collapses to direct', () {
|
||||
final legacy = ContactRouteCodec.toLegacyRawPath(-1, Uint8List(0));
|
||||
|
||||
expect(legacy.length, 0);
|
||||
expect(legacy.path, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
37
test/models/custom_map_config_test.dart
Normal file
37
test/models/custom_map_config_test.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:meshcore_sar_app/models/custom_map_config.dart';
|
||||
|
||||
void main() {
|
||||
test('displayBounds stays within valid LatLng limits for tall images', () {
|
||||
const config = CustomMapConfig(
|
||||
filePath: '/tmp/map.png',
|
||||
displayName: 'Tall Map',
|
||||
mapId: 'map-id',
|
||||
imageWidth: 2400,
|
||||
imageHeight: 3213,
|
||||
);
|
||||
|
||||
expect(config.displayBounds.north, lessThanOrEqualTo(90));
|
||||
expect(config.displayBounds.south, greaterThanOrEqualTo(-90));
|
||||
expect(config.displayBounds.east, lessThanOrEqualTo(180));
|
||||
expect(config.displayBounds.west, greaterThanOrEqualTo(-180));
|
||||
});
|
||||
|
||||
test('display point conversion preserves stored coordinates', () {
|
||||
const config = CustomMapConfig(
|
||||
filePath: '/tmp/map.png',
|
||||
displayName: 'Tall Map',
|
||||
mapId: 'map-id',
|
||||
imageWidth: 2400,
|
||||
imageHeight: 3213,
|
||||
);
|
||||
const storedPoint = LatLng(1600, 1200);
|
||||
|
||||
final displayPoint = config.toDisplayPoint(storedPoint);
|
||||
final roundTrip = config.fromDisplayPoint(displayPoint);
|
||||
|
||||
expect(roundTrip.latitude, closeTo(storedPoint.latitude, 0.001));
|
||||
expect(roundTrip.longitude, closeTo(storedPoint.longitude, 0.001));
|
||||
});
|
||||
}
|
||||
50
test/models/message_reception_details_test.dart
Normal file
50
test/models/message_reception_details_test.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/models/message_reception_details.dart';
|
||||
|
||||
void main() {
|
||||
test('drops impossible transmit estimate for received messages', () {
|
||||
expect(
|
||||
sanitizeEstimatedTransmitMs(
|
||||
estimatedTransmitMs: 16 * 60 * 1000 + 54 * 1000,
|
||||
senderToReceiptMs: 4200,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'keeps close transmit estimate despite second-level timestamp rounding',
|
||||
() {
|
||||
expect(
|
||||
sanitizeEstimatedTransmitMs(
|
||||
estimatedTransmitMs: 1800,
|
||||
senderToReceiptMs: 900,
|
||||
),
|
||||
1800,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('round trips reception details json', () {
|
||||
final details = MessageReceptionDetails(
|
||||
capturedAt: DateTime.fromMillisecondsSinceEpoch(1700000000000),
|
||||
packetLoggedAt: DateTime.fromMillisecondsSinceEpoch(1700000000500),
|
||||
rssiDbm: -92,
|
||||
snrDb: 7.5,
|
||||
pathBytes: const [0xAA, 0xBB, 0xCC],
|
||||
senderToReceiptMs: 4200,
|
||||
estimatedTransmitMs: 1800,
|
||||
postTransmitDelayMs: 2400,
|
||||
);
|
||||
|
||||
final decoded = MessageReceptionDetails.fromJson(details.toJson());
|
||||
|
||||
expect(decoded, isNotNull);
|
||||
expect(decoded!.rssiDbm, -92);
|
||||
expect(decoded.snrDb, 7.5);
|
||||
expect(decoded.pathBytesHex, 'aa:bb:cc');
|
||||
expect(decoded.senderToReceiptMs, 4200);
|
||||
expect(decoded.estimatedTransmitMs, 1800);
|
||||
expect(decoded.postTransmitDelayMs, 2400);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user