import 'dart:async'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:proto_socket/proto_socket.dart'; import 'package:alt_client/src/generated/alt/v1/backtest.pb.dart'; import 'package:alt_client/src/generated/alt/v1/common.pb.dart'; import 'package:alt_client/src/integrations/socket/alt_socket_client.dart'; import 'package:alt_client/src/integrations/socket/socket_endpoint.dart'; class FakeWebSocket extends Stream implements WebSocket { final StreamController _controller = StreamController(); final List> sentBytes = []; bool isClosed = false; @override void add(dynamic data) { if (data is List) { sentBytes.add(data); } } @override Future close([int? code, String? reason]) async { isClosed = true; await _controller.close(); } void feedFromServer(List data) { _controller.add(data); } @override StreamSubscription listen( void Function(dynamic event)? onData, { Function? onError, void Function()? onDone, bool? cancelOnError, }) { return _controller.stream.listen( onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError, ); } @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } void main() { group('AltSocketEndpoint tests', () { test('default constructor values', () { const endpoint = AltSocketEndpoint(); expect(endpoint.host, equals('127.0.0.1')); expect(endpoint.port, equals(8080)); expect(endpoint.path, equals('/socket')); expect(endpoint.toString(), equals('ws://127.0.0.1:8080/socket')); }); test('custom values', () { const endpoint = AltSocketEndpoint( host: 'api.example.com', port: 443, path: '/ws', ); expect(endpoint.host, equals('api.example.com')); expect(endpoint.port, equals(443)); expect(endpoint.path, equals('/ws')); expect(endpoint.toString(), equals('ws://api.example.com:443/ws')); }); test('equality and hashCode', () { const e1 = AltSocketEndpoint(host: '1.2.3.4', port: 9000, path: '/a'); const e2 = AltSocketEndpoint(host: '1.2.3.4', port: 9000, path: '/a'); const e3 = AltSocketEndpoint(host: '1.2.3.4', port: 9000, path: '/b'); expect(e1, equals(e2)); expect(e1.hashCode, equals(e2.hashCode)); expect(e1, isNot(equals(e3))); }); }); group('AltSocketClient tests', () { late FakeWebSocket fakeWs; late AltSocketClient client; setUp(() { fakeWs = FakeWebSocket(); client = AltSocketClient( fakeWs, heartbeatIntervalTime: 1, heartbeatWaitTime: 1, ); }); tearDown(() async { await client.close(); }); test('client initialization and parser registration', () { expect(client.isAlive, isTrue); final helloRequestType = HelloRequest.getDefault().info_.qualifiedMessageName; final helloResponseType = HelloResponse.getDefault().info_.qualifiedMessageName; expect(client.getGenerator(helloRequestType), isNotNull); expect(client.getGenerator(helloResponseType), isNotNull); }); test('hello handshake request-response loop', () async { final helloFuture = client.hello(timeout: const Duration(seconds: 1)); await Future.delayed(Duration.zero); expect(fakeWs.sentBytes, isNotEmpty); final firstSentPacketBytes = fakeWs.sentBytes.first; final sentPacket = PacketBase.fromBuffer(firstSentPacketBytes); final helloRequestType = HelloRequest.getDefault().info_.qualifiedMessageName; expect(sentPacket.typeName, equals(helloRequestType)); final sentHelloRequest = HelloRequest.fromBuffer(sentPacket.data); expect(sentHelloRequest.clientName, equals('alt-client')); expect(sentHelloRequest.clientVersion, equals('dev')); expect(sentHelloRequest.altProtocolVersion, equals('alt.v1')); final expectedResponse = HelloResponse() ..serverName = 'alt-server' ..serverVersion = '1.0.0' ..altProtocolVersion = 'alt.v1'; final responsePacket = PacketBase() ..typeName = HelloResponse.getDefault().info_.qualifiedMessageName ..nonce = 100 ..responseNonce = sentPacket.nonce ..data = expectedResponse.writeToBuffer(); fakeWs.feedFromServer(responsePacket.writeToBuffer()); final response = await helloFuture; expect(response.serverName, equals('alt-server')); expect(response.serverVersion, equals('1.0.0')); expect(response.altProtocolVersion, equals('alt.v1')); }); // Drains the request sent by [trigger], asserts the sent packet type/payload // through [onSent], then feeds a matching response packet back so the worker // -> API -> client backtest boundary round-trips consistently. Future roundTrip({ required Future Function() trigger, required void Function(PacketBase sentPacket) onSent, required String responseTypeName, required List responseData, }) async { final sentCountBefore = fakeWs.sentBytes.length; unawaited(trigger()); await Future.delayed(Duration.zero); expect(fakeWs.sentBytes.length, greaterThan(sentCountBefore)); final sentPacket = PacketBase.fromBuffer(fakeWs.sentBytes.last); onSent(sentPacket); final responsePacket = PacketBase() ..typeName = responseTypeName ..nonce = 200 ..responseNonce = sentPacket.nonce ..data = responseData; fakeWs.feedFromServer(responsePacket.writeToBuffer()); return sentPacket; } test('listBacktestRuns request-response loop', () async { final request = ListBacktestRunsRequest() ..status = BacktestRunStatus.BACKTEST_RUN_STATUS_SUCCEEDED; final expectedResponse = ListBacktestRunsResponse() ..runs.add(BacktestRun() ..id = 'run-1' ..status = BacktestRunStatus.BACKTEST_RUN_STATUS_SUCCEEDED); final future = client.listBacktestRuns( request, timeout: const Duration(seconds: 1), ); await roundTrip( trigger: () => future, onSent: (sentPacket) { expect( sentPacket.typeName, equals( ListBacktestRunsRequest.getDefault().info_.qualifiedMessageName, ), ); final sentRequest = ListBacktestRunsRequest.fromBuffer(sentPacket.data); expect( sentRequest.status, equals(BacktestRunStatus.BACKTEST_RUN_STATUS_SUCCEEDED), ); }, responseTypeName: ListBacktestRunsResponse.getDefault().info_.qualifiedMessageName, responseData: expectedResponse.writeToBuffer(), ); final response = await future; expect(response.runs, hasLength(1)); expect(response.runs.first.id, equals('run-1')); expect( response.runs.first.status, equals(BacktestRunStatus.BACKTEST_RUN_STATUS_SUCCEEDED), ); }); test('getBacktestRunDetail request-response loop', () async { final request = GetBacktestRunDetailRequest()..runId = 'run-7'; final expectedResponse = GetBacktestRunDetailResponse() ..run = (BacktestRun()..id = 'run-7') ..result = (BacktestResult()..runId = 'run-7'); final future = client.getBacktestRunDetail( request, timeout: const Duration(seconds: 1), ); await roundTrip( trigger: () => future, onSent: (sentPacket) { expect( sentPacket.typeName, equals( GetBacktestRunDetailRequest.getDefault() .info_ .qualifiedMessageName, ), ); final sentRequest = GetBacktestRunDetailRequest.fromBuffer(sentPacket.data); expect(sentRequest.runId, equals('run-7')); }, responseTypeName: GetBacktestRunDetailResponse.getDefault() .info_ .qualifiedMessageName, responseData: expectedResponse.writeToBuffer(), ); final response = await future; expect(response.run.id, equals('run-7')); expect(response.result.runId, equals('run-7')); }); test('getBacktestResult request-response loop', () async { final request = GetBacktestResultRequest()..runId = 'run-9'; final expectedResponse = GetBacktestResultResponse() ..result = (BacktestResult()..runId = 'run-9'); final future = client.getBacktestResult( request, timeout: const Duration(seconds: 1), ); await roundTrip( trigger: () => future, onSent: (sentPacket) { expect( sentPacket.typeName, equals( GetBacktestResultRequest.getDefault().info_.qualifiedMessageName, ), ); final sentRequest = GetBacktestResultRequest.fromBuffer(sentPacket.data); expect(sentRequest.runId, equals('run-9')); }, responseTypeName: GetBacktestResultResponse.getDefault().info_.qualifiedMessageName, responseData: expectedResponse.writeToBuffer(), ); final response = await future; expect(response.result.runId, equals('run-9')); }); test('compareBacktestRuns request-response loop', () async { final request = CompareBacktestRunsRequest() ..runIds.addAll(['run-1', 'run-2']); final expectedResponse = CompareBacktestRunsResponse() ..results.add(BacktestResult()..runId = 'run-1') ..results.add(BacktestResult()..runId = 'run-2'); final future = client.compareBacktestRuns( request, timeout: const Duration(seconds: 1), ); await roundTrip( trigger: () => future, onSent: (sentPacket) { expect( sentPacket.typeName, equals( CompareBacktestRunsRequest.getDefault().info_.qualifiedMessageName, ), ); final sentRequest = CompareBacktestRunsRequest.fromBuffer(sentPacket.data); expect(sentRequest.runIds, equals(['run-1', 'run-2'])); }, responseTypeName: CompareBacktestRunsResponse.getDefault().info_.qualifiedMessageName, responseData: expectedResponse.writeToBuffer(), ); final response = await future; expect(response.results, hasLength(2)); expect( response.results.map((r) => r.runId), equals(['run-1', 'run-2']), ); }); }); }