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/contracts/alt_api_result.dart'; import 'package:alt_client/src/generated/alt/v1/common.pb.dart'; import 'package:alt_client/src/generated/alt/v1/backtest.pb.dart'; import 'package:alt_client/src/integrations/socket/alt_socket_client.dart'; import 'package:alt_client/src/features/backtest/data/backtest_repository.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('BacktestRepository tests', () { late FakeWebSocket fakeWs; late AltSocketClient client; late BacktestRepository repository; setUp(() { fakeWs = FakeWebSocket(); client = AltSocketClient( fakeWs, heartbeatIntervalTime: 10, heartbeatWaitTime: 10, ); repository = BacktestRepository(client); }); tearDown(() async { await client.close(); }); 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 success flows', () async { final request = ListBacktestRunsRequest() ..status = BacktestRunStatus.BACKTEST_RUN_STATUS_RUNNING; final expectedResponse = ListBacktestRunsResponse() ..runs.add( BacktestRun() ..id = 'run-1' ..spec = (BacktestRunSpec()..strategyId = 'strat-1'), ); final future = repository.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_RUNNING), ); }, responseTypeName: ListBacktestRunsResponse.getDefault().info_.qualifiedMessageName, responseData: expectedResponse.writeToBuffer(), ); final result = await future; expect(result, isA>()); final val = (result as AltApiSuccess).value; expect(val.runs, hasLength(1)); expect(val.runs.first.id, equals('run-1')); }); test('getBacktestRunDetail success flows', () async { final request = GetBacktestRunDetailRequest()..runId = 'run-1'; final expectedResponse = GetBacktestRunDetailResponse() ..run = (BacktestRun() ..id = 'run-1' ..spec = (BacktestRunSpec()..strategyId = 'strat-1')); final future = repository.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-1')); }, responseTypeName: GetBacktestRunDetailResponse.getDefault() .info_ .qualifiedMessageName, responseData: expectedResponse.writeToBuffer(), ); final result = await future; expect(result, isA>()); final val = (result as AltApiSuccess).value; expect(val.run.id, equals('run-1')); }); test('getBacktestResult success flows', () async { final request = GetBacktestResultRequest()..runId = 'run-1'; final expectedResponse = GetBacktestResultResponse() ..result = (BacktestResult() ..runId = 'run-1' ..summary = (BacktestSummaryMetrics()..tradeCount = 42)); final future = repository.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-1')); }, responseTypeName: GetBacktestResultResponse.getDefault().info_.qualifiedMessageName, responseData: expectedResponse.writeToBuffer(), ); final result = await future; expect(result, isA>()); final val = (result as AltApiSuccess).value; expect(val.result.runId, equals('run-1')); expect(val.result.summary.tradeCount, equals(42)); }); test('startBacktest success flows', () async { final request = StartBacktestRequest() ..spec = (BacktestRunSpec() ..strategyId = 'strat-1' ..market = Market.MARKET_US); final expectedResponse = StartBacktestResponse() ..run = (BacktestRun()..id = 'run-new'); final future = repository.startBacktest( request, timeout: const Duration(seconds: 1), ); await roundTrip( trigger: () => future, onSent: (sentPacket) { expect( sentPacket.typeName, equals( StartBacktestRequest.getDefault().info_.qualifiedMessageName, ), ); final sentRequest = StartBacktestRequest.fromBuffer(sentPacket.data); expect(sentRequest.spec.strategyId, equals('strat-1')); expect(sentRequest.spec.market, equals(Market.MARKET_US)); }, responseTypeName: StartBacktestResponse.getDefault().info_.qualifiedMessageName, responseData: expectedResponse.writeToBuffer(), ); final result = await future; expect(result, isA>()); final val = (result as AltApiSuccess).value; expect(val.run.id, equals('run-new')); }); test('ErrorInfo maps correctly to AltApiFailure across calls', () async { final request = StartBacktestRequest() ..spec = (BacktestRunSpec()..strategyId = 'invalid-strat'); final expectedResponse = StartBacktestResponse() ..error = ErrorInfo( code: 'STRATEGY_NOT_FOUND', message: 'no strategy with id invalid-strat', ); final future = repository.startBacktest( request, timeout: const Duration(seconds: 1), ); await roundTrip( trigger: () => future, onSent: (sentPacket) {}, responseTypeName: StartBacktestResponse.getDefault().info_.qualifiedMessageName, responseData: expectedResponse.writeToBuffer(), ); final result = await future; expect(result, isA>()); final failure = result as AltApiFailure; expect(failure.error.code, equals('STRATEGY_NOT_FOUND')); expect( failure.error.message, equals('no strategy with id invalid-strat'), ); }); test('timeout maps correctly to AltTransportKind.timeout', () async { final request = StartBacktestRequest(); final result = await repository.startBacktest( request, timeout: const Duration(milliseconds: 10), ); expect(result, isA>()); final failure = result as AltTransportFailure; expect(failure.kind, equals(AltTransportKind.timeout)); }); test( 'type mismatch maps correctly to AltTransportKind.decodeFailure', () async { final request = StartBacktestRequest(); final future = repository.startBacktest( request, timeout: const Duration(seconds: 1), ); await roundTrip( trigger: () => future, onSent: (sentPacket) {}, responseTypeName: HelloResponse.getDefault().info_.qualifiedMessageName, responseData: HelloResponse().writeToBuffer(), ); final result = await future; expect(result, isA>()); final failure = result as AltTransportFailure; expect(failure.kind, equals(AltTransportKind.decodeFailure)); expect(failure.detail, contains('Bad state')); }, ); test('client not alive returns AltTransportKind.disconnected', () async { await client.close(); final result = await repository.startBacktest(StartBacktestRequest()); expect(result, isA>()); final failure = result as AltTransportFailure; expect(failure.kind, equals(AltTransportKind.disconnected)); expect(failure.detail, contains('client is not alive')); }); }); }