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:
Janez Troha
2026-09-04 16:53:52 +02:00
commit 7924803351
516 changed files with 207265 additions and 0 deletions

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

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

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

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