Files
oott/frontend/test/widget/settings_push_test.dart
T
rzuastiandClaude Opus 4.8 4f3fe10332 Implement push notifications Phase 1 (FCM + project relay)
Backend (Rust):
- push_tokens migration, model (PushToken/PushPlatform), and db layer
  (upsert/list/delete/delete_many)
- PUT/DELETE /api/push_tokens endpoints wired into the router + OpenAPI
- "push" notification method: relay sender (reqwest) that forwards only the
  sanitized title/body and prunes dead tokens, plus settings with a default
  relay_url
- 164 tests pass, clippy clean

Relay (push_relay/, TypeScript Firebase Cloud Function):
- POST /v1/push (firebase-admin sendEach + per-token status mapping),
  GET /healthz, payload validation, per-IP Firestore rate limiting
- Jest tests + README documenting the manual project-owned setup

Frontend (Flutter):
- oott_api_push.dart (register/unregister), push_service.dart behind a
  PushService abstraction, and a per-device push toggle in settings
- firebase_core/firebase_messaging/flutter_local_notifications deps
- 145 tests pass, dart analyze clean

Dev shell:
- add nodejs_22 to the Nix dev shell so the relay tests/build run locally

Remaining (manual, project-owned): create the Firebase project, deploy the
relay and set the real default_relay_url, add native Firebase config, and test
on real devices.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 12:28:03 -04:00

120 lines
3.5 KiB
Dart

import 'package:encrypter/encrypter/xor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:frontend/settings/settings.dart';
import 'package:frontend/utils/pref_utils.dart';
import 'package:frontend/utils/push_service.dart';
import '../helpers/backend_test_harness.dart';
import '../helpers/pump_app.dart';
// A fake so the toggle can be exercised without Firebase.
class _FakePushService implements PushService {
_FakePushService({this.supported = true, this.enableResult = true});
final bool supported;
final bool enableResult;
int enableCalls = 0;
int disableCalls = 0;
@override
bool get isSupported => supported;
@override
Future<bool> enable() async {
enableCalls++;
return enableResult;
}
@override
Future<void> disable() async {
disableCalls++;
}
}
const _toggleText = 'Push notifications on this device';
void main() {
setUp(() async {
await setUpBackendForTest(
prefs: {
'base_url': 'http://my.server/api',
'api_key': XOR().xorEncode('topsecret'),
'theme': 'catppuccin_mocha',
},
);
// The mock SharedPreferences persist across tests in this isolate, so reset
// the per-device push intent to a known-off baseline before each test.
await PrefUtil.setValue('push_enabled', false);
});
testWidgets('shows the push toggle on a supported platform', (tester) async {
await pumpScreen(tester, Settings(pushService: _FakePushService()));
expect(find.text(_toggleText), findsOneWidget);
});
testWidgets('hides the push toggle when push is unsupported', (tester) async {
await pumpScreen(
tester,
Settings(pushService: _FakePushService(supported: false)),
);
expect(find.text(_toggleText), findsNothing);
});
testWidgets('enabling push registers and shows a success message', (
tester,
) async {
final service = _FakePushService(enableResult: true);
await pumpScreen(tester, Settings(pushService: service));
await tester.tap(find.byType(SwitchListTile));
await pumpUntilFound(
tester,
find.text('Push notifications enabled on this device'),
);
expect(service.enableCalls, 1);
expect(PrefUtil.getValue('push_enabled', false), isTrue);
expect(tester.widget<SwitchListTile>(find.byType(SwitchListTile)).value, isTrue);
});
testWidgets('a declined permission leaves the toggle off with an error', (
tester,
) async {
final service = _FakePushService(enableResult: false);
await pumpScreen(tester, Settings(pushService: service));
await tester.tap(find.byType(SwitchListTile));
await pumpUntilFound(
tester,
find.textContaining('Could not enable push'),
);
expect(service.enableCalls, 1);
expect(tester.widget<SwitchListTile>(find.byType(SwitchListTile)).value, isFalse);
});
testWidgets('disabling push unregisters and shows a success message', (
tester,
) async {
await PrefUtil.setValue('push_enabled', true);
final service = _FakePushService();
await pumpScreen(tester, Settings(pushService: service));
// Starts on because the stored intent is enabled.
expect(
tester.widget<SwitchListTile>(find.byType(SwitchListTile)).value,
isTrue,
);
await tester.tap(find.byType(SwitchListTile));
await pumpUntilFound(
tester,
find.text('Push notifications disabled on this device'),
);
expect(service.disableCalls, 1);
expect(PrefUtil.getValue('push_enabled', false), isFalse);
});
}