- Add socket handler and server implementations for real-time communication - Add Mattermost integration (auth service, push client, plugin client) - Add Mattermost push host integration tests - Update Flutter client app with new app structure and bootstrap - Update Android build configurations and add Google Services config - Add asset keep file for Flutter client - Update agent-roadmap with api-centered-proto-socket-rail milestone - Update agent-task with planning and code review documents for all groups - Update domain rules for api, client, worker and project rules
174 lines
4.8 KiB
Dart
174 lines
4.8 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:alt_client/src/integrations/mattermost/mattermost_auth_service.dart';
|
|
import 'package:alt_client/src/integrations/mattermost/mattermost_push_client.dart';
|
|
import 'package:alt_client/src/integrations/mattermost/mattermost_push_host_integration.dart';
|
|
|
|
class _FakePushClient implements MattermostPushClient {
|
|
int initializeCalls = 0;
|
|
int navigateChannelAssignments = 0;
|
|
int navigateThreadAssignments = 0;
|
|
|
|
final StreamController<Map<String, dynamic>> _controller =
|
|
StreamController<Map<String, dynamic>>.broadcast();
|
|
|
|
void Function(String serverUrl, String channelId)? _navigateChannel;
|
|
void Function(String serverUrl, String rootId)? _navigateThread;
|
|
|
|
@override
|
|
Stream<Map<String, dynamic>> get onNotification => _controller.stream;
|
|
|
|
void emit(Map<String, dynamic> data) => _controller.add(data);
|
|
|
|
Future<void> dispose() => _controller.close();
|
|
|
|
@override
|
|
Future<void> initialize() async {
|
|
initializeCalls += 1;
|
|
}
|
|
|
|
@override
|
|
Future<String?> getDeviceToken() async => null;
|
|
|
|
@override
|
|
Future<void> setAuthToken(
|
|
String serverUrl,
|
|
String token, {
|
|
String? identifier,
|
|
}) async {}
|
|
|
|
@override
|
|
Future<void> setSigningKey(String serverUrl, String signingKey) async {}
|
|
|
|
@override
|
|
set onDeviceTokenReady(Future<void> Function(String token)? callback) {}
|
|
|
|
@override
|
|
set onNavigateToChannel(
|
|
void Function(String serverUrl, String channelId)? callback,
|
|
) {
|
|
navigateChannelAssignments += 1;
|
|
_navigateChannel = callback;
|
|
}
|
|
|
|
@override
|
|
set onNavigateToThread(
|
|
void Function(String serverUrl, String rootId)? callback,
|
|
) {
|
|
navigateThreadAssignments += 1;
|
|
_navigateThread = callback;
|
|
}
|
|
|
|
void triggerNavigateChannel(String serverUrl, String channelId) {
|
|
_navigateChannel?.call(serverUrl, channelId);
|
|
}
|
|
|
|
void triggerNavigateThread(String serverUrl, String rootId) {
|
|
_navigateThread?.call(serverUrl, rootId);
|
|
}
|
|
}
|
|
|
|
class _NoopAuthService implements MattermostAuthService {
|
|
int autoLoginCalls = 0;
|
|
final bool throwOnLogin;
|
|
|
|
_NoopAuthService({this.throwOnLogin = false});
|
|
|
|
@override
|
|
Future<void> autoLoginAndRegister() async {
|
|
autoLoginCalls += 1;
|
|
if (throwOnLogin) {
|
|
throw StateError('credentials missing');
|
|
}
|
|
}
|
|
|
|
@override
|
|
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|
|
|
|
void main() {
|
|
test('initialize calls push client init exactly once', () async {
|
|
final push = _FakePushClient();
|
|
final auth = _NoopAuthService();
|
|
final host = MattermostPushHostIntegration(
|
|
pushClient: push,
|
|
authServiceFactory: (_) => auth,
|
|
);
|
|
|
|
await host.initialize();
|
|
await host.initialize();
|
|
|
|
expect(push.initializeCalls, equals(1));
|
|
expect(auth.autoLoginCalls, equals(1));
|
|
expect(host.isInitialized, isTrue);
|
|
|
|
await push.dispose();
|
|
});
|
|
|
|
test('auto-login failure does not block initialize', () async {
|
|
final push = _FakePushClient();
|
|
final auth = _NoopAuthService(throwOnLogin: true);
|
|
final host = MattermostPushHostIntegration(
|
|
pushClient: push,
|
|
authServiceFactory: (_) => auth,
|
|
);
|
|
|
|
await host.initialize();
|
|
|
|
expect(host.isInitialized, isTrue);
|
|
expect(push.initializeCalls, equals(1));
|
|
|
|
await push.dispose();
|
|
});
|
|
|
|
test(
|
|
'navigation callbacks are assigned exactly once during initialize',
|
|
() async {
|
|
final push = _FakePushClient();
|
|
final host = MattermostPushHostIntegration(
|
|
pushClient: push,
|
|
authServiceFactory: (_) => _NoopAuthService(),
|
|
);
|
|
|
|
String? capturedChannel;
|
|
String? capturedThread;
|
|
await host.initialize(
|
|
onNavigateToChannel: (_, channelId) => capturedChannel = channelId,
|
|
onNavigateToThread: (_, rootId) => capturedThread = rootId,
|
|
);
|
|
|
|
expect(push.navigateChannelAssignments, equals(1));
|
|
expect(push.navigateThreadAssignments, equals(1));
|
|
|
|
push.triggerNavigateChannel('https://srv', 'channel-1');
|
|
push.triggerNavigateThread('https://srv', 'root-1');
|
|
|
|
expect(capturedChannel, equals('channel-1'));
|
|
expect(capturedThread, equals('root-1'));
|
|
|
|
await push.dispose();
|
|
},
|
|
);
|
|
|
|
test('notification stream remains consumable through integration', () async {
|
|
final push = _FakePushClient();
|
|
final host = MattermostPushHostIntegration(
|
|
pushClient: push,
|
|
authServiceFactory: (_) => _NoopAuthService(),
|
|
);
|
|
await host.initialize();
|
|
|
|
final received = <Map<String, dynamic>>[];
|
|
final sub = host.onNotification.listen(received.add);
|
|
|
|
push.emit(const {'type': 'message', 'message': 'hi'});
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(received, hasLength(1));
|
|
expect(received.first['message'], equals('hi'));
|
|
|
|
await sub.cancel();
|
|
await push.dispose();
|
|
});
|
|
}
|