MotorSignal-App/test/account_deletion_lifecycle_...

219 lines
7.4 KiB
Dart

import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:motorcycle_dash/database/database.dart';
import 'package:motorcycle_dash/features/settings/account_screen.dart';
import 'package:motorcycle_dash/services/auth/auth_providers.dart';
import 'package:motorcycle_dash/services/auth/auth_service.dart';
import 'package:motorcycle_dash/services/auth/auth_state.dart';
import 'package:motorcycle_dash/services/auth/auth_user.dart';
import 'package:motorcycle_dash/services/auth/account_lifecycle_service.dart';
import 'package:motorcycle_dash/services/cloud/account_cloud_cleanup_service.dart';
import 'package:motorcycle_dash/services/sync/sync_providers.dart';
import 'package:motorcycle_dash/features/records/records_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
class _FakeAuth implements AuthService {
final _states = StreamController<AuthState>.broadcast();
AuthUser? user = const AuthUser(uid: 'delete-test', email: 'delete@test.com', providerId: 'password', emailVerified: true);
bool reauthenticated = false;
bool deleted = false;
Object? reauthError;
@override
Stream<AuthState> get authStateChanges => _states.stream;
@override
AuthState get currentState => user == null
? const AuthState.unauthenticated(isFirebaseConfigured: true)
: AuthState.authenticated(user!, isFirebaseConfigured: true);
@override
AuthUser? get currentUser => user;
@override
bool get isConfigured => true;
@override
bool get isEmailVerified => true;
@override
Future<void> reauthenticate({String? password}) async {
if (reauthError != null) throw reauthError!;
reauthenticated = true;
}
@override
Future<void> deleteCurrentUser() async {
deleted = true;
user = null;
_states.add(currentState);
}
@override
Future<String?> getIdToken([bool forceRefresh = false]) async => 'test-token';
@override
Future<void> signInWithEmailAndPassword(String email, String password) async {}
@override
Future<void> createUserWithEmailAndPassword(String email, String password, {String? displayName}) async {}
@override
Future<void> signInWithGoogle() async {}
@override
Future<void> signInAnonymously() async {}
@override
Future<void> sendPasswordResetEmail(String email) async {}
@override
Future<void> sendEmailVerification() async {}
@override
Future<void> reloadCurrentUser() async {}
@override
Future<void> signOut() async {}
@override
void dispose() => _states.close();
}
class _FakeCloud implements CloudAccountCleanup {
bool called = false;
Object? error;
@override
Future<void> deleteCloudData() async {
called = true;
if (error != null) throw error!;
}
}
Future<AppDatabase> _pumpAccountScreen(
WidgetTester tester,
_FakeAuth auth,
_FakeCloud cloud,
) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
final db = AppDatabase.forTesting(NativeDatabase.memory());
await tester.pumpWidget(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
databaseProvider.overrideWithValue(db),
authServiceProvider.overrideWithValue(auth),
accountLifecycleServiceProvider.overrideWithValue(
AccountLifecycleService(auth: auth, cloud: cloud, db: db),
),
],
child: const MaterialApp(home: AccountScreen()),
),
);
await tester.pumpAndSettle();
return db;
}
void main() {
testWidgets(
'delete account uses one password dialog without lifecycle assertion',
(tester) async {
final auth = _FakeAuth();
final cloud = _FakeCloud();
final db = await _pumpAccountScreen(tester, auth, cloud);
addTearDown(() async {
auth.dispose();
await db.close();
});
expect(find.text('DELETE ACCOUNT'), findsOneWidget);
await tester.tap(find.text('DELETE ACCOUNT').last);
await tester.pump();
expect(find.text('Delete Account'), findsOneWidget);
expect(find.byType(AlertDialog), findsOneWidget);
await tester.enterText(find.byType(TextField), 'test-password');
await tester.tap(find.text('DELETE ACCOUNT').last);
await tester.pump();
await tester.pump();
await tester.pumpAndSettle();
expect(auth.reauthenticated, isTrue);
expect(cloud.called, isTrue);
expect(auth.deleted, isTrue);
expect(find.text('Account & Cloud Identity'), findsOneWidget);
expect(find.text('SIGN IN TO MOTORCLOUD'), findsOneWidget);
expect(find.text('Account deleted. Local rides remain on this device.'), findsNothing);
expect(find.text('Confirm Your Password'), findsNothing);
expect(tester.takeException(), isNull);
},
);
testWidgets('canceling account deletion performs no lifecycle work', (tester) async {
final auth = _FakeAuth();
final cloud = _FakeCloud();
final db = await _pumpAccountScreen(tester, auth, cloud);
addTearDown(() async {
auth.dispose();
await db.close();
});
await tester.tap(find.text('DELETE ACCOUNT').last);
await tester.pump();
await tester.tap(find.text('CANCEL').last);
await tester.pumpAndSettle();
expect(auth.reauthenticated, isFalse);
expect(auth.deleted, isFalse);
expect(cloud.called, isFalse);
expect(find.byType(AlertDialog), findsNothing);
expect(tester.takeException(), isNull);
});
testWidgets('failed password reauthentication stays lifecycle safe', (tester) async {
final auth = _FakeAuth()..reauthError = StateError('invalid password');
final cloud = _FakeCloud();
final db = await _pumpAccountScreen(tester, auth, cloud);
addTearDown(() async {
auth.dispose();
await db.close();
});
await tester.tap(find.text('DELETE ACCOUNT').last);
await tester.pump();
await tester.enterText(find.byType(TextField), 'wrong-password');
await tester.tap(find.text('DELETE ACCOUNT').last);
await tester.pumpAndSettle();
expect(auth.reauthenticated, isFalse);
expect(auth.deleted, isFalse);
expect(cloud.called, isFalse);
expect(
find.text('Account deletion could not be completed. Your account and local rides are unchanged.'),
findsOneWidget,
);
expect(tester.takeException(), isNull);
});
testWidgets('cloud cleanup failure stays controlled and preserves session', (tester) async {
final auth = _FakeAuth();
final cloud = _FakeCloud()
..error = FirebaseException(
plugin: 'cloud_firestore',
code: 'permission-denied',
message: 'cleanup unavailable',
);
final db = await _pumpAccountScreen(tester, auth, cloud);
addTearDown(() async {
auth.dispose();
await db.close();
});
await tester.tap(find.text('DELETE ACCOUNT').last);
await tester.pump();
await tester.enterText(find.byType(TextField), 'test-password');
await tester.tap(find.text('DELETE ACCOUNT').last);
await tester.pumpAndSettle();
expect(auth.reauthenticated, isTrue);
expect(auth.deleted, isFalse);
expect(cloud.called, isTrue);
expect(auth.currentUser, isNotNull);
expect(
find.text('Account deletion could not be completed. Your account and local rides are unchanged.'),
findsOneWidget,
);
expect(tester.takeException(), isNull);
});
}