alt/apps/client/test/features/backtest/backtest_repository_test.dart

337 lines
11 KiB
Dart

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<dynamic> implements WebSocket {
final StreamController<dynamic> _controller = StreamController<dynamic>();
final List<List<int>> sentBytes = [];
bool isClosed = false;
@override
void add(dynamic data) {
if (data is List<int>) {
sentBytes.add(data);
}
}
@override
Future<void> close([int? code, String? reason]) async {
isClosed = true;
await _controller.close();
}
void feedFromServer(List<int> data) {
_controller.add(data);
}
@override
StreamSubscription<dynamic> 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<PacketBase> roundTrip({
required Future<void> Function() trigger,
required void Function(PacketBase sentPacket) onSent,
required String responseTypeName,
required List<int> responseData,
}) async {
final sentCountBefore = fakeWs.sentBytes.length;
unawaited(trigger());
await Future<void>.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<AltApiSuccess<ListBacktestRunsResponse>>());
final val = (result as AltApiSuccess<ListBacktestRunsResponse>).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<AltApiSuccess<GetBacktestRunDetailResponse>>());
final val = (result as AltApiSuccess<GetBacktestRunDetailResponse>).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<AltApiSuccess<GetBacktestResultResponse>>());
final val = (result as AltApiSuccess<GetBacktestResultResponse>).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<AltApiSuccess<StartBacktestResponse>>());
final val = (result as AltApiSuccess<StartBacktestResponse>).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<AltApiFailure<StartBacktestResponse>>());
final failure = result as AltApiFailure<StartBacktestResponse>;
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<AltTransportFailure<StartBacktestResponse>>());
final failure = result as AltTransportFailure<StartBacktestResponse>;
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<AltTransportFailure<StartBacktestResponse>>());
final failure = result as AltTransportFailure<StartBacktestResponse>;
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<AltTransportFailure<StartBacktestResponse>>());
final failure = result as AltTransportFailure<StartBacktestResponse>;
expect(failure.kind, equals(AltTransportKind.disconnected));
expect(failure.detail, contains('client is not alive'));
});
});
}