179 lines
5.6 KiB
Dart
179 lines
5.6 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|