Files
sync_replay_project/packages/server/test/session_test.dart
T
foxyzandClaude Opus 5.5 e7f33b5dc3
ci / core (push) Successful in 28s
ci / updater-windows (push) Successful in 49s
ci / server (push) Successful in 49s
Relay: a member's media through the server's buffer (proto 8)
Viewers who cannot reach the media get it through the server: a file on
any member's computer, a link passed on by a member ("Through me") or
fetched by the server ("Server fetches it", password-protected servers
only), and Jellyfin items for guests without an account, sourced by
whoever put them up with their own sign-in (or a pasted link's key),
which never leaves their machine. Fixes guests on a dedicated server
without a Jellyfin login being blocked.

The source dials /r/.../source (works behind NAT) and answers pulls with
an adaptive in-flight window (2 x rate x min RTT, 256 KiB..8 MiB). The
server keeps a memory-only buffer of 1 MiB slices sized like a viewer's
mpv buffer (pinned head/tail, 64 MiB back, 256 MiB ahead), fills it with
one sequential pull at a time (blocked viewers first, newest request
only), fetches nothing while nobody watches through it, and drops it on
the next swap or stop. Measured with mpv through shaped links
(tool/relay_e2e.dart): 400 Mbit/s seeks ~0.03 s; 40 Mbit/s no stalls,
3 viewers cost the source ~1x the bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:41:01 +03:00

129 lines
4.9 KiB
Dart

import 'package:sync_relay_core/core.dart';
import 'package:sync_relay_server/server.dart';
import 'package:test/test.dart';
void main() {
group('Session model', () {
test('tokens are unique 128-bit hex', () {
final s = Session(id: 'default', secret: 'shh');
final a = s.issueToken('alice', '');
final b = s.issueToken('bob', '');
expect(a, hasLength(32));
expect(a, isNot(b));
expect(RegExp(r'^[0-9a-f]{32}$').hasMatch(a), isTrue);
});
test('gate: unknown -> unauthorized, known -> ok', () {
final s = Session(id: 'default', secret: 'shh');
expect(s.gate(null), MediaGate.unauthorized);
expect(s.gate('nope'), MediaGate.unauthorized);
final t = s.issueToken('alice', '');
expect(s.gate(t), MediaGate.ok);
});
test('kick unknown throws, kick known forbids + drops', () async {
final s = Session(id: 'default', secret: 'shh');
expect(() => s.kick('nope'), throwsA(isA<AuthException>()));
final t = s.issueToken('mallory', '');
var dropped = false;
String? gotReason;
s.tokens[t]!.onKick = (reason) async {
dropped = true;
gotReason = reason;
};
await s.kick(t, reason: 'idle');
expect(s.gate(t), MediaGate.forbidden);
expect(dropped, isTrue);
expect(gotReason, 'idle');
});
test('oversize avatar rejected', () {
final s = Session(id: 'default', secret: 'shh');
final big = 'x' * (RelayConstants.maxAvatarBytes + 1);
expect(() => s.issueToken('alice', big), throwsA(isA<AuthException>()));
});
test('64 KiB avatar accepted under raised cap', () {
final s = Session(id: 'default', secret: 'shh');
final png = 'x' * (64 * 1024);
expect(s.issueToken('alice', png), isNotEmpty);
expect(s.tokens.values.single.avatar, hasLength(64 * 1024));
});
test('names truncated to cap, blank becomes ?', () {
final s = Session(id: 'default', secret: 'shh');
final long = s.issueToken('a' * 100, '');
expect(s.tokens[long]!.name, hasLength(RelayConstants.maxNameLength));
final blank = s.issueToken(' ', '');
expect(s.tokens[blank]!.name, '?');
});
test('byte counters accumulate', () { final s = Session(id: 'default', secret: 'shh');
final a = s.issueToken('alice', '');
final b = s.issueToken('bob', '');
s.addBytes(a, 100);
s.addBytes(b, 50);
s.addBytes('ghost', 999);
expect(s.tokens[a]!.bytesServed, 100);
expect(s.totalBytesServed, 150);
});
test('SessionMap create/get/drop', () {
final m = SessionMap();
expect(m.get('default'), isNull);
final s = m.getOrCreate('default', 'shh');
expect(m.get('default'), same(s));
expect(m.getOrCreate('default', 'other'), same(s));
m.drop('default');
expect(m.get('default'), isNull);
});
});
group('core welcome + proto', () {
test('proto 8: relayed media', () {
expect(RelayConstants.protoVersion, 8);
const m = HelloMedia(name: 'a', size: 1, duration: 0, relay: MediaRelay(source: 'm1', live: true));
final back = HelloMedia.fromJson(Map<String, Object?>.from(m.toJson()));
expect(back.relay!.source, 'm1');
expect(back.relay!.live, isTrue);
expect(back.relay!.fetch, isFalse);
expect(const MediaRelay().toJson(), isEmpty);
});
test('welcome round-trips with token + member id', () {
const w = WelcomeMessage(token: 'abc', memberId: 'm1', mediaUrl: 'http://h/m');
final back = WelcomeMessage.fromJson(Map<String, Object?>.from(w.toJson()));
expect(back.token, 'abc');
expect(back.memberId, 'm1');
expect(back.mediaUrl, 'http://h/m');
expect(back.state, isNull);
});
test('welcome rejects wrong type', () {
expect(() => WelcomeMessage.fromJson({'t': 'hello'}), throwsA(isA<ProtocolException>()));
});
test('kicked + kick request round-trip', () {
const k = KickedMessage(reason: 'idle');
final back = KickedMessage.fromJson(Map<String, Object?>.from(k.toJson()));
expect(back.reason, 'idle');
const r = KickRequest(target: 'abc', reason: 'afk?');
final backR = KickRequest.fromJson(Map<String, Object?>.from(r.toJson()));
expect(backR.target, 'abc');
expect(backR.reason, 'afk?');
expect(() => KickedMessage.fromJson({'t': 'err'}), throwsA(isA<ProtocolException>()));
expect(() => KickRequest.fromJson({'t': 'err'}), throwsA(isA<ProtocolException>()));
});
test('lastSeen touch + stale helper', () {
final s = Session(id: 'default', secret: 'shh');
final t = s.issueToken('alice', '');
final viewer = s.tokens[t]!;
expect(viewer.isStale(const Duration(seconds: 30)), isFalse);
viewer.touch(DateTime.utc(2020, 1, 1));
expect(viewer.isStale(const Duration(seconds: 30)), isTrue);
expect(viewer.isStale(const Duration(seconds: 30), DateTime.utc(2020, 1, 1, 0, 0, 10)), isFalse);
});
});
}