You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
6.7 KiB
222 lines
6.7 KiB
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:meshcore_open/helpers/pocketmesh_reaction.dart';
|
|
|
|
void main() {
|
|
group('PocketMeshReaction', () {
|
|
group('computeHash', () {
|
|
test('reproduces the hash from the live capture (GH #378)', () {
|
|
// Captured 2026-07-24: a MeshCore One peer reacted to this message and
|
|
// sent the hash below. If this ever fails, our hash input drifted from
|
|
// theirs and every incoming reaction will silently stop matching.
|
|
expect(
|
|
PocketMeshReaction.computeHash(
|
|
"It's a hash from MeshCore One for emojis. "
|
|
'Looking at how to render better.',
|
|
1784871761,
|
|
),
|
|
'dyps6yf0',
|
|
);
|
|
});
|
|
|
|
test('is stable for the same input', () {
|
|
final a = PocketMeshReaction.computeHash('Hello', 1704067200);
|
|
final b = PocketMeshReaction.computeHash('Hello', 1704067200);
|
|
expect(a, b);
|
|
expect(a.length, 8);
|
|
});
|
|
|
|
test('the timestamp is part of the hash', () {
|
|
expect(
|
|
PocketMeshReaction.computeHash('Hello', 1704067200),
|
|
isNot(PocketMeshReaction.computeHash('Hello', 1704067201)),
|
|
);
|
|
});
|
|
|
|
test('handles a timestamp past the 32-bit signed boundary', () {
|
|
// 2^31 + 1. A signed 32-bit shift on the web target would misencode it.
|
|
final hash = PocketMeshReaction.computeHash('Hello', 2147483649);
|
|
expect(hash.length, 8);
|
|
expect(hash, isNot(PocketMeshReaction.computeHash('Hello', 1)));
|
|
});
|
|
|
|
test('hashes the body, not the sender-prefixed wire text', () {
|
|
const body = 'Test';
|
|
expect(
|
|
PocketMeshReaction.computeHash(body, 1784871761),
|
|
isNot(PocketMeshReaction.computeHash('Someone: $body', 1784871761)),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('parse, channel form', () {
|
|
test('parses the exact payload from the live capture', () {
|
|
final parsed = PocketMeshReaction.parse(
|
|
'\u{1F44D}@[Strycher WM\u{1F6F0}\u{FE0F}]\ndyps6yf0',
|
|
isDm: false,
|
|
);
|
|
|
|
expect(parsed, isNotNull);
|
|
expect(parsed!.emoji, '\u{1F44D}');
|
|
expect(parsed.targetSenderName, 'Strycher WM\u{1F6F0}\u{FE0F}');
|
|
expect(parsed.targetHash, 'dyps6yf0');
|
|
});
|
|
|
|
test('normalises an uppercase hash', () {
|
|
final parsed = PocketMeshReaction.parse(
|
|
'\u{1F44D}@[Node]\nDYPS6YF0',
|
|
isDm: false,
|
|
);
|
|
expect(parsed?.targetHash, 'dyps6yf0');
|
|
});
|
|
|
|
test('rejects a missing sender block', () {
|
|
expect(
|
|
PocketMeshReaction.parse('\u{1F44D}\ndyps6yf0', isDm: false),
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
test('rejects an unterminated sender block', () {
|
|
expect(
|
|
PocketMeshReaction.parse('\u{1F44D}@[Node\ndyps6yf0', isDm: false),
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
test('rejects an empty sender', () {
|
|
expect(
|
|
PocketMeshReaction.parse('\u{1F44D}@[]\ndyps6yf0', isDm: false),
|
|
isNull,
|
|
);
|
|
});
|
|
});
|
|
|
|
group('parse, direct form', () {
|
|
test('parses emoji and hash', () {
|
|
final parsed = PocketMeshReaction.parse(
|
|
'\u{1F44D}\ndyps6yf0',
|
|
isDm: true,
|
|
);
|
|
|
|
expect(parsed, isNotNull);
|
|
expect(parsed!.emoji, '\u{1F44D}');
|
|
expect(parsed.targetSenderName, isNull);
|
|
expect(parsed.targetHash, 'dyps6yf0');
|
|
});
|
|
|
|
test('rejects the channel form', () {
|
|
expect(
|
|
PocketMeshReaction.parse('\u{1F44D}@[Node]\ndyps6yf0', isDm: true),
|
|
isNull,
|
|
);
|
|
});
|
|
});
|
|
|
|
group('parse rejects ordinary messages', () {
|
|
test('no newline', () {
|
|
expect(
|
|
PocketMeshReaction.parse('\u{1F44D}dyps6yf0', isDm: true),
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
test('tail is not 8 characters', () {
|
|
expect(
|
|
PocketMeshReaction.parse('\u{1F44D}\ndyps6yf', isDm: true),
|
|
isNull,
|
|
);
|
|
expect(
|
|
PocketMeshReaction.parse('\u{1F44D}\ndyps6yf00', isDm: true),
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
test('tail holds a character outside the alphabet', () {
|
|
expect(
|
|
PocketMeshReaction.parse('\u{1F44D}\ndypsuyf0', isDm: true),
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
test('nothing before the newline', () {
|
|
expect(PocketMeshReaction.parse('\ndyps6yf0', isDm: true), isNull);
|
|
});
|
|
|
|
test('a sentence starting with an arrow is not a reaction', () {
|
|
// Arrows carry the Unicode Emoji property but are ordinary punctuation.
|
|
// A live capture from this mesh had U+2192 mid-sentence in a normal
|
|
// channel message, so the range is excluded outright.
|
|
expect(
|
|
PocketMeshReaction.parse(
|
|
'\u{2190} Turn left at the fork\ndyps6yf0',
|
|
isDm: true,
|
|
),
|
|
isNull,
|
|
);
|
|
expect(
|
|
PocketMeshReaction.parse('\u{2192}\ndyps6yf0', isDm: true),
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
test('a long run of text after a real emoji is not a reaction', () {
|
|
// The swallow risk the rune cap exists for: without it the whole body
|
|
// would become the reaction "emoji".
|
|
expect(
|
|
PocketMeshReaction.parse(
|
|
'\u{1F44D} thanks, that fixed it for me\ndyps6yf0',
|
|
isDm: true,
|
|
),
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
test('accepts a multi-rune emoji sequence', () {
|
|
// Skin tone modifier, then a ZWJ family sequence.
|
|
expect(
|
|
PocketMeshReaction.parse('\u{1F44D}\u{1F3FD}\ndyps6yf0', isDm: true),
|
|
isNotNull,
|
|
);
|
|
expect(
|
|
PocketMeshReaction.parse(
|
|
'\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\ndyps6yf0',
|
|
isDm: true,
|
|
),
|
|
isNotNull,
|
|
);
|
|
});
|
|
|
|
test('the leading character is not an emoji', () {
|
|
// The realistic false positive: a two-line message whose last line
|
|
// happens to be eight Crockford characters.
|
|
expect(
|
|
PocketMeshReaction.parse('see below\ndyps6yf0', isDm: true),
|
|
isNull,
|
|
);
|
|
expect(PocketMeshReaction.parse('A\ndyps6yf0', isDm: true), isNull);
|
|
});
|
|
|
|
test('our own reaction format is not mistaken for theirs', () {
|
|
expect(PocketMeshReaction.parse('r:3f2a:05', isDm: true), isNull);
|
|
});
|
|
|
|
test('accepts the emoji the picker offers as quick reactions', () {
|
|
for (final emoji in [
|
|
'\u{1F44D}',
|
|
'\u{2764}\u{FE0F}',
|
|
'\u{1F602}',
|
|
'\u{1F389}',
|
|
'\u{1F44F}',
|
|
'\u{1F525}',
|
|
]) {
|
|
expect(
|
|
PocketMeshReaction.parse('$emoji\ndyps6yf0', isDm: true),
|
|
isNotNull,
|
|
reason: 'should accept $emoji',
|
|
);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|