import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:friendly_map/modules/core/core.dart'; import 'package:friendly_map/modules/transport/transport.dart'; import 'support/envelopes.dart'; const a = '+821011112222'; const b = '+821022223333'; Future> collect(Transport t, Future Function() act) async { final got = []; final sub = t.incoming.listen(got.add); await act(); await Future.delayed(const Duration(milliseconds: 50)); await sub.cancel(); return got; } /// 보관함 서버를 띄운다. 윈도우의 python3 는 스토어 바로가기일 수 있어 python, py 순으로도 시도한다. Future<(Process, String)> startServer(List extra) async { for (final exe in ['python3', 'python', 'py']) { Process? p; try { p = await Process.start(exe, ['mailbox_server.py', '--host', '127.0.0.1', '--port', '0', ...extra], workingDirectory: 'server'); final line = await p.stdout.transform(utf8.decoder).transform(const LineSplitter()).first .timeout(const Duration(seconds: 5)); return (p, 'http://127.0.0.1:${line.split(' ').last}'); } catch (_) { p?.kill(); } } throw StateError('파이썬을 찾지 못함'); } void main() { group('메모리 연결', () { test('켜져 있으면 바로 도착', () async { final hub = MemoryHub(); final ta = MemoryTransport(a, hub), tb = MemoryTransport(b, hub); await ta.start(); await tb.start(); final got = await collect(tb, () async => expect(await ta.send(envelope('1')), true)); expect(got.single.id, '1'); }); test('꺼져 있으면 보관함에 쌓였다가 가져옴', () async { final hub = MemoryHub(); final ta = MemoryTransport(a, hub), tb = MemoryTransport(b, hub); await ta.start(); expect(await ta.send(envelope('1')), true); await tb.start(); final got = await collect(tb, () async => expect(await tb.fetchPending(), 1)); expect(got.single.id, '1'); expect(await tb.fetchPending(), 0); }); test('보관함을 끄면 못 보냄', () async { final hub = MemoryHub()..mailboxEnabled = false; final ta = MemoryTransport(a, hub); MemoryTransport(b, hub); await ta.start(); expect(await ta.send(envelope('1')), false); }); }); group('묶음 연결', () { test('앞 방식이 못 보내면 다음 방식으로 보냄', () async { final hub1 = MemoryHub()..mailboxEnabled = false; final hub2 = MemoryHub(); final first = MemoryTransport(a, hub1); final second = MemoryTransport(a, hub2); final tb = MemoryTransport(b, hub2); final c = CompositeTransport([first, second]); await c.start(); await tb.start(); MemoryTransport(b, hub1); final got = await collect(tb, () async => expect(await c.send(envelope('1')), true)); expect(got.single.id, '1'); }); test('모두 못 보내면 false, 받은 것은 하나로 합쳐 줌', () async { final hub = MemoryHub()..mailboxEnabled = false; final t = MemoryTransport(a, hub); final c = CompositeTransport([t]); await c.start(); expect(await c.send(envelope('1')), false); final peer = MemoryTransport(b, hub); await peer.start(); final got = await collect(c, () async => await peer.send(envelope('2', from: b, to: a))); expect(got.single.id, '2'); }); }); group('직접 연결 (같은 네트워크)', () { test('주소를 알면 바로 보내고, 모르면 false', () async { DirectAddress? where; final recv = DirectTransport(resolve: (_) async => null); final send = DirectTransport(resolve: (_) async => where); await recv.start(); await send.start(); expect(await send.send(envelope('1')), false); where = DirectAddress('127.0.0.1', recv.boundPort); final got = await collect(recv, () async => expect(await send.send(envelope('2')), true)); expect(got.single.id, '2'); await recv.stop(); await send.stop(); }); }); group('외부 보관함 연결', () { Process? server; String? base; setUpAll(() async { try { (server, base) = await startServer([]); } catch (_) { server = null; // 파이썬이 없으면 이 묶음은 건너뜀 } }); tearDownAll(() => server?.kill()); test('보내고, 가져오고, 지움', () async { if (server == null) return; final ta = MailboxTransport(baseUrl: base!, myPhone: a, token: 'TA'); final tb = MailboxTransport(baseUrl: base!, myPhone: b, token: 'TB'); expect(await ta.send(envelope('1')), true); final got = await collect(tb, () async => expect(await tb.fetchPending(), 1)); expect(got.single.body, 'SEALED'); expect(await tb.fetchPending(), 0); // 받았다고 알렸으니 사라짐 await ta.stop(); await tb.stop(); }); test('다른 비밀값으로는 우편함을 못 염', () async { if (server == null) return; final first = MailboxTransport(baseUrl: base!, myPhone: '+821033334444', token: 'T1'); final other = MailboxTransport(baseUrl: base!, myPhone: '+821033334444', token: 'T2'); await first.fetchPending(); expect(await other.fetchPending(), 0); }); test('서버 시각을 알려 주고, 서버가 꺼져 있으면 보내기 실패', () async { if (server == null) return; final t = MailboxTransport(baseUrl: base!, myPhone: a, token: 'TA'); expect((await t.referenceStamp())!, greaterThan(170000000000)); final dead = MailboxTransport( baseUrl: 'http://127.0.0.1:1', myPhone: a, token: 'TA', timeout: const Duration(seconds: 1)); expect(await dead.send(envelope('1')), false); expect(await dead.referenceStamp(), null); }); }); group('늦게 만드는 연결', () { test('시작 전에는 보내기 실패, 시작하면 안쪽 연결로 전달', () async { final hub = MemoryHub(); final late = LateTransport(() async => MemoryTransport(a, hub)); final tb = MemoryTransport(b, hub); await tb.start(); expect(await late.send(envelope('0')), false); await late.start(); final got = await collect(tb, () async => expect(await late.send(envelope('1')), true)); expect(got.length, 1); await late.stop(); }); test('안쪽 연결이 없으면(null) 조용히 아무것도 안 함', () async { final late = LateTransport(() async => null); await late.start(); expect(await late.send(envelope('1')), false); expect(await late.fetchPending(), 0); expect(await late.referenceStamp(), null); }); test('받은 봉투를 바깥으로 흘려 준다', () async { final hub = MemoryHub(); final late = LateTransport(() async => MemoryTransport(a, hub)); final tb = MemoryTransport(b, hub); await late.start(); await tb.start(); final got = await collect(late, () async => expect(await tb.send(envelope('2', to: a)), true)); expect(got.length, 1); }); }); group('문자 확인', () { Process? server; String? base; late File smsLog; final token = 'c' * 32; setUpAll(() async { try { smsLog = File('${Directory.systemTemp.createTempSync('fm_sms').path}/sms.log'); (server, base) = await startServer(['--sms-log', smsLog.path]); } catch (_) { server = null; } }); tearDownAll(() => server?.kill()); String lastCode() => RegExp(r'\b(\d{6})\b').allMatches(smsLog.readAsStringSync()).last.group(1)!; test('요청하면 문자가 가고, 맞는 번호를 넣으면 확인되고 틀리면 거절', () async { if (server == null) return; final v = MailboxVerifier(baseUrl: base!, myPhone: a, token: token); expect(await v.requestCode(), VerifyStart.sent); expect(await v.confirm('000000' == lastCode() ? '111111' : '000000'), false); expect(await v.confirm(lastCode()), true); }); test('1시간에 3번이 넘으면 잠시 막힘', () async { if (server == null) return; final v = MailboxVerifier(baseUrl: base!, myPhone: b, token: token); for (var i = 0; i < 3; i++) { expect(await v.requestCode(), VerifyStart.sent); } expect(await v.requestCode(), VerifyStart.rateLimited); }); test('서버가 없으면 실패, 문자 확인을 안 쓰는 서버면 쓸 수 없음', () async { expect(await MailboxVerifier(baseUrl: 'http://127.0.0.1:1', myPhone: a, token: token).requestCode(), VerifyStart.failed); expect(await LatePhoneVerifier(() async => null).requestCode(), VerifyStart.unavailable); expect(await LatePhoneVerifier(() async => null).confirm('123456'), false); }); }); }