import 'package:flutter_test/flutter_test.dart'; import 'package:timbre/remote/messages.dart'; import 'package:timbre/subsonic/models.dart'; Song _song(String id, {String? title}) => Song(id: id, title: title ?? id); void main() { group('RemoteAuth', () { test('proof matches when the password matches', () { const nonce = 'abc123'; final proof = RemoteAuth.proofFor(password: 'hunter2', nonce: nonce); expect( RemoteAuth.verify(password: 'hunter2', nonce: nonce, proof: proof), isTrue, ); }); test('proof fails on the wrong password', () { const nonce = 'abc123'; final proof = RemoteAuth.proofFor(password: 'hunter2', nonce: nonce); expect( RemoteAuth.verify(password: 'wrong', nonce: nonce, proof: proof), isFalse, ); }); test('proof is nonce-bound (no replay across connections)', () { final p1 = RemoteAuth.proofFor(password: 'pw', nonce: 'n1'); final p2 = RemoteAuth.proofFor(password: 'pw', nonce: 'n2'); expect(p1, isNot(p2)); expect(RemoteAuth.verify(password: 'pw', nonce: 'n2', proof: p1), isFalse); }); test('newNonce is fresh each call', () { expect(RemoteAuth.newNonce(), isNot(RemoteAuth.newNonce())); }); }); group('StateSnapshot codec', () { test('round-trips through JSON', () { final snap = StateSnapshot( queue: [_song('1', title: 'One'), _song('2', title: 'Two')], currentIndex: 1, playing: true, positionMs: 42000, durationMs: 180000, shuffle: true, loop: 'all', seq: 7, ); final back = StateSnapshot.fromJson( StateSnapshot.fromJson(snap.toJson()).toJson(), ); expect(back.queue.map((s) => s.id), ['1', '2']); expect(back.currentIndex, 1); expect(back.playing, isTrue); expect(back.positionMs, 42000); expect(back.durationMs, 180000); expect(back.shuffle, isTrue); expect(back.loop, 'all'); expect(back.seq, 7); }); test('tolerates a missing/empty queue', () { final snap = StateSnapshot.fromJson({'playing': false}); expect(snap.queue, isEmpty); expect(snap.currentIndex, isNull); expect(snap.loop, 'off'); }); }); group('RemoteCommand codec', () { test('payload-carrying ops round-trip', () { final cmds = [ const RemoteCommand.playPause(), const RemoteCommand.seek(12345), const RemoteCommand.jumpTo(3), const RemoteCommand.removeAt(2), const RemoteCommand.reorder(1, 4), RemoteCommand.playSongs([_song('a'), _song('b')], startIndex: 1), RemoteCommand.playNext(_song('c')), RemoteCommand.addToQueue(_song('d')), const RemoteCommand.toggleShuffle(), const RemoteCommand.cycleLoop(), ]; for (final c in cmds) { final back = RemoteCommand.fromJson(c.toJson()); expect(back.op, c.op); expect(back.index, c.index); expect(back.oldIndex, c.oldIndex); expect(back.newIndex, c.newIndex); expect(back.startIndex, c.startIndex); expect(back.positionMs, c.positionMs); expect(back.song?.id, c.song?.id); expect(back.songs?.map((s) => s.id), c.songs?.map((s) => s.id)); } }); test('unknown op throws', () { expect( () => RemoteCommand.fromJson({'op': 'nope'}), throwsFormatException, ); }); }); group('RemoteMessage envelope', () { test('encodes and decodes each message type', () { final msgs = [ const ChallengeMessage( version: 1, serverKey: 'k', nonce: 'n', device: 'iPad'), const AuthMessage( version: 1, serverKey: 'k', proof: 'p', device: 'Phone'), const WelcomeMessage(device: 'iPad'), const DenyMessage(reason: 'bad auth'), SnapshotMessage(StateSnapshot( queue: [_song('1')], currentIndex: 0, playing: false, positionMs: 0, durationMs: 0, shuffle: false, loop: 'off', seq: 1, )), const CommandMessage(RemoteCommand.next()), ]; for (final m in msgs) { final back = RemoteMessage.decode(m.encode()); expect(back.runtimeType, m.runtimeType); } }); test('decodes handshake fields', () { final decoded = RemoteMessage.decode(const ChallengeMessage( version: kRemoteProtocolVersion, serverKey: 'server-key', nonce: 'the-nonce', device: 'Living Room iPad', ).encode()); expect(decoded, isA()); final c = decoded as ChallengeMessage; expect(c.version, kRemoteProtocolVersion); expect(c.serverKey, 'server-key'); expect(c.nonce, 'the-nonce'); expect(c.device, 'Living Room iPad'); }); test('rejects a non-object frame', () { expect(() => RemoteMessage.decode('42'), throwsFormatException); }); test('rejects an unknown message type', () { expect(() => RemoteMessage.decode('{"t":"bogus"}'), throwsFormatException); }); }); }