feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled

Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
2026-08-13 18:34:25 +08:00
parent 61c3fa1df9
commit 9dfa06ffee
3785 changed files with 1085458 additions and 2 deletions
@@ -0,0 +1,469 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:buzz/shared/relay/animated_image_sanitizer.dart';
void main() {
test('strips APNG metadata without changing animation chunks', () {
final clean = _animatedPng(metadata: false);
final dirty = _animatedPng(metadata: true)
..addAll(utf8.encode('trailing metadata'));
expect(
sanitizeAnimatedImageForUpload(Uint8List.fromList(dirty), 'image/png'),
clean,
);
});
test('strips identity APNG orientation but rejects display transforms', () {
final clean = _animatedPng(metadata: false);
expect(
sanitizeAnimatedImageForUpload(
Uint8List.fromList(_animatedPng(metadata: false, orientation: 1)),
'image/png',
),
clean,
);
for (final endian in [Endian.little, Endian.big]) {
expect(
() => sanitizeAnimatedImageForUpload(
Uint8List.fromList(
_animatedPng(
metadata: false,
orientation: 6,
orientationEndian: endian,
),
),
'image/png',
),
throwsA(
isA<FormatException>().having(
(error) => error.message,
'message',
contains('orientation'),
),
),
);
}
});
test('rejects APNG ICC profiles that affect color rendering', () {
expect(
() => sanitizeAnimatedImageForUpload(
Uint8List.fromList(_animatedPng(metadata: false, iccProfile: true)),
'image/png',
),
throwsA(
isA<FormatException>().having(
(error) => error.message,
'message',
contains('ICC profile'),
),
),
);
});
test('strips animated WebP metadata and clears metadata flags', () {
final clean = _animatedWebp(metadata: false);
final dirty = _animatedWebp(metadata: true)
..addAll(utf8.encode('trailing metadata'));
expect(
sanitizeAnimatedImageForUpload(Uint8List.fromList(dirty), 'image/webp'),
clean,
);
});
test('strips identity WebP orientation but rejects display transforms', () {
final clean = _animatedWebp(metadata: false);
expect(
sanitizeAnimatedImageForUpload(
Uint8List.fromList(_animatedWebp(metadata: false, orientation: 1)),
'image/webp',
),
clean,
);
for (final endian in [Endian.little, Endian.big]) {
expect(
() => sanitizeAnimatedImageForUpload(
Uint8List.fromList(
_animatedWebp(
metadata: false,
orientation: 6,
orientationEndian: endian,
),
),
'image/webp',
),
throwsA(
isA<FormatException>().having(
(error) => error.message,
'message',
contains('orientation'),
),
),
);
}
});
test('rejects animated WebP ICC profiles that affect color rendering', () {
expect(
() => sanitizeAnimatedImageForUpload(
Uint8List.fromList(_animatedWebp(metadata: false, iccProfile: true)),
'image/webp',
),
throwsA(
isA<FormatException>().having(
(error) => error.message,
'message',
contains('ICC profile'),
),
),
);
});
test('removes metadata chunks nested inside animated WebP frames', () {
expect(
sanitizeAnimatedImageForUpload(
Uint8List.fromList(
_animatedWebp(metadata: false, nestedMetadata: true),
),
'image/webp',
),
_animatedWebp(metadata: false),
);
});
test('strips GIF metadata without changing animation blocks', () {
final clean = _minimalGif();
final dirty = <int>[
...clean.sublist(0, 19),
..._gifCommentExtension(),
..._gifApplicationExtension('XMP DataXMP', utf8.encode('<x/>')),
...clean.sublist(19),
...utf8.encode('trailing metadata'),
];
expect(
sanitizeAnimatedImageForUpload(Uint8List.fromList(dirty), 'image/gif'),
clean,
);
});
test('canonicalizes GIF loop extensions with hidden sub-blocks', () {
final clean = _minimalGif();
final cleanLoop = _gifLoopExtension();
final dirty = <int>[
...clean.sublist(0, 19),
..._gifLoopExtension(extra: utf8.encode('location')),
...clean.sublist(19 + cleanLoop.length),
];
expect(
sanitizeAnimatedImageForUpload(Uint8List.fromList(dirty), 'image/gif'),
clean,
);
});
test('drops GIF applications with binary authentication codes', () {
final clean = _minimalGif();
final dirty = <int>[
...clean.sublist(0, 19),
..._gifApplicationExtensionBytes([
...ascii.encode('FOREIGN1'),
0xff,
0x80,
0x00,
], utf8.encode('private')),
...clean.sublist(19),
];
expect(
sanitizeAnimatedImageForUpload(Uint8List.fromList(dirty), 'image/gif'),
clean,
);
});
test('removes a GIF graphic control consumed by stripped plain text', () {
final clean = _minimalGif();
final dirty = <int>[
...clean.sublist(0, 19),
0x21,
0xf9,
4,
0x09,
0x1e,
0,
1,
0,
..._gifPlainTextExtension(),
...clean.sublist(19),
];
expect(
sanitizeAnimatedImageForUpload(Uint8List.fromList(dirty), 'image/gif'),
clean,
);
});
test('keeps clean animated containers byte-identical', () {
for (final (mimeType, bytes) in [
('image/png', _animatedPng(metadata: false)),
('image/webp', _animatedWebp(metadata: false)),
('image/gif', _minimalGif()),
]) {
expect(
sanitizeAnimatedImageForUpload(Uint8List.fromList(bytes), mimeType),
bytes,
);
}
});
test('fails closed for malformed animated containers', () {
for (final (mimeType, bytes) in [
('image/png', <int>[0x89, 0x50, 0x4e, 0x47]),
('image/webp', ascii.encode('RIFFxxxxWEBP')),
('image/gif', ascii.encode('GIF89a')),
]) {
expect(
() =>
sanitizeAnimatedImageForUpload(Uint8List.fromList(bytes), mimeType),
throwsFormatException,
);
}
});
}
List<int> _pngChunk(String type, List<int> payload) {
return [
..._uint32BigEndian(payload.length),
...ascii.encode(type),
...payload,
0,
0,
0,
0,
];
}
List<int> _animatedPng({
required bool metadata,
int? orientation,
Endian orientationEndian = Endian.little,
bool iccProfile = false,
}) {
return [
0x89,
0x50,
0x4e,
0x47,
0x0d,
0x0a,
0x1a,
0x0a,
..._pngChunk('IHDR', List.filled(13, 0)),
..._pngChunk('acTL', [0, 0, 0, 2, 0, 0, 0, 0]),
if (iccProfile) ..._pngChunk('iCCP', utf8.encode('profile')),
if (orientation != null)
..._pngChunk(
'eXIf',
_exifOrientation(
orientation,
orientationEndian,
includePreamble: false,
),
),
if (metadata) ..._pngChunk('tEXt', utf8.encode('Location\u0000secret')),
if (metadata) ..._pngChunk('pHYs', List.filled(9, 0)),
..._pngChunk('fcTL', List.filled(26, 0)),
..._pngChunk('IDAT', [1, 2, 3]),
..._pngChunk('fdAT', [0, 0, 0, 1, 4, 5]),
..._pngChunk('IEND', const []),
];
}
List<int> _webpChunk(String type, List<int> payload) {
return [
...ascii.encode(type),
..._uint32LittleEndian(payload.length),
...payload,
if (payload.length.isOdd) 0,
];
}
List<int> _animatedWebp({
required bool metadata,
int? orientation,
Endian orientationEndian = Endian.little,
bool iccProfile = false,
bool nestedMetadata = false,
}) {
final hasExif = metadata || orientation != null;
final frame = <int>[
...List.filled(16, 0),
..._webpChunk('VP8 ', [1, 2, 3]),
if (nestedMetadata) ..._webpChunk('JUNK', utf8.encode('location')),
];
final chunks = <int>[
..._webpChunk('VP8X', [
0x02 | (hasExif ? 0x0c : 0) | (iccProfile ? 0x20 : 0),
0,
0,
0,
0,
0,
0,
0,
0,
0,
]),
..._webpChunk('ANIM', List.filled(6, 0)),
if (iccProfile) ..._webpChunk('ICCP', utf8.encode('profile')),
if (orientation != null)
..._webpChunk(
'EXIF',
_exifOrientation(orientation, orientationEndian, includePreamble: true),
)
else if (metadata)
..._webpChunk('EXIF', utf8.encode('location')),
if (metadata) ..._webpChunk('XMP ', utf8.encode('<xmp/>')),
if (metadata) ..._webpChunk('JUNK', utf8.encode('private')),
..._webpChunk('ANMF', frame),
];
return [
...ascii.encode('RIFF'),
..._uint32LittleEndian(chunks.length + 4),
...ascii.encode('WEBP'),
...chunks,
];
}
List<int> _exifOrientation(
int orientation,
Endian endian, {
required bool includePreamble,
}) {
final bytes = BytesBuilder();
if (includePreamble) {
bytes
..add(ascii.encode('Exif'))
..add(const [0, 0]);
}
final tiff = ByteData(26);
if (endian == Endian.little) {
tiff.setUint8(0, 0x49);
tiff.setUint8(1, 0x49);
} else {
tiff.setUint8(0, 0x4d);
tiff.setUint8(1, 0x4d);
}
tiff.setUint16(2, 42, endian);
tiff.setUint32(4, 8, endian);
tiff.setUint16(8, 1, endian);
tiff.setUint16(10, 0x0112, endian);
tiff.setUint16(12, 3, endian);
tiff.setUint32(14, 1, endian);
tiff.setUint16(18, orientation, endian);
tiff.setUint32(22, 0, endian);
bytes.add(tiff.buffer.asUint8List());
return bytes.takeBytes();
}
List<int> _minimalGif() {
return [
...ascii.encode('GIF89a'),
2,
0,
2,
0,
0x80,
0,
0,
0,
0,
0,
0xff,
0xff,
0xff,
..._gifLoopExtension(),
0x21,
0xf9,
4,
0,
10,
0,
0,
0,
0x2c,
0,
0,
0,
0,
2,
0,
2,
0,
0,
2,
2,
0x44,
1,
0,
0x3b,
];
}
List<int> _gifLoopExtension({List<int> extra = const []}) {
return [
0x21,
0xff,
11,
...ascii.encode('NETSCAPE2.0'),
3,
1,
0,
0,
if (extra.isNotEmpty) ...[extra.length, ...extra],
0,
];
}
List<int> _gifCommentExtension() {
return [0x21, 0xfe, 5, ...ascii.encode('hello'), 0];
}
List<int> _gifApplicationExtension(String identifier, List<int> payload) {
return _gifApplicationExtensionBytes(ascii.encode(identifier), payload);
}
List<int> _gifApplicationExtensionBytes(
List<int> identifier,
List<int> payload,
) {
return [0x21, 0xff, 11, ...identifier, payload.length, ...payload, 0];
}
List<int> _gifPlainTextExtension() {
return [0x21, 0x01, 12, 0, 0, 0, 0, 2, 0, 2, 0, 1, 1, 1, 0, 1, 0x78, 0];
}
List<int> _uint32BigEndian(int value) {
return [
value >> 24 & 0xff,
value >> 16 & 0xff,
value >> 8 & 0xff,
value & 0xff,
];
}
List<int> _uint32LittleEndian(int value) {
return [
value & 0xff,
value >> 8 & 0xff,
value >> 16 & 0xff,
value >> 24 & 0xff,
];
}
@@ -0,0 +1,92 @@
import 'package:buzz/shared/relay/identity_scoped_prefs.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const canonical = 'k_v1:https://relay.example:pk';
const legacy = 'k_v1:wss://relay.example:pk';
Future<SharedPreferences> prefsWith(Map<String, Object> values) async {
SharedPreferences.setMockInitialValues(values);
return SharedPreferences.getInstance();
}
String? readString(SharedPreferences prefs) => readMigratedPref<String>(
prefs,
canonicalKey: canonical,
legacyKey: legacy,
read: prefs.getString,
write: prefs.setString,
);
test('returns the canonical value when present', () async {
final prefs = await prefsWith({canonical: 'new', legacy: 'old'});
expect(readString(prefs), 'new');
});
test('falls back to the legacy value and returns it immediately', () async {
final prefs = await prefsWith({legacy: 'carried over'});
expect(readString(prefs), 'carried over');
});
test('promotes the legacy value onto the canonical key', () async {
final prefs = await prefsWith({legacy: 'carried over'});
readString(prefs);
await Future<void>.delayed(Duration.zero);
expect(prefs.getString(canonical), 'carried over');
expect(prefs.getString(legacy), isNull, reason: 'legacy entry is cleared');
});
test('is idempotent across repeated reads', () async {
final prefs = await prefsWith({legacy: 'carried over'});
readString(prefs);
await Future<void>.delayed(Duration.zero);
expect(readString(prefs), 'carried over');
await Future<void>.delayed(Duration.zero);
expect(prefs.getString(canonical), 'carried over');
});
test('returns null when neither key holds a value', () async {
final prefs = await prefsWith({});
expect(readString(prefs), isNull);
});
test('does not touch storage when the keys coincide', () async {
// A pairing-created community already stores an HTTP origin, so canonical
// and legacy are the same string and there is nothing to migrate.
SharedPreferences.setMockInitialValues({canonical: 'only'});
final prefs = await SharedPreferences.getInstance();
final value = readMigratedPref<String>(
prefs,
canonicalKey: canonical,
legacyKey: canonical,
read: prefs.getString,
write: prefs.setString,
);
await Future<void>.delayed(Duration.zero);
expect(value, 'only');
expect(prefs.getString(canonical), 'only');
});
test('migrates string lists as well as strings', () async {
final prefs = await prefsWith({
legacy: <String>['alpha', 'beta'],
});
final value = readMigratedPref<List<String>>(
prefs,
canonicalKey: canonical,
legacyKey: legacy,
read: prefs.getStringList,
write: prefs.setStringList,
);
await Future<void>.delayed(Duration.zero);
expect(value, ['alpha', 'beta']);
expect(prefs.getStringList(canonical), ['alpha', 'beta']);
expect(prefs.getStringList(legacy), isNull);
});
}
@@ -0,0 +1,239 @@
import 'dart:convert';
import 'dart:async';
import 'package:buzz/shared/relay/media_auth.dart';
import 'package:buzz/shared/relay/media_image.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart' as http_testing;
import 'package:nostr/nostr.dart' as nostr;
// Minimal valid 1x1 transparent PNG.
final _pngBytes = base64Decode(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAA'
'AAYAAjCB0C8AAAAASUVORK5CYII=',
);
const _relayBase = 'https://relay.example.com';
const _mediaUrl = '$_relayBase/media/abc123.png';
MediaGetAuthService _auth({String? nsec, DateTime Function()? now}) =>
MediaGetAuthService(baseUrl: _relayBase, nsec: nsec, now: now);
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
MediaImageProvider.debugResetCooldowns();
MediaImageProvider.debugNow = DateTime.now;
PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLiveImages();
});
group('MediaGetAuthService memoization', () {
test('repeated calls return byte-identical headers', () {
final nsec = nostr.Keys.generate().nsec;
final auth = _auth(nsec: nsec);
final first = auth.headersFor(_mediaUrl);
final second = auth.headersFor(_mediaUrl);
expect(first, isNotEmpty);
expect(identical(first, second), isTrue);
});
test('re-signs only at the refresh margin before expiry', () {
final nsec = nostr.Keys.generate().nsec;
var current = DateTime.utc(2026, 7, 21, 12);
final auth = _auth(nsec: nsec, now: () => current);
final first = auth.headersFor(_mediaUrl);
// 600s lifetime - 60s margin = re-sign boundary at +540s.
current = current.add(const Duration(seconds: 539));
expect(identical(auth.headersFor(_mediaUrl), first), isTrue);
current = current.add(const Duration(seconds: 2));
final refreshed = auth.headersFor(_mediaUrl);
expect(identical(refreshed, first), isFalse);
expect(refreshed['Authorization'], isNot(first['Authorization']));
});
test('non-relay URLs get no headers even with a key', () {
final nsec = nostr.Keys.generate().nsec;
final auth = _auth(nsec: nsec);
expect(auth.headersFor('https://elsewhere.com/media/abc.png'), isEmpty);
expect(auth.headersFor('$_relayBase/not-media/abc.png'), isEmpty);
});
});
group('MediaImageProvider cache identity', () {
test('equal url + same auth service => equal keys', () {
final auth = _auth(nsec: nostr.Keys.generate().nsec);
final client = http.Client();
addTearDown(client.close);
final a = MediaImageProvider(url: _mediaUrl, auth: auth, client: client);
final b = MediaImageProvider(url: _mediaUrl, auth: auth, client: client);
expect(a, equals(b));
expect(a.hashCode, equals(b.hashCode));
});
test('different auth service (relay/account switch) => unequal keys', () {
final client = http.Client();
addTearDown(client.close);
final a = MediaImageProvider(
url: _mediaUrl,
auth: _auth(nsec: nostr.Keys.generate().nsec),
client: client,
);
final b = MediaImageProvider(
url: _mediaUrl,
auth: _auth(nsec: nostr.Keys.generate().nsec),
client: client,
);
expect(a, isNot(equals(b)));
});
test('transport client is not part of identity', () {
final auth = _auth(nsec: nostr.Keys.generate().nsec);
final c1 = http.Client();
final c2 = http.Client();
addTearDown(c1.close);
addTearDown(c2.close);
expect(
MediaImageProvider(url: _mediaUrl, auth: auth, client: c1),
equals(MediaImageProvider(url: _mediaUrl, auth: auth, client: c2)),
);
});
});
group('MediaImageProvider fetching', () {
test('resolves one fetch for repeated resolves of the same key', () async {
var fetches = 0;
final client = http_testing.MockClient((request) async {
fetches += 1;
return http.Response.bytes(_pngBytes, 200);
});
final auth = _auth(nsec: nostr.Keys.generate().nsec);
for (var i = 0; i < 3; i++) {
final provider = MediaImageProvider(
url: _mediaUrl,
auth: auth,
client: client,
);
final completer = provider.resolve(ImageConfiguration.empty);
await _wait(completer);
}
expect(fetches, 1);
});
test('sends auth headers with the fetch', () async {
Map<String, String>? seen;
final client = http_testing.MockClient((request) async {
seen = request.headers;
return http.Response.bytes(_pngBytes, 200);
});
final auth = _auth(nsec: nostr.Keys.generate().nsec);
final provider = MediaImageProvider(
url: _mediaUrl,
auth: auth,
client: client,
);
await _wait(provider.resolve(ImageConfiguration.empty));
expect(seen?['Authorization'], startsWith('Nostr '));
});
test('failed URL is not refetched until cooldown elapses', () async {
var fetches = 0;
final client = http_testing.MockClient((request) async {
fetches += 1;
return http.Response('rate limited', 429);
});
final auth = _auth(nsec: nostr.Keys.generate().nsec);
var current = DateTime.utc(2026, 7, 21, 12);
MediaImageProvider.debugNow = () => current;
Future<Object?> attempt() async {
final provider = MediaImageProvider(
url: _mediaUrl,
auth: auth,
client: client,
);
return _waitError(provider.resolve(ImageConfiguration.empty));
}
expect(await attempt(), isA<NetworkImageLoadException>());
expect(fetches, 1);
// Within cooldown: suppressed, no network call.
expect(await attempt(), isA<MediaImageCooldownException>());
expect(fetches, 1);
// After cooldown: retried.
current = current.add(const Duration(seconds: 31));
expect(await attempt(), isA<NetworkImageLoadException>());
expect(fetches, 2);
});
test('honors Retry-After on 429 (capped)', () async {
var fetches = 0;
final client = http_testing.MockClient((request) async {
fetches += 1;
return http.Response(
'rate limited',
429,
headers: {'retry-after': '120'},
);
});
final auth = _auth(nsec: nostr.Keys.generate().nsec);
var current = DateTime.utc(2026, 7, 21, 12);
MediaImageProvider.debugNow = () => current;
Future<Object?> attempt() async {
final provider = MediaImageProvider(
url: _mediaUrl,
auth: auth,
client: client,
);
return _waitError(provider.resolve(ImageConfiguration.empty));
}
await attempt();
expect(fetches, 1);
current = current.add(const Duration(seconds: 60));
expect(await attempt(), isA<MediaImageCooldownException>());
expect(fetches, 1);
current = current.add(const Duration(seconds: 61));
await attempt();
expect(fetches, 2);
});
});
}
Future<void> _wait(ImageStream stream) {
final done = Completer<void>();
late final ImageStreamListener listener;
listener = ImageStreamListener(
(image, sync) {
image.dispose();
stream.removeListener(listener);
if (!done.isCompleted) done.complete();
},
onError: (error, stack) {
stream.removeListener(listener);
if (!done.isCompleted) done.completeError(error, stack);
},
);
stream.addListener(listener);
return done.future;
}
Future<Object?> _waitError(ImageStream stream) async {
try {
await _wait(stream);
return null;
} catch (e) {
return e;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,145 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:buzz/shared/relay/mp4_fast_start.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
late Directory tempDirectory;
setUp(() async {
tempDirectory = await Directory.systemTemp.createTemp(
'buzz-faststart-test-',
);
});
tearDown(() async {
await tempDirectory.delete(recursive: true);
});
for (final offsetBox in ['stco', 'co64']) {
test('moves moov before mdat and patches $offsetBox offsets', () async {
final ftyp = _box('ftyp', [
...ascii.encode('isom'),
0,
0,
0,
0,
...ascii.encode('isom'),
]);
final mdat = _box('mdat', [1, 2, 3, 4]);
final originalMediaOffset = ftyp.length + 8;
final offsetPayload = BytesBuilder()
..add([0, 0, 0, 0])
..add(_uint32(1))
..add(
offsetBox == 'stco'
? _uint32(originalMediaOffset)
: _uint64(originalMediaOffset),
);
final sampleTable = _box(offsetBox, offsetPayload.takeBytes());
final moov = _nestedMoov(sampleTable);
final sourceBytes = Uint8List.fromList([...ftyp, ...mdat, ...moov]);
final source = File('${tempDirectory.path}/source.mp4');
final destination = File('${tempDirectory.path}/output.mp4');
await source.writeAsBytes(sourceBytes);
await rewriteMp4ForFastStart(source, destination);
final output = await destination.readAsBytes();
expect(_topLevelTypes(output), ['ftyp', 'moov', 'mdat']);
expect(
output.sublist(ftyp.length + moov.length + 8),
equals([1, 2, 3, 4]),
);
final typeOffset = _findAscii(output, offsetBox);
expect(typeOffset, greaterThanOrEqualTo(4));
final entryOffset = typeOffset + 12;
final adjusted = offsetBox == 'stco'
? _readUint32(output, entryOffset)
: _readUint64(output, entryOffset);
expect(adjusted, originalMediaOffset + moov.length);
});
}
test(
'rejects excessive nested box depth and deletes partial output',
() async {
final ftyp = _box('ftyp', [
...ascii.encode('isom'),
0,
0,
0,
0,
...ascii.encode('isom'),
]);
final mdat = _box('mdat', [1]);
var nested = _box('stco', [0, 0, 0, 0, ..._uint32(0)]);
for (var index = 0; index < 34; index++) {
nested = _box('stbl', nested);
}
final source = File('${tempDirectory.path}/deep.mp4');
final destination = File('${tempDirectory.path}/output.mp4');
await source.writeAsBytes([...ftyp, ...mdat, ..._box('moov', nested)]);
await expectLater(
rewriteMp4ForFastStart(source, destination),
throwsA(isA<FormatException>()),
);
expect(await destination.exists(), isFalse);
},
);
}
Uint8List _nestedMoov(List<int> leaf) =>
_box('moov', _box('trak', _box('mdia', _box('minf', _box('stbl', leaf)))));
Uint8List _box(String type, List<int> payload) => Uint8List.fromList([
..._uint32(payload.length + 8),
...ascii.encode(type),
...payload,
]);
Uint8List _uint32(int value) {
final bytes = ByteData(4)..setUint32(0, value, Endian.big);
return bytes.buffer.asUint8List();
}
Uint8List _uint64(int value) {
final bytes = ByteData(8)..setUint64(0, value, Endian.big);
return bytes.buffer.asUint8List();
}
int _readUint32(Uint8List bytes, int offset) =>
ByteData.sublistView(bytes).getUint32(offset, Endian.big);
int _readUint64(Uint8List bytes, int offset) =>
ByteData.sublistView(bytes).getUint64(offset, Endian.big);
List<String> _topLevelTypes(Uint8List bytes) {
final types = <String>[];
var offset = 0;
while (offset < bytes.length) {
final size = _readUint32(bytes, offset);
types.add(ascii.decode(bytes.sublist(offset + 4, offset + 8)));
offset += size;
}
return types;
}
int _findAscii(Uint8List bytes, String value) {
final needle = ascii.encode(value);
for (var offset = 0; offset + needle.length <= bytes.length; offset++) {
var matches = true;
for (var index = 0; index < needle.length; index++) {
if (bytes[offset + index] != needle[index]) {
matches = false;
break;
}
}
if (matches) return offset;
}
return -1;
}
@@ -0,0 +1,101 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:buzz/shared/relay/nostr_models.dart';
void main() {
test('NostrFilter serializes and preserves authors', () {
const filter = NostrFilter(
kinds: [EventKind.readState],
authors: ['pubkey-a'],
tags: {
'#t': ['read-state'],
},
since: 10,
limit: 5,
);
expect(filter.toJson(), {
'kinds': [EventKind.readState],
'limit': 5,
'authors': ['pubkey-a'],
'since': 10,
'#t': ['read-state'],
});
final copied = filter.copyWithSince(20);
expect(copied.toJson(), {
'kinds': [EventKind.readState],
'limit': 5,
'authors': ['pubkey-a'],
'since': 20,
'#t': ['read-state'],
});
});
test('NostrFilter can serialize broad deletion subscriptions', () {
const filter = NostrFilter(kinds: [EventKind.deletion], limit: 0);
expect(filter.toJson(), {
'kinds': [EventKind.deletion],
'limit': 0,
});
});
test('NostrFilter serializes relay query extensions', () {
const filter = NostrFilter(
kinds: EventKind.channelTimelineContentKinds,
tags: {
'#h': ['channel-id'],
},
limit: 50,
extensions: {
'top_level': true,
'include_summaries': true,
'include_aux': true,
'before_id': 'cursor-id',
},
);
expect(filter.toJson(), {
'kinds': EventKind.channelTimelineContentKinds,
'limit': 50,
'#h': ['channel-id'],
'top_level': true,
'include_summaries': true,
'include_aux': true,
'before_id': 'cursor-id',
});
});
test('channel unread activity kinds exclude non-message updates', () {
expect(
EventKind.channelMessageEventKinds,
contains(EventKind.streamMessage),
);
expect(EventKind.channelMessageEventKinds, contains(EventKind.forumPost));
expect(
EventKind.channelMessageEventKinds,
contains(EventKind.forumComment),
);
expect(
EventKind.channelMessageEventKinds,
isNot(contains(EventKind.reaction)),
);
expect(
EventKind.channelMessageEventKinds,
isNot(contains(EventKind.streamMessageEdit)),
);
expect(
EventKind.channelMessageEventKinds,
isNot(contains(EventKind.streamMessageDiff)),
);
expect(
EventKind.channelMessageEventKinds,
isNot(contains(EventKind.deletion)),
);
expect(
EventKind.channelMessageEventKinds,
isNot(contains(EventKind.systemMessage)),
);
});
}
@@ -0,0 +1,45 @@
import 'package:buzz/shared/relay/relay.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('classifyRelayClosed', () {
const cases = {
'rate-limited: quota exceeded; retry in 4s': RelayClosedClass.rateLimited,
'rate-limited: too many concurrent requests':
RelayClosedClass.rateLimited,
'restricted: access revoked': RelayClosedClass.terminal,
'auth-required: verification failed': RelayClosedClass.terminal,
'blocked: community policy': RelayClosedClass.terminal,
'invalid: malformed filter': RelayClosedClass.terminal,
'pow: insufficient work': RelayClosedClass.terminal,
'duplicate: subscription already exists': RelayClosedClass.terminal,
'unsupported: filter extension': RelayClosedClass.terminal,
'error: mixed search and channel filter': RelayClosedClass.terminal,
'error: too many subscriptions': RelayClosedClass.terminal,
'error: relay temporarily unavailable': RelayClosedClass.retryable,
'subscription closed by relay': RelayClosedClass.retryable,
};
for (final entry in cases.entries) {
test(entry.key, () {
expect(classifyRelayClosed(entry.key), entry.value);
});
}
});
test('parseRateLimitRetrySeconds handles canonical and absent hints', () {
expect(
parseRateLimitRetrySeconds('rate-limited: quota exceeded; retry in 17s'),
17,
);
expect(parseRateLimitRetrySeconds('RETRY IN 2S'), 2);
expect(parseRateLimitRetrySeconds('retry in 0s'), 0);
expect(parseRateLimitRetrySeconds('retry in 999999s'), 999999);
final oversizedHint = List.filled(1000, '9').join();
expect(parseRateLimitRetrySeconds('retry in ${oversizedHint}s'), isNull);
expect(
parseRateLimitRetrySeconds('rate-limited: too many concurrent requests'),
isNull,
);
});
}
@@ -0,0 +1,67 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:buzz/shared/relay/relay_provider.dart';
void main() {
group('RelayConfig.baseUrl normalization', () {
test('folds a wss:// community URL to https://', () {
// Invite joins persist the relay URL straight off the invite link, which
// deep_link.dart always emits as ws:// or wss://.
final config = RelayConfig(baseUrl: 'wss://relay.example.com');
expect(config.baseUrl, 'https://relay.example.com');
});
test('folds a ws:// community URL to http://', () {
final config = RelayConfig(baseUrl: 'ws://relay.example.com:3000');
expect(config.baseUrl, 'http://relay.example.com:3000');
});
test('leaves an https:// community URL untouched', () {
// Device pairing rejects anything but https://, so these already conform.
final config = RelayConfig(baseUrl: 'https://relay.example.com');
expect(config.baseUrl, 'https://relay.example.com');
});
test('leaves an http:// community URL untouched', () {
final config = RelayConfig(baseUrl: 'http://localhost:3000');
expect(config.baseUrl, 'http://localhost:3000');
});
test('preserves a non-default port', () {
final config = RelayConfig(baseUrl: 'wss://relay.example.com:8443');
expect(config.baseUrl, 'https://relay.example.com:8443');
});
});
group('RelayConfig.wsUrl', () {
test('keeps TLS for a relay joined by invite', () {
// Regression: a wss:// base used to fall through to the non-https branch
// and downgrade to ws://, dialing port 80 — which never connects on a
// relay that only serves 443, and drops TLS everywhere else.
final config = RelayConfig(baseUrl: 'wss://relay.example.com');
expect(config.wsUrl, 'wss://relay.example.com');
});
test('keeps TLS for a relay added by pairing', () {
final config = RelayConfig(baseUrl: 'https://relay.example.com');
expect(config.wsUrl, 'wss://relay.example.com');
});
test('both onboarding paths agree on the same relay', () {
final invited = RelayConfig(baseUrl: 'wss://relay.example.com');
final paired = RelayConfig(baseUrl: 'https://relay.example.com');
expect(invited.wsUrl, paired.wsUrl);
expect(invited.baseUrl, paired.baseUrl);
});
test('stays plaintext for local development', () {
final config = RelayConfig(baseUrl: 'http://localhost:3000');
expect(config.wsUrl, 'ws://localhost:3000');
});
test('preserves a non-default port', () {
final config = RelayConfig(baseUrl: 'wss://relay.example.com:8443');
expect(config.wsUrl, 'wss://relay.example.com:8443');
});
});
}
@@ -0,0 +1,118 @@
import 'dart:async';
import 'package:buzz/shared/relay/relay.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('uses the default for absent and non-positive hints', () {
var now = DateTime.utc(2026);
final timers = <_ManualTimer>[];
final gate = RelayRateLimitGate(
now: () => now,
timerFactory: (duration, callback) {
final timer = _ManualTimer(duration, callback);
timers.add(timer);
return timer;
},
);
gate.activate(null);
expect(gate.remainingMs(), 10000);
expect(timers.single.duration, const Duration(seconds: 10));
now = now.add(const Duration(seconds: 11));
gate.activate(0);
expect(timers.last.duration, const Duration(seconds: 10));
});
test('clamps large hints to five minutes', () {
final timers = <_ManualTimer>[];
final gate = RelayRateLimitGate(
now: () => DateTime.utc(2026),
timerFactory: (duration, callback) {
final timer = _ManualTimer(duration, callback);
timers.add(timer);
return timer;
},
);
gate.activate(999999);
expect(timers.single.duration, const Duration(seconds: 300));
expect(gate.remainingMs(), 300000);
});
test('overlapping activations only extend the active window', () async {
var now = DateTime.utc(2026);
final timers = <_ManualTimer>[];
final gate = RelayRateLimitGate(
now: () => now,
timerFactory: (duration, callback) {
final timer = _ManualTimer(duration, callback);
timers.add(timer);
return timer;
},
);
gate.activate(30);
final wait = gate.wait();
final firstTimer = timers.single;
now = now.add(const Duration(seconds: 5));
gate.activate(10);
expect(timers, hasLength(1));
expect(gate.remainingMs(), 25000);
gate.activate(40);
expect(timers, hasLength(2));
expect(firstTimer.isActive, isFalse);
expect(gate.remainingMs(), 40000);
timers.last.fire();
await wait;
expect(gate.isActive, isFalse);
expect(gate.remainingMs(), 0);
});
test('reset releases waiters and cancels the active timer', () async {
final timers = <_ManualTimer>[];
final gate = RelayRateLimitGate(
timerFactory: (duration, callback) {
final timer = _ManualTimer(duration, callback);
timers.add(timer);
return timer;
},
);
gate.activate(20);
final wait = gate.wait();
gate.reset();
await wait;
expect(timers.single.isActive, isFalse);
expect(gate.isActive, isFalse);
});
}
class _ManualTimer implements Timer {
_ManualTimer(this.duration, this._callback);
final Duration duration;
final void Function() _callback;
bool _active = true;
void fire() {
if (!_active) return;
_active = false;
_callback();
}
@override
void cancel() => _active = false;
@override
bool get isActive => _active;
@override
int get tick => _active ? 0 : 1;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,111 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:buzz/shared/relay/relay_socket.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter_test/flutter_test.dart';
/// A server that completes the WS handshake then never speaks again: no pongs,
/// no close frame. Only a client-side ping timeout can notice.
Future<ServerSocket> _silentAfterHandshakeServer() async {
final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
server.listen((client) {
client.listen(
(data) {
final match = RegExp(
r'Sec-WebSocket-Key: (.*)\r\n',
caseSensitive: false,
).firstMatch(String.fromCharCodes(data));
if (match == null) return;
final accept = base64.encode(
sha1
.convert(
utf8.encode(
'${match.group(1)!.trim()}258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
),
)
.bytes,
);
client.write(
'HTTP/1.1 101 Switching Protocols\r\n'
'Upgrade: websocket\r\nConnection: Upgrade\r\n'
'Sec-WebSocket-Accept: $accept\r\n\r\n',
);
},
onError: (_) {},
onDone: () {},
);
});
return server;
}
void main() {
const testPingInterval = Duration(milliseconds: 150);
setUp(() {
RelaySocket.debugPingInterval = testPingInterval;
});
tearDown(() {
RelaySocket.debugPingInterval = RelaySocket.pingInterval;
});
test('detects a peer that stops answering pings', () async {
final server = await _silentAfterHandshakeServer();
final disconnected = Completer<Object?>();
final socket = RelaySocket(
wsUrl: 'ws://127.0.0.1:${server.port}',
nsec: null,
onMessage: (_) {},
onConnected: () {},
onDisconnected: (error) {
if (!disconnected.isCompleted) disconnected.complete(error);
},
);
unawaited(socket.connect());
var detected = true;
try {
await disconnected.future.timeout(testPingInterval * 4);
} on TimeoutException {
detected = false;
}
expect(
detected,
isTrue,
reason:
'RelaySocket must surface an unanswered ping through onDisconnected',
);
socket.dispose();
await server.close();
});
test('keeps an idle but healthy peer connected', () async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
server.transform(WebSocketTransformer()).listen((ws) {
// A healthy relay answers pings without sending application data.
ws.listen((_) {}, onError: (_) {}, onDone: () {});
});
var tornDown = false;
final socket = RelaySocket(
wsUrl: 'ws://127.0.0.1:${server.port}',
nsec: null,
onMessage: (_) {},
onConnected: () {},
onDisconnected: (_) => tornDown = true,
);
unawaited(socket.connect());
await Future<void>.delayed(testPingInterval * 4);
expect(tornDown, isFalse);
await socket.disconnect();
await server.close(force: true);
});
}
@@ -0,0 +1,161 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:buzz/shared/relay/relay_validation.dart';
void main() {
group('validateInviteRelayUri', () {
test('accepts public secure relay origins', () {
for (final url in [
'wss://relay.example.com',
'wss://relay.example.com/',
'wss://relay.example.com:8443',
'wss://8.8.8.8',
'wss://[2001:4860:4860::8888]',
'wss://[2001:1::1]',
'wss://[2001:1::2]',
'wss://[2001:1::3]',
'wss://[2001:3::1]',
'wss://[2001:4:112::1]',
'wss://[2001:20::1]',
'wss://[2001:30::1]',
'wss://[2001:200::1]',
'wss://[2620:4f:8000::1]',
'wss://[3fff:1000::1]',
'wss://[2606:4700:4700::1111]',
]) {
expect(
() => validateInviteRelayUri(
Uri.parse(url),
allowInsecureLocalhost: false,
),
returnsNormally,
reason: url,
);
}
});
test('allows plaintext localhost only when explicitly enabled', () {
for (final url in ['ws://localhost:3000', 'ws://relay.localhost:3000']) {
expect(
() => validateInviteRelayUri(
Uri.parse(url),
allowInsecureLocalhost: true,
),
returnsNormally,
reason: url,
);
expect(
() => validateInviteRelayUri(
Uri.parse(url),
allowInsecureLocalhost: false,
),
throwsFormatException,
reason: url,
);
}
});
test('rejects plaintext public relays', () {
expect(
() => validateInviteRelayUri(
Uri.parse('ws://relay.example.com'),
allowInsecureLocalhost: false,
),
throwsFormatException,
);
});
test('rejects non-public IPv4 literals', () {
for (final host in [
'0.0.0.0',
'10.0.0.1',
'100.64.0.1',
'127.0.0.1',
'169.254.169.254',
'172.16.0.1',
'192.168.0.1',
'198.18.0.1',
'224.0.0.1',
]) {
expect(
() => validateInviteRelayUri(
Uri.parse('wss://$host'),
allowInsecureLocalhost: false,
),
throwsFormatException,
reason: host,
);
}
});
test('rejects non-public IPv6 literals and mapped IPv4', () {
for (final host in [
'[::]',
'[::1]',
'[::ffff:127.0.0.1]',
'[::ffff:169.254.169.254]',
'[::ffff:a9fe:a9fe]',
'[64:ff9b::a9fe:a9fe]',
'[100::1]',
'[100:0:0:1::1]',
'[2001::1]',
'[2001:1::4]',
'[2001:2::1]',
'[2001:4:111::1]',
'[2001:10::1]',
'[2001:40::1]',
'[2001:db8::1]',
'[2002:a9fe:a9fe::1]',
'[3fff::1]',
'[3fff:fff::1]',
'[5f00::1]',
'[fc00::1]',
'[fd00::1]',
'[fe80::1]',
'[fec0::1]',
'[ff02::1]',
]) {
expect(
() => validateInviteRelayUri(
Uri.parse('wss://$host'),
allowInsecureLocalhost: false,
),
throwsFormatException,
reason: host,
);
}
});
test('rejects legacy numeric IPv4 spellings', () {
for (final host in ['2130706433', '0x7f000001', '0177.0.0.1', '127.1']) {
expect(
() => validateInviteRelayUri(
Uri.parse('wss://$host'),
allowInsecureLocalhost: false,
),
throwsFormatException,
reason: host,
);
}
});
test('rejects non-origin URLs', () {
for (final url in [
'https://relay.example.com',
'wss://user@relay.example.com',
'wss://relay.example.com/path',
'wss://relay.example.com?query=x',
'wss://relay.example.com#fragment',
]) {
expect(
() => validateInviteRelayUri(
Uri.parse(url),
allowInsecureLocalhost: false,
),
throwsFormatException,
reason: url,
);
}
});
});
}