128 lines
4.2 KiB
Dart
128 lines
4.2 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:ikar/backend/api.dart';
|
|
import 'package:ikar/backend/ikar_transport.dart';
|
|
import 'package:ikar/core/protocol/opcode_map.dart';
|
|
import 'package:ikar/core/protocol/packet.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
|
|
|
test(
|
|
'repeated failed connections stay quiet and recover to online',
|
|
() async {
|
|
var available = false;
|
|
final api = Api(
|
|
transportFactory: (uri) => IkarTransport(
|
|
uri,
|
|
client: MockClient((request) async {
|
|
if (!available) return http.Response('Gateway unavailable', 503);
|
|
return http.Response(jsonEncode({'cmd': 1, 'payload': {}}), 200);
|
|
}),
|
|
),
|
|
);
|
|
addTearDown(api.dispose);
|
|
final errors = <String>[];
|
|
final states = <SessionState>[];
|
|
final errorsSub = api.errorStream.listen(errors.add);
|
|
final statesSub = api.stateStream.listen(states.add);
|
|
addTearDown(errorsSub.cancel);
|
|
addTearDown(statesSub.cancel);
|
|
await api.connect();
|
|
await api.connect();
|
|
expect(api.state, SessionState.disconnected);
|
|
available = true;
|
|
await api.connect();
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(errors, isEmpty);
|
|
expect(states.where((s) => s == SessionState.disconnected), hasLength(2));
|
|
expect(api.state, SessionState.online);
|
|
expect(states.last, SessionState.online);
|
|
},
|
|
);
|
|
|
|
for (final failure in <Object>[
|
|
const SocketException('Server unavailable'),
|
|
TimeoutException('Server unavailable'),
|
|
const HttpException('Server unavailable'),
|
|
http.ClientException('Server unavailable'),
|
|
]) {
|
|
test(
|
|
'a ${failure.runtimeType} request immediately changes connection state without a toast',
|
|
() async {
|
|
var fail = false;
|
|
final api = Api(
|
|
transportFactory: (uri) => IkarTransport(
|
|
uri,
|
|
client: MockClient((request) async {
|
|
if (fail) throw failure;
|
|
return http.Response(jsonEncode({'cmd': 1, 'payload': {}}), 200);
|
|
}),
|
|
),
|
|
);
|
|
addTearDown(api.dispose);
|
|
final errors = <String>[];
|
|
final sub = api.errorStream.listen(errors.add);
|
|
addTearDown(sub.cancel);
|
|
await api.connect();
|
|
fail = true;
|
|
await expectLater(
|
|
api.sendRequest(Opcode.contactsGet, {}),
|
|
throwsA(same(failure)),
|
|
);
|
|
expect(api.state, SessionState.disconnected);
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(errors, isEmpty);
|
|
fail = false;
|
|
await api.connect();
|
|
expect(api.state, SessionState.online);
|
|
},
|
|
);
|
|
}
|
|
|
|
test('operation errors on an online server are still reported', () async {
|
|
final api = Api(
|
|
transportFactory: (uri) => IkarTransport(
|
|
uri,
|
|
client: MockClient((request) async {
|
|
final opcode = int.parse(request.url.pathSegments.last);
|
|
return http.Response(
|
|
jsonEncode(
|
|
opcode == Opcode.ping
|
|
? {'cmd': 1, 'payload': {}}
|
|
: {
|
|
'cmd': 3,
|
|
'payload': {
|
|
'error': 'user.not.found',
|
|
'message': 'Пользователь не найден',
|
|
},
|
|
},
|
|
),
|
|
200,
|
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
|
);
|
|
}),
|
|
),
|
|
);
|
|
addTearDown(api.dispose);
|
|
final errors = <String>[];
|
|
final sub = api.errorStream.listen(errors.add);
|
|
addTearDown(sub.cancel);
|
|
await api.connect();
|
|
await expectLater(
|
|
api.sendRequest(Opcode.contactInfoByPhone, {'phone': '+79000000000'}),
|
|
throwsA(isA<PacketError>()),
|
|
);
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(api.state, SessionState.online);
|
|
expect(errors, ['Пользователь не найден']);
|
|
});
|
|
}
|