implemented dual Nextcloud push registration with separate general and talk apptypes to ensure reliable Talk notification delivery; introduced stacked MessagingStyle notifications for chat threads with support for circular conversation avatars and disk caching
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/push/push_registration.dart';
|
||||
import 'package:marianum_mobile/push/push_registration_store.dart';
|
||||
import 'package:marianum_mobile/push/push_registration_type.dart';
|
||||
import 'package:marianum_mobile/push/push_secure_storage.dart';
|
||||
|
||||
class _MemoryStorage implements FlutterSecureStorageLike {
|
||||
final Map<String, String> values = {};
|
||||
|
||||
@override
|
||||
Future<String?> read({required String key}) async => values[key];
|
||||
|
||||
@override
|
||||
Future<void> write({required String key, required String? value}) async {
|
||||
if (value == null) {
|
||||
values.remove(key);
|
||||
} else {
|
||||
values[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete({required String key}) async => values.remove(key);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('talk user agents', () {
|
||||
// Quoted from nextcloud/server lib/public/IRequest.php.
|
||||
final uaTalkAndroid = RegExp(
|
||||
r'^Mozilla/5\.0 \(Android\) Nextcloud-Talk v([^ ]*).*$',
|
||||
);
|
||||
final uaTalkIos = RegExp(
|
||||
r'^Mozilla/5\.0 \(iOS\) Nextcloud-Talk v([^ ]*).*$',
|
||||
);
|
||||
|
||||
test('android UA matches USER_AGENT_TALK_ANDROID', () {
|
||||
expect(
|
||||
uaTalkAndroid.hasMatch(PushRegistration.talkUserAgentAndroid),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
uaTalkIos.hasMatch(PushRegistration.talkUserAgentAndroid),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('ios UA matches USER_AGENT_TALK_IOS', () {
|
||||
expect(uaTalkIos.hasMatch(PushRegistration.talkUserAgentIos), isTrue);
|
||||
expect(
|
||||
uaTalkAndroid.hasMatch(PushRegistration.talkUserAgentIos),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('pushTokenVariant', () {
|
||||
test('general uses the raw token, talk appends the suffix', () {
|
||||
expect(pushTokenVariant('tok', PushRegistrationType.general), 'tok');
|
||||
expect(pushTokenVariant('tok', PushRegistrationType.talk), 'tok#talk');
|
||||
});
|
||||
|
||||
test('variants always differ (NC would delete same-hash siblings)', () {
|
||||
const token = 'any-token';
|
||||
expect(
|
||||
pushTokenVariant(token, PushRegistrationType.general),
|
||||
isNot(pushTokenVariant(token, PushRegistrationType.talk)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('PushRegistrationStore', () {
|
||||
test('pre-dual entries are read as the general registration', () async {
|
||||
final storage = _MemoryStorage();
|
||||
// State written by a pre-dual app version (no type suffixes).
|
||||
storage.values.addAll({
|
||||
'push_device_identifier': 'legacy-device',
|
||||
'push_server_public_key_pem': 'legacy-key',
|
||||
'push_registered_fcm_token': 'legacy-token',
|
||||
'push_registered_proxy_server': 'https://old/push-proxy/',
|
||||
'push_registered_nc_base_url': 'https://cloud',
|
||||
'push_last_registration_at': '2026-07-01T10:00:00.000',
|
||||
'push_last_registration_error': '',
|
||||
});
|
||||
final store = PushRegistrationStore(storage);
|
||||
|
||||
expect(
|
||||
await store.deviceIdentifier(PushRegistrationType.general),
|
||||
'legacy-device',
|
||||
);
|
||||
expect(await store.isRegistered(PushRegistrationType.general), isTrue);
|
||||
expect(
|
||||
await store.registeredProxyServer(PushRegistrationType.general),
|
||||
'https://old/push-proxy/',
|
||||
);
|
||||
expect(
|
||||
await store.lastRegistrationAt(PushRegistrationType.general),
|
||||
DateTime(2026, 7, 1, 10),
|
||||
);
|
||||
|
||||
// The talk registration is genuinely absent — the self-heal adds it.
|
||||
expect(await store.deviceIdentifier(PushRegistrationType.talk), isNull);
|
||||
expect(await store.isRegistered(PushRegistrationType.talk), isFalse);
|
||||
});
|
||||
|
||||
test('per-type values stay independent', () async {
|
||||
final storage = _MemoryStorage();
|
||||
final store = PushRegistrationStore(storage);
|
||||
|
||||
await store.save(
|
||||
type: PushRegistrationType.general,
|
||||
deviceIdentifier: 'dev-general',
|
||||
serverPublicKeyPem: 'server-key',
|
||||
fcmToken: 'token',
|
||||
proxyServer: 'https://a/push-proxy/',
|
||||
ncBaseUrl: 'https://cloud',
|
||||
);
|
||||
await store.save(
|
||||
type: PushRegistrationType.talk,
|
||||
deviceIdentifier: 'dev-talk',
|
||||
serverPublicKeyPem: 'server-key',
|
||||
fcmToken: 'token',
|
||||
proxyServer: 'https://a/push-proxy/',
|
||||
ncBaseUrl: 'https://cloud',
|
||||
);
|
||||
await store.saveLastRegistrationAttempt(
|
||||
type: PushRegistrationType.talk,
|
||||
at: DateTime(2026, 7, 5, 12),
|
||||
error: 'HTTP 404',
|
||||
);
|
||||
|
||||
expect(
|
||||
await store.deviceIdentifier(PushRegistrationType.general),
|
||||
'dev-general',
|
||||
);
|
||||
expect(
|
||||
await store.deviceIdentifier(PushRegistrationType.talk),
|
||||
'dev-talk',
|
||||
);
|
||||
// Shared server key: last write wins, both read the same value.
|
||||
expect(await store.serverPublicKeyPem(), 'server-key');
|
||||
expect(
|
||||
await store.lastRegistrationError(PushRegistrationType.general),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
await store.lastRegistrationError(PushRegistrationType.talk),
|
||||
'HTTP 404',
|
||||
);
|
||||
});
|
||||
|
||||
test('clear removes both registrations and shared keys', () async {
|
||||
final storage = _MemoryStorage();
|
||||
final store = PushRegistrationStore(storage);
|
||||
for (final type in PushRegistrationType.values) {
|
||||
await store.save(
|
||||
type: type,
|
||||
deviceIdentifier: 'dev',
|
||||
serverPublicKeyPem: 'key',
|
||||
fcmToken: 'token',
|
||||
proxyServer: 'https://a/',
|
||||
ncBaseUrl: 'https://cloud',
|
||||
);
|
||||
await store.saveLastRegistrationAttempt(
|
||||
type: type,
|
||||
at: DateTime(2026),
|
||||
error: 'x',
|
||||
);
|
||||
}
|
||||
await store.saveNativeAuthContext(
|
||||
username: 'user',
|
||||
baseUrl: 'https://cloud',
|
||||
);
|
||||
|
||||
await store.clear();
|
||||
expect(storage.values, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user