- ProtobufServer: named constructor .secure() for SSL mode - SecurityContext parameter for certificate/key loading - SecureServerSocket 분기 처리, stop()에서 양쪽 close - isSecure getter 추가 - ProtobufClient: static factory 메서드 추가 - connect() — plain TCP - connectSecure() — SSL/TLS (SecureSocket extends Socket이라 내부 무수정) - 테스트: SSL 그룹 4개 추가 (총 13개, 전부 통과) - 자체 서명 인증서로 실제 SSL 연결 검증 - plain / SSL 동일 시나리오 병렬 커버 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
252 lines
8 KiB
Dart
252 lines
8 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:test/test.dart';
|
|
import 'package:toki_socket/toki_socket.dart';
|
|
|
|
const _testPort = 19090;
|
|
const _testPortSsl = 19091;
|
|
const _host = 'localhost';
|
|
const _certPath = 'test/certs/server.crt';
|
|
const _keyPath = 'test/certs/server.key';
|
|
|
|
// ── 테스트 전용 픽스처 ──────────────────────────────────────────
|
|
|
|
class _TestClient extends ProtobufClient {
|
|
_TestClient(Socket socket)
|
|
: super(socket, 5, 3, {
|
|
TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer,
|
|
});
|
|
}
|
|
|
|
class _TestServer extends ProtobufServer {
|
|
final receivedMessages = <TestData>[];
|
|
final connectedClients = <ProtobufClient>[];
|
|
|
|
_TestServer() : super(_host, _testPort, (socket) => _TestClient(socket));
|
|
|
|
@override
|
|
void onClientConnected(ProtobufClient client) {
|
|
connectedClients.add(client);
|
|
client.addListener<TestData>((data) => receivedMessages.add(data));
|
|
}
|
|
}
|
|
|
|
class _TestServerSsl extends ProtobufServer {
|
|
final receivedMessages = <TestData>[];
|
|
final connectedClients = <ProtobufClient>[];
|
|
|
|
_TestServerSsl(SecurityContext ctx)
|
|
: super.secure(_host, _testPortSsl, ctx, (socket) => _TestClient(socket));
|
|
|
|
@override
|
|
void onClientConnected(ProtobufClient client) {
|
|
connectedClients.add(client);
|
|
client.addListener<TestData>((data) => receivedMessages.add(data));
|
|
}
|
|
}
|
|
|
|
// ── 테스트 ──────────────────────────────────────────────────────
|
|
|
|
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 {
|
|
client.dispose();
|
|
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 toki-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 toki-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<TestData>();
|
|
client.addListener<TestData>((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<void>();
|
|
server.connectedClients.first.addDisconnectListener((_) {
|
|
if (!completer.isCompleted) completer.complete();
|
|
});
|
|
|
|
client.dispose();
|
|
await completer.future.timeout(const Duration(seconds: 2));
|
|
expect(completer.isCompleted, isTrue);
|
|
});
|
|
|
|
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));
|
|
});
|
|
});
|
|
|
|
// ── 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,
|
|
);
|
|
client = _TestClient(socket);
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
});
|
|
|
|
tearDown(() async {
|
|
client.dispose();
|
|
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<TestData>();
|
|
client.addListener<TestData>((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<void>();
|
|
server.connectedClients.first.addDisconnectListener((_) {
|
|
if (!completer.isCompleted) completer.complete();
|
|
});
|
|
|
|
client.dispose();
|
|
await completer.future.timeout(const Duration(seconds: 2));
|
|
expect(completer.isCompleted, isTrue);
|
|
});
|
|
});
|
|
}
|