import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:test/test.dart'; import 'package:proto_socket/proto_socket.dart'; const _testPort = 19090; const _testPortSsl = 19091; const _testPortWs = 19092; const _testPortWss = 19093; const _host = '127.0.0.1'; const _certPath = 'test/certs/server.crt'; const _keyPath = 'test/certs/server.key'; bool _acceptTestCertificate(X509Certificate certificate) => certificate.pem == File(_certPath).readAsStringSync(); // ── 테스트 전용 픽스처 ────────────────────────────────────────── class _TestClient extends ProtobufClient { _TestClient(Socket socket) : super(socket, 5, 3, { TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer, }); } class _TestServer extends ProtobufServer { final receivedMessages = []; final connectedClients = []; _TestServer() : super(_host, _testPort, (socket) => _TestClient(socket)); @override void onClientConnected(ProtobufClient client) { connectedClients.add(client); client.addListener((data) => receivedMessages.add(data)); } } class _TestServerSsl extends ProtobufServer { final receivedMessages = []; final connectedClients = []; _TestServerSsl(SecurityContext ctx) : super.secure(_host, _testPortSsl, ctx, (socket) => _TestClient(socket)); @override void onClientConnected(ProtobufClient client) { connectedClients.add(client); client.addListener((data) => receivedMessages.add(data)); } } class _TestWsClient extends WsProtobufClient { _TestWsClient(WebSocket ws) : super(ws, 5, 3, { TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer, }); } class _TestWsServer extends WsProtobufServer { final receivedMessages = []; final connectedClients = []; _TestWsServer() : super(_host, _testPortWs, (ws) => _TestWsClient(ws)); @override void onClientConnected(WsProtobufClient client) { connectedClients.add(client); client.addListener((data) => receivedMessages.add(data)); } } class _TestWsServerSsl extends WsProtobufServer { final receivedMessages = []; final connectedClients = []; _TestWsServerSsl(SecurityContext ctx) : super.secure(_host, _testPortWss, ctx, (ws) => _TestWsClient(ws)); @override void onClientConnected(WsProtobufClient client) { connectedClients.add(client); client.addListener((data) => receivedMessages.add(data)); } } class _TestReqServer extends ProtobufServer { _TestReqServer() : super(_host, _testPort, (socket) => _TestClient(socket)); @override void onClientConnected(ProtobufClient client) { client.addRequestListener((req) async { return TestData() ..index = req.index * 2 ..message = 'echo: ${req.message}'; }); } } class _TestWsReqServer extends WsProtobufServer { _TestWsReqServer() : super(_host, _testPortWs, (ws) => _TestWsClient(ws)); @override void onClientConnected(WsProtobufClient client) { client.addRequestListener((req) async { return TestData() ..index = req.index * 2 ..message = 'echo: ${req.message}'; }); } } class _FastHeartbeatClient extends ProtobufClient { _FastHeartbeatClient(Socket socket) : super(socket, 1, 1, { TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer, }); } class _NoHeartbeatClient extends ProtobufClient { _NoHeartbeatClient(Socket socket) : super(socket, 5, 3, { TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer, }); @override void onHeartBeat(HeartBeat data) {} } class _NoHeartbeatServer extends ProtobufServer { _NoHeartbeatServer() : super(_host, _testPort, (socket) => _NoHeartbeatClient(socket)); @override void onClientConnected(ProtobufClient client) {} } class _FastHeartbeatWsClient extends WsProtobufClient { _FastHeartbeatWsClient(WebSocket ws) : super(ws, 1, 1, { TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer, }); } class _NoHeartbeatWsClient extends WsProtobufClient { _NoHeartbeatWsClient(WebSocket ws) : super(ws, 5, 3, { TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer, }); @override void onHeartBeat(HeartBeat data) {} } class _NoHeartbeatWsServer extends WsProtobufServer { _NoHeartbeatWsServer() : super(_host, _testPortWs, (ws) => _NoHeartbeatWsClient(ws)); @override void onClientConnected(WsProtobufClient client) {} } // ── 테스트 ────────────────────────────────────────────────────── void main() { // ── Plain TCP ──────────────────────────────────────────────── group('ProtobufServer (plain)', () { late _TestServer server; setUp(() async { server = _TestServer(); await server.start(); }); tearDown(() async { await server.stop(); }); test('서버가 정상 시작된다', () { expect(server.started, isTrue); expect(server.isSecure, isFalse); }); }); group('ProtobufClient (plain)', () { late _TestServer server; late _TestClient client; setUp(() async { server = _TestServer(); await server.start(); final socket = await ProtobufClient.connect(_host, _testPort); client = _TestClient(socket); await Future.delayed(const Duration(milliseconds: 100)); }); tearDown(() async { await client.close(); await Future.delayed(const Duration(milliseconds: 100)); await server.stop(); }); test('클라이언트가 서버에 연결된다', () { expect(server.connectedClients, isNotEmpty); }); test('TestData 메시지를 서버가 수신한다', () async { await client.send(TestData() ..index = 42 ..message = 'hello proto-socket'); await Future.delayed(const Duration(milliseconds: 200)); expect(server.receivedMessages, hasLength(1)); expect(server.receivedMessages.first.index, equals(42)); expect( server.receivedMessages.first.message, equals('hello proto-socket')); }); test('여러 메시지를 순서대로 수신한다', () async { for (var i = 0; i < 5; i++) { await client.send(TestData() ..index = i ..message = 'msg$i'); } await Future.delayed(const Duration(milliseconds: 300)); expect(server.receivedMessages, hasLength(5)); for (var i = 0; i < 5; i++) { expect(server.receivedMessages[i].index, equals(i)); } }); test('서버에서 클라이언트로 메시지를 전송한다', () async { final completer = Completer(); client.addListener((data) => completer.complete(data)); await Future.delayed(const Duration(milliseconds: 100)); await server.connectedClients.first.send(TestData() ..index = 99 ..message = 'from server'); final received = await completer.future.timeout(const Duration(seconds: 2)); expect(received.index, equals(99)); expect(received.message, equals('from server')); }); test('nonce가 송신마다 증가한다', () async { for (var i = 1; i <= 3; i++) { await client.send(TestData()..index = i); } await Future.delayed(const Duration(milliseconds: 200)); expect(server.receivedMessages, hasLength(3)); }); test('클라이언트 disconnect 시 서버 콜백이 호출된다', () async { final completer = Completer(); server.connectedClients.first.addDisconnectListener((_) { if (!completer.isCompleted) completer.complete(); }); await client.close(); await completer.future.timeout(const Duration(seconds: 2)); expect(completer.isCompleted, isTrue); }); test('서버 stop 시 클라이언트 disconnect 콜백이 호출된다', () async { final completer = Completer(); client.addDisconnectListener((_) { if (!completer.isCompleted) completer.complete(); }); await server.stop(); await completer.future.timeout(const Duration(seconds: 2)); expect(client.isAlive, isFalse); }); test('HeartBeat interval 동안 연결이 유지된다', () async { await Future.delayed(const Duration(seconds: 2)); await client.send(TestData() ..index = 1 ..message = 'alive check'); await Future.delayed(const Duration(milliseconds: 200)); expect(server.receivedMessages, hasLength(1)); }); test('close 후 isAlive가 false다', () async { await client.close(); await Future.delayed(const Duration(milliseconds: 100)); expect(client.isAlive, isFalse); }); test('서버가 모든 클라이언트에게 브로드캐스트한다', () async { final socket = await ProtobufClient.connect(_host, _testPort); final secondClient = _TestClient(socket); try { await Future.delayed(const Duration(milliseconds: 100)); final firstReceived = Completer(); final secondReceived = Completer(); client.addListener((data) { if (!firstReceived.isCompleted) firstReceived.complete(data); }); secondClient.addListener((data) { if (!secondReceived.isCompleted) secondReceived.complete(data); }); await server.broadcast(TestData() ..index = 77 ..message = 'broadcast'); final first = await firstReceived.future.timeout(const Duration(seconds: 2)); final second = await secondReceived.future.timeout(const Duration(seconds: 2)); expect(first.message, equals('broadcast')); expect(second.message, equals('broadcast')); } finally { await secondClient.close(); } }); test('close 후 send는 무시된다', () async { await client.close(); await Future.delayed(const Duration(milliseconds: 100)); await expectLater( client.send(TestData() ..index = 1 ..message = 'ignored'), completes, ); expect(client.isAlive, isFalse); }); test('TCP closes on oversized packet length', () async { final rawSocket = await Socket.connect(_host, _testPort); try { await Future.delayed(const Duration(milliseconds: 100)); final serverClient = server.connectedClients.last; final disconnected = Completer(); serverClient.addDisconnectListener((_) { if (!disconnected.isCompleted) disconnected.complete(); }); final header = Uint8List(4) ..buffer.asByteData().setInt32(0, maxPacketSize + 1); rawSocket.add(header); await rawSocket.flush(); await disconnected.future.timeout(const Duration(seconds: 2)); expect(serverClient.isAlive, isFalse); } finally { rawSocket.destroy(); } }); }); group('Heartbeat timeout', () { test('TCP heartbeat 타임아웃 시 disconnect 콜백이 호출된다', () async { final server = _NoHeartbeatServer(); await server.start(); final socket = await ProtobufClient.connect(_host, _testPort); final client = _FastHeartbeatClient(socket); final completer = Completer(); client.addDisconnectListener((_) { if (!completer.isCompleted) completer.complete(); }); try { await completer.future.timeout(const Duration(seconds: 4)); expect(client.isAlive, isFalse); } finally { await client.close(); await server.stop(); } }); test('WS heartbeat 타임아웃 시 disconnect 콜백이 호출된다', () async { final server = _NoHeartbeatWsServer(); await server.start(); final ws = await WsProtobufClient.connect(_host, _testPortWs); final client = _FastHeartbeatWsClient(ws); final completer = Completer(); client.addDisconnectListener((_) { if (!completer.isCompleted) completer.complete(); }); try { await completer.future.timeout(const Duration(seconds: 4)); expect(client.isAlive, isFalse); } finally { await client.close(); await server.stop(); } }); }); // ── SSL/TLS ────────────────────────────────────────────────── group('ProtobufServer (SSL)', () { late _TestServerSsl server; setUp(() async { final ctx = SecurityContext() ..useCertificateChain(_certPath) ..usePrivateKey(_keyPath); server = _TestServerSsl(ctx); await server.start(); }); tearDown(() async { await server.stop(); }); test('SSL 서버가 정상 시작된다', () { expect(server.started, isTrue); expect(server.isSecure, isTrue); }); }); group('ProtobufClient (SSL)', () { late _TestServerSsl server; late _TestClient client; setUp(() async { final serverCtx = SecurityContext() ..useCertificateChain(_certPath) ..usePrivateKey(_keyPath); server = _TestServerSsl(serverCtx); await server.start(); final clientCtx = SecurityContext()..setTrustedCertificates(_certPath); final socket = await ProtobufClient.connectSecure( _host, _testPortSsl, context: clientCtx, onBadCertificate: _acceptTestCertificate, ); client = _TestClient(socket); await Future.delayed(const Duration(milliseconds: 100)); }); tearDown(() async { await client.close(); await Future.delayed(const Duration(milliseconds: 100)); await server.stop(); }); test('SSL 클라이언트가 서버에 연결된다', () { expect(server.connectedClients, isNotEmpty); }); test('SSL TestData 메시지를 서버가 수신한다', () async { await client.send(TestData() ..index = 7 ..message = 'hello over ssl'); await Future.delayed(const Duration(milliseconds: 200)); expect(server.receivedMessages, hasLength(1)); expect(server.receivedMessages.first.index, equals(7)); expect(server.receivedMessages.first.message, equals('hello over ssl')); }); test('SSL 서버에서 클라이언트로 메시지를 전송한다', () async { final completer = Completer(); client.addListener((data) => completer.complete(data)); await Future.delayed(const Duration(milliseconds: 100)); await server.connectedClients.first.send(TestData() ..index = 55 ..message = 'from ssl server'); final received = await completer.future.timeout(const Duration(seconds: 2)); expect(received.index, equals(55)); expect(received.message, equals('from ssl server')); }); test('SSL disconnect 시 서버 콜백이 호출된다', () async { final completer = Completer(); server.connectedClients.first.addDisconnectListener((_) { if (!completer.isCompleted) completer.complete(); }); await client.close(); await completer.future.timeout(const Duration(seconds: 2)); expect(completer.isCompleted, isTrue); }); }); // ── WebSocket ──────────────────────────────────────────────── group('WsProtobufServer (plain)', () { late _TestWsServer server; setUp(() async { server = _TestWsServer(); await server.start(); }); tearDown(() async { await server.stop(); }); test('WS 서버가 정상 시작된다', () { expect(server.started, isTrue); expect(server.isSecure, isFalse); }); }); group('WsProtobufClient (plain)', () { late _TestWsServer server; late _TestWsClient client; setUp(() async { server = _TestWsServer(); await server.start(); final ws = await WsProtobufClient.connect(_host, _testPortWs); client = _TestWsClient(ws); await Future.delayed(const Duration(milliseconds: 100)); }); tearDown(() async { await client.close(); await Future.delayed(const Duration(milliseconds: 100)); await server.stop(); }); test('WS 클라이언트가 서버에 연결된다', () { expect(server.connectedClients, isNotEmpty); }); test('WS TestData 메시지를 서버가 수신한다', () async { await client.send(TestData() ..index = 42 ..message = 'hello ws'); await Future.delayed(const Duration(milliseconds: 200)); expect(server.receivedMessages, hasLength(1)); expect(server.receivedMessages.first.index, equals(42)); expect(server.receivedMessages.first.message, equals('hello ws')); }); test('WS 서버에서 클라이언트로 메시지를 전송한다', () async { final completer = Completer(); client.addListener((data) => completer.complete(data)); await Future.delayed(const Duration(milliseconds: 100)); await server.connectedClients.first.send(TestData() ..index = 99 ..message = 'from ws server'); final received = await completer.future.timeout(const Duration(seconds: 2)); expect(received.index, equals(99)); expect(received.message, equals('from ws server')); }); test('WS 클라이언트 disconnect 시 서버 콜백이 호출된다', () async { final completer = Completer(); server.connectedClients.first.addDisconnectListener((_) { if (!completer.isCompleted) completer.complete(); }); await client.close(); await completer.future.timeout(const Duration(seconds: 2)); expect(completer.isCompleted, isTrue); }); test('WS 서버 stop 시 클라이언트 disconnect 콜백이 호출된다', () async { final completer = Completer(); client.addDisconnectListener((_) { if (!completer.isCompleted) completer.complete(); }); await server.stop(); await completer.future.timeout(const Duration(seconds: 2)); expect(client.isAlive, isFalse); }); test('WS close 후 isAlive가 false다', () async { await client.close(); await Future.delayed(const Duration(milliseconds: 100)); expect(client.isAlive, isFalse); }); test('WS 서버가 모든 클라이언트에게 브로드캐스트한다', () async { final ws = await WsProtobufClient.connect(_host, _testPortWs); final secondClient = _TestWsClient(ws); try { await Future.delayed(const Duration(milliseconds: 100)); final firstReceived = Completer(); final secondReceived = Completer(); client.addListener((data) { if (!firstReceived.isCompleted) firstReceived.complete(data); }); secondClient.addListener((data) { if (!secondReceived.isCompleted) secondReceived.complete(data); }); await server.broadcast(TestData() ..index = 88 ..message = 'ws broadcast'); final first = await firstReceived.future.timeout(const Duration(seconds: 2)); final second = await secondReceived.future.timeout(const Duration(seconds: 2)); expect(first.message, equals('ws broadcast')); expect(second.message, equals('ws broadcast')); } finally { await secondClient.close(); } }); test('WS close 후 send는 무시된다', () async { await client.close(); await Future.delayed(const Duration(milliseconds: 100)); await expectLater( client.send(TestData() ..index = 1 ..message = 'ignored'), completes, ); expect(client.isAlive, isFalse); }); }); // ── WebSocket SSL ───────────────────────────────────────────── group('WsProtobufServer (SSL)', () { late _TestWsServerSsl server; setUp(() async { final ctx = SecurityContext() ..useCertificateChain(_certPath) ..usePrivateKey(_keyPath); server = _TestWsServerSsl(ctx); await server.start(); }); tearDown(() async { await server.stop(); }); test('WSS 서버가 정상 시작된다', () { expect(server.started, isTrue); expect(server.isSecure, isTrue); }); }); group('WsProtobufClient (SSL)', () { late _TestWsServerSsl server; late _TestWsClient client; setUp(() async { final serverCtx = SecurityContext() ..useCertificateChain(_certPath) ..usePrivateKey(_keyPath); server = _TestWsServerSsl(serverCtx); await server.start(); final clientCtx = SecurityContext()..setTrustedCertificates(_certPath); final ws = await WsProtobufClient.connectSecure( _host, _testPortWss, context: clientCtx, onBadCertificate: _acceptTestCertificate, ); client = _TestWsClient(ws); await Future.delayed(const Duration(milliseconds: 100)); }); tearDown(() async { await client.close(); await Future.delayed(const Duration(milliseconds: 100)); await server.stop(); }); test('WSS 클라이언트가 서버에 연결된다', () { expect(server.connectedClients, isNotEmpty); }); test('WSS TestData 메시지를 서버가 수신한다', () async { await client.send(TestData() ..index = 7 ..message = 'hello over wss'); await Future.delayed(const Duration(milliseconds: 200)); expect(server.receivedMessages, hasLength(1)); expect(server.receivedMessages.first.index, equals(7)); expect(server.receivedMessages.first.message, equals('hello over wss')); }); test('WSS disconnect 시 서버 콜백이 호출된다', () async { final completer = Completer(); server.connectedClients.first.addDisconnectListener((_) { if (!completer.isCompleted) completer.complete(); }); await client.close(); await completer.future.timeout(const Duration(seconds: 2)); expect(completer.isCompleted, isTrue); }); }); // ── Request-Response (TCP) ──────────────────────────────────── group('Request-Response (TCP plain)', () { late _TestReqServer server; late _TestClient client; setUp(() async { server = _TestReqServer(); await server.start(); final socket = await ProtobufClient.connect(_host, _testPort); client = _TestClient(socket); await Future.delayed(const Duration(milliseconds: 100)); }); tearDown(() async { await client.close(); await Future.delayed(const Duration(milliseconds: 100)); await server.stop(); }); test('sendRequest로 서버 응답을 받는다', () async { final response = await client .sendRequest(TestData() ..index = 21 ..message = 'hello') .timeout(const Duration(seconds: 2)); expect(response.index, equals(42)); expect(response.message, equals('echo: hello')); }); test('여러 sendRequest가 각각 올바른 응답을 받는다', () async { final futures = [ client.sendRequest(TestData() ..index = 1 ..message = 'a'), client.sendRequest(TestData() ..index = 2 ..message = 'b'), client.sendRequest(TestData() ..index = 3 ..message = 'c'), ]; final results = await Future.wait(futures).timeout(const Duration(seconds: 3)); expect(results[0].index, equals(2)); expect(results[1].index, equals(4)); expect(results[2].index, equals(6)); expect(results[0].message, equals('echo: a')); expect(results[1].message, equals('echo: b')); expect(results[2].message, equals('echo: c')); }); }); // ── Request-Response (WS) ───────────────────────────────────── group('Request-Response (WS plain)', () { late _TestWsReqServer server; late _TestWsClient client; setUp(() async { server = _TestWsReqServer(); await server.start(); final ws = await WsProtobufClient.connect(_host, _testPortWs); client = _TestWsClient(ws); await Future.delayed(const Duration(milliseconds: 100)); }); tearDown(() async { await client.close(); await Future.delayed(const Duration(milliseconds: 100)); await server.stop(); }); test('WS sendRequest로 서버 응답을 받는다', () async { final response = await client .sendRequest(TestData() ..index = 21 ..message = 'hello ws') .timeout(const Duration(seconds: 2)); expect(response.index, equals(42)); expect(response.message, equals('echo: hello ws')); }); test('WS 여러 sendRequest가 각각 올바른 응답을 받는다', () async { final futures = [ client.sendRequest(TestData() ..index = 1 ..message = 'x'), client.sendRequest(TestData() ..index = 2 ..message = 'y'), client.sendRequest(TestData() ..index = 3 ..message = 'z'), ]; final results = await Future.wait(futures).timeout(const Duration(seconds: 3)); expect(results[0].index, equals(2)); expect(results[1].index, equals(4)); expect(results[2].index, equals(6)); }); test('TCP inbound backpressure pauses subscription on heavy load', () async { final server = _TestBackpressureServer(); final completer = Completer(); server.handlerCompleter = completer; await server.start(); final rawSocket = await ProtobufClient.connect(_host, _testPort); final client = _TestClient(rawSocket); final requestFuture = client.sendRequest( TestData()..index = 1..message = 'first', ); await Future.delayed(Duration.zero); for (var i = 2; i <= 65; i++) { await client.send(TestData()..index = i..message = 'heavy'); } await client.send(TestData()..index = 66..message = 'overflow'); await Future.delayed(const Duration(milliseconds: 100)); final srvClient = server.connectedClients.single; expect(srvClient.isSourcePaused, isTrue); completer.complete(TestData()..index = 100..message = 'done'); await requestFuture; await Future.delayed(const Duration(milliseconds: 50)); expect(srvClient.isSourcePaused, isFalse); await client.close(); await server.stop(); }); }); } class _TestBackpressureServer extends ProtobufServer { final connectedClients = []; Completer? handlerCompleter; _TestBackpressureServer() : super(_host, _testPort, (socket) => _TestClient(socket)); @override void onClientConnected(ProtobufClient client) { connectedClients.add(client); client.addRequestListener((req) async { return handlerCompleter!.future; }); } }