329 lines
9.0 KiB
Dart
329 lines
9.0 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:ikar/backend/api.dart';
|
|
import 'package:ikar/backend/modules/forward_sender.dart';
|
|
import 'package:ikar/backend/modules/messages.dart';
|
|
import 'package:ikar/core/crypto/e2ee_service.dart';
|
|
import 'package:ikar/core/protocol/packet.dart';
|
|
import 'package:ikar/core/storage/app_database.dart';
|
|
import 'package:ikar/core/storage/chat_encryption_store.dart';
|
|
import 'package:ikar/core/storage/token_storage.dart';
|
|
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
|
|
|
class _SyntheticPathProvider extends PathProviderPlatform
|
|
with MockPlatformInterfaceMixin {
|
|
_SyntheticPathProvider(this.path);
|
|
|
|
final String path;
|
|
|
|
@override
|
|
Future<String?> getApplicationSupportPath() async => path;
|
|
}
|
|
|
|
class _RecordingApi extends Api {
|
|
int epoch = 1;
|
|
final requests = <Map<dynamic, dynamic>>[];
|
|
Future<void> Function()? beforeDispatch;
|
|
Future<void> Function()? afterDispatch;
|
|
|
|
@override
|
|
int get sessionEpoch => epoch;
|
|
|
|
@override
|
|
Future<Packet> sendRequest(
|
|
int opcode,
|
|
Map<dynamic, dynamic> payload, {
|
|
bool silent = false,
|
|
void Function()? beforeSend,
|
|
}) async {
|
|
await beforeDispatch?.call();
|
|
beforeSend?.call();
|
|
requests.add(payload);
|
|
await afterDispatch?.call();
|
|
return Packet(
|
|
cmd: CmdType.ok,
|
|
payload: {
|
|
'message': {'id': '', 'text': 'Synthetic delivery', 'time': 1000},
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
final _source = CachedMessage(
|
|
id: '701',
|
|
accountId: 101,
|
|
chatId: 303,
|
|
senderId: 202,
|
|
text: 'Synthetic forward',
|
|
time: 1000,
|
|
);
|
|
|
|
ForwardRequest _request({bool hideSender = false}) => ForwardRequest(
|
|
sourceChatId: 303,
|
|
sourceChatName: 'Synthetic source',
|
|
sourceChatIconUrl: '',
|
|
sourceChatType: 'CHAT',
|
|
messages: [_source],
|
|
hideSender: hideSender,
|
|
);
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
late _RecordingApi api;
|
|
late MessagesModule sender;
|
|
|
|
setUpAll(AppDatabase.init);
|
|
|
|
Future<void> setPhase(String phase) async {
|
|
E2eeService.instance.forgetAccount(101);
|
|
await AppDatabase.saveE2eeSession({
|
|
'account_id': 101,
|
|
'chat_id': 404,
|
|
'peer_id': 202,
|
|
'phase': phase,
|
|
'updated': 1000,
|
|
});
|
|
}
|
|
|
|
setUp(() async {
|
|
SharedPreferences.setMockInitialValues({'active_account_id': '101'});
|
|
final directory = await Directory.systemTemp.createTemp(
|
|
'synthetic_forward',
|
|
);
|
|
final previousProvider = PathProviderPlatform.instance;
|
|
PathProviderPlatform.instance = _SyntheticPathProvider(directory.path);
|
|
await databaseFactory.setDatabasesPath(directory.path);
|
|
await AppDatabase.saveProfile(ProfileData.stub(101));
|
|
E2eeService.instance.forgetAccount(101);
|
|
await ChatEncryptionStore.instance.setEnabled(101, 404, false);
|
|
api = _RecordingApi();
|
|
sender = MessagesModule(api);
|
|
addTearDown(() async {
|
|
E2eeService.instance.forgetAccount(101);
|
|
await ChatEncryptionStore.instance.setEnabled(101, 404, false);
|
|
await api.dispose();
|
|
await AppDatabase.close();
|
|
PathProviderPlatform.instance = previousProvider;
|
|
await directory.delete(recursive: true);
|
|
});
|
|
});
|
|
|
|
for (final phase in [
|
|
'established',
|
|
'keyChanged',
|
|
'offered',
|
|
'pendingConsent',
|
|
]) {
|
|
for (final copy in [false, true]) {
|
|
test(
|
|
'$phase destination rejects direct ${copy ? "copy" : "forward"}',
|
|
() async {
|
|
await setPhase(phase);
|
|
|
|
await expectLater(
|
|
copy
|
|
? ForwardSender.sendCopy(101, 404, _source, sender: sender)
|
|
: ForwardSender.sendForward(
|
|
101,
|
|
404,
|
|
_request(),
|
|
_source,
|
|
sender: sender,
|
|
),
|
|
throwsA(isA<StateError>()),
|
|
);
|
|
expect(api.requests, isEmpty);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
test('legacy destination rejects a caption-only resumed batch', () async {
|
|
await ChatEncryptionStore.instance.setEnabled(101, 404, true);
|
|
|
|
final result = await ForwardSender.send(
|
|
accountId: 101,
|
|
chatIds: [404],
|
|
request: _request(),
|
|
caption: const ForwardCaption('Synthetic caption'),
|
|
resumeFrom: {404: 1},
|
|
sender: sender,
|
|
);
|
|
|
|
expect(result.unfinished, {404: 1});
|
|
expect(api.requests, isEmpty);
|
|
});
|
|
|
|
test('mixed batch delivers only to the unencrypted destination', () async {
|
|
await setPhase('established');
|
|
|
|
final result = await ForwardSender.send(
|
|
accountId: 101,
|
|
chatIds: [404, 505],
|
|
request: _request(hideSender: true),
|
|
caption: const ForwardCaption('Synthetic caption'),
|
|
sender: sender,
|
|
);
|
|
|
|
expect(result.delivered, {505});
|
|
expect(result.unfinished, {404: 0});
|
|
expect(api.requests.map((p) => p['chatId']), [505, 505]);
|
|
});
|
|
|
|
test('encryption enabled between steps blocks the caption', () async {
|
|
api.afterDispatch = () =>
|
|
ChatEncryptionStore.instance.setEnabled(101, 404, true);
|
|
|
|
final result = await ForwardSender.send(
|
|
accountId: 101,
|
|
chatIds: [404],
|
|
request: _request(),
|
|
caption: const ForwardCaption('Synthetic caption'),
|
|
sender: sender,
|
|
);
|
|
|
|
expect(result.unfinished, {404: 1});
|
|
expect(api.requests, hasLength(1));
|
|
});
|
|
|
|
test(
|
|
'encryption enabled while dispatch waits blocks the wire request',
|
|
() async {
|
|
api.beforeDispatch = () =>
|
|
ChatEncryptionStore.instance.setEnabled(101, 404, true);
|
|
|
|
await expectLater(
|
|
ForwardSender.sendCopy(101, 404, _source, sender: sender),
|
|
throwsA(isA<StateError>()),
|
|
);
|
|
expect(api.requests, isEmpty);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'forgotten account while dispatch waits blocks the wire request',
|
|
() async {
|
|
api.beforeDispatch = () async => E2eeService.instance.forgetAccount(101);
|
|
|
|
await expectLater(
|
|
ForwardSender.sendCopy(101, 404, _source, sender: sender),
|
|
throwsA(isA<StateError>()),
|
|
);
|
|
expect(api.requests, isEmpty);
|
|
},
|
|
);
|
|
|
|
test('new session while dispatch waits blocks the wire request', () async {
|
|
api.beforeDispatch = () async => api.epoch++;
|
|
|
|
await expectLater(
|
|
ForwardSender.sendCopy(101, 404, _source, sender: sender),
|
|
throwsA(isA<StateError>()),
|
|
);
|
|
expect(api.requests, isEmpty);
|
|
});
|
|
|
|
test('a batch cannot continue in the replacement session', () async {
|
|
api.beforeDispatch = () async {
|
|
api.beforeDispatch = null;
|
|
api.epoch++;
|
|
};
|
|
|
|
final result = await ForwardSender.send(
|
|
accountId: 101,
|
|
chatIds: [404, 505, 606, 707],
|
|
request: _request(),
|
|
sender: sender,
|
|
);
|
|
|
|
expect(result.unfinished, {404: 0, 505: 0, 606: 0, 707: 0});
|
|
expect(api.requests, isEmpty);
|
|
});
|
|
|
|
test(
|
|
'a caption cannot follow its forward in a replacement session',
|
|
() async {
|
|
api.afterDispatch = () async => api.epoch++;
|
|
|
|
final result = await ForwardSender.send(
|
|
accountId: 101,
|
|
chatIds: [404],
|
|
request: _request(),
|
|
caption: const ForwardCaption('Synthetic caption'),
|
|
sender: sender,
|
|
);
|
|
|
|
expect(result.unfinished, {404: 1});
|
|
expect(api.requests, hasLength(1));
|
|
},
|
|
);
|
|
|
|
test(
|
|
'v2 encryption enabled while dispatch waits blocks the caption',
|
|
() async {
|
|
api.beforeDispatch = () async {
|
|
await setPhase('established');
|
|
await E2eeService.instance.ensureLoaded(101);
|
|
};
|
|
|
|
final result = await ForwardSender.send(
|
|
accountId: 101,
|
|
chatIds: [404],
|
|
request: _request(),
|
|
caption: const ForwardCaption('Synthetic caption'),
|
|
resumeFrom: {404: 1},
|
|
sender: sender,
|
|
);
|
|
|
|
expect(result.unfinished, {404: 1});
|
|
expect(api.requests, isEmpty);
|
|
},
|
|
);
|
|
|
|
test('ordinary destination supports direct forward and copy', () async {
|
|
await ForwardSender.sendForward(
|
|
101,
|
|
404,
|
|
_request(),
|
|
_source,
|
|
sender: sender,
|
|
);
|
|
await ForwardSender.sendCopy(101, 404, _source, sender: sender);
|
|
|
|
expect(api.requests, hasLength(2));
|
|
});
|
|
|
|
test('account switch at dispatch blocks copying', () async {
|
|
api.beforeDispatch = () => TokenStorage.setActiveAccount(909);
|
|
await expectLater(
|
|
ForwardSender.sendCopy(101, 404, _source, sender: sender),
|
|
throwsA(isA<StateError>()),
|
|
);
|
|
expect(api.requests, isEmpty);
|
|
});
|
|
|
|
test('source encryption at dispatch blocks forwarding', () async {
|
|
api.beforeDispatch = () =>
|
|
ChatEncryptionStore.instance.setEnabled(101, 303, true);
|
|
try {
|
|
await expectLater(
|
|
ForwardSender.sendForward(
|
|
101,
|
|
404,
|
|
_request(),
|
|
_source,
|
|
sender: sender,
|
|
),
|
|
throwsA(isA<StateError>()),
|
|
);
|
|
expect(api.requests, isEmpty);
|
|
} finally {
|
|
await ChatEncryptionStore.instance.setEnabled(101, 303, false);
|
|
}
|
|
});
|
|
}
|