forked from eatvio/eatvio_app
test(session): cover avatar refresh and session teardown
Add regression tests for avatar reset and token-change refresh, stale 401 handling, idempotent session teardown and account-scoped preference cleanup. Update existing cubit tests for the new session dependency. Refs: #1113
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class MockProfileRepository extends Mock implements ProfileRepository {}
|
||||
|
||||
const _profileA = ProfileModel(
|
||||
id: 1,
|
||||
slug: 'user-a',
|
||||
name: 'User A',
|
||||
email: 'a@example.com',
|
||||
phoneConfirmationState: 'confirmed',
|
||||
isPublicProfile: true,
|
||||
isBlockedByMe: false,
|
||||
isBlockedByAuthor: false,
|
||||
isBlockedByUser: false,
|
||||
isEntitySubscribedByMe: false,
|
||||
profilePhotoUrl: 'avatars/a.png',
|
||||
roles: [],
|
||||
badges: [],
|
||||
subscriptions: [],
|
||||
subscribers: [],
|
||||
);
|
||||
|
||||
const _profileB = ProfileModel(
|
||||
id: 2,
|
||||
slug: 'user-b',
|
||||
name: 'User B',
|
||||
email: 'b@example.com',
|
||||
phoneConfirmationState: 'confirmed',
|
||||
isPublicProfile: true,
|
||||
isBlockedByMe: false,
|
||||
isBlockedByAuthor: false,
|
||||
isBlockedByUser: false,
|
||||
isEntitySubscribedByMe: false,
|
||||
profilePhotoUrl: 'avatars/b.png',
|
||||
roles: [],
|
||||
badges: [],
|
||||
subscriptions: [],
|
||||
subscribers: [],
|
||||
);
|
||||
|
||||
const _details = ProfileDetailsModel(
|
||||
firstName: 'User',
|
||||
lastName: 'Test',
|
||||
birthdate: '1990-01-01',
|
||||
gender: 'male',
|
||||
country: 'Russia',
|
||||
city: 'Moscow',
|
||||
lang: 'ru',
|
||||
weight: 70.0,
|
||||
height: 175,
|
||||
chestCircumference: 100,
|
||||
waistCircumference: 80,
|
||||
hipCircumference: 90,
|
||||
bloodPressureUp: 120,
|
||||
bloodPressureDown: 80,
|
||||
hasHypo: false,
|
||||
hasHyper: false,
|
||||
aboutAlergy: '-',
|
||||
aboutTakingPils: '-',
|
||||
aboutSportHobbies: '-',
|
||||
favoriteFoods: '-',
|
||||
unfavoriteFoods: '-',
|
||||
additionalInfo: '-',
|
||||
isMoodFillable: true,
|
||||
isBedtimeFillable: true,
|
||||
activityCoefficient: 1.5,
|
||||
);
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late MockProfileRepository repository;
|
||||
ProfileAvatarCubit? cubit;
|
||||
|
||||
setUp(() async {
|
||||
await sl.reset();
|
||||
SharedPreferences.setMockInitialValues({'token': 'token-a'});
|
||||
TokenStorage.clearCache();
|
||||
repository = MockProfileRepository();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
if (cubit != null && !cubit!.isClosed) {
|
||||
await cubit!.close();
|
||||
}
|
||||
cubit = null;
|
||||
await sl.reset();
|
||||
TokenStorage.clearCache();
|
||||
});
|
||||
|
||||
void stubSuccessfulProfile(ProfileModel profile) {
|
||||
when(() => repository.getMyProfile()).thenAnswer((_) async => profile);
|
||||
when(
|
||||
() => repository.getMyProfileDetails(),
|
||||
).thenAnswer((_) async => _details);
|
||||
}
|
||||
|
||||
test('reset emits loading and clears the cached profile', () async {
|
||||
stubSuccessfulProfile(_profileA);
|
||||
cubit = ProfileAvatarCubit(profileRepository: repository);
|
||||
|
||||
await cubit!.ensureFreshForCurrentSession();
|
||||
expect(cubit!.state, isA<ProfileAvatarStateLoaded>());
|
||||
|
||||
cubit!.reset();
|
||||
expect(cubit!.state, isA<ProfileAvatarStateLoading>());
|
||||
|
||||
await cubit!.ensureFreshForCurrentSession();
|
||||
|
||||
verify(() => repository.getMyProfile()).called(2);
|
||||
expect(cubit!.state, isA<ProfileAvatarStateLoaded>());
|
||||
});
|
||||
|
||||
test('late getMyProfile completion after reset is ignored', () async {
|
||||
final profileCompleter = Completer<ProfileModel>();
|
||||
when(
|
||||
() => repository.getMyProfile(),
|
||||
).thenAnswer((_) => profileCompleter.future);
|
||||
when(
|
||||
() => repository.getMyProfileDetails(),
|
||||
).thenAnswer((_) async => _details);
|
||||
|
||||
cubit = ProfileAvatarCubit(profileRepository: repository);
|
||||
|
||||
final pendingRequest = cubit!.getMyProfile();
|
||||
await pumpEventQueue();
|
||||
expect(cubit!.state, isA<ProfileAvatarStateLoading>());
|
||||
|
||||
cubit!.reset();
|
||||
profileCompleter.complete(_profileA);
|
||||
await pendingRequest;
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(cubit!.state, isA<ProfileAvatarStateLoading>());
|
||||
verifyNever(() => repository.getMyProfileDetails());
|
||||
});
|
||||
|
||||
test(
|
||||
'ensureFreshForCurrentSession does not refetch for the same token',
|
||||
() async {
|
||||
stubSuccessfulProfile(_profileA);
|
||||
cubit = ProfileAvatarCubit(profileRepository: repository);
|
||||
|
||||
await cubit!.ensureFreshForCurrentSession();
|
||||
await cubit!.ensureFreshForCurrentSession();
|
||||
|
||||
verify(() => repository.getMyProfile()).called(1);
|
||||
expect(cubit!.state, isA<ProfileAvatarStateLoaded>());
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'ensureFreshForCurrentSession refetches after the token changes',
|
||||
() async {
|
||||
final profiles = <ProfileModel>[_profileA, _profileB];
|
||||
when(
|
||||
() => repository.getMyProfile(),
|
||||
).thenAnswer((_) async => profiles.removeAt(0));
|
||||
when(
|
||||
() => repository.getMyProfileDetails(),
|
||||
).thenAnswer((_) async => _details);
|
||||
|
||||
cubit = ProfileAvatarCubit(profileRepository: repository);
|
||||
|
||||
await cubit!.ensureFreshForCurrentSession();
|
||||
final firstState = cubit!.state as ProfileAvatarStateLoaded;
|
||||
expect(firstState.profile.id, _profileA.id);
|
||||
|
||||
await TokenStorage.saveTokenInStorage('token-b');
|
||||
await cubit!.ensureFreshForCurrentSession();
|
||||
|
||||
final secondState = cubit!.state as ProfileAvatarStateLoaded;
|
||||
expect(secondState.profile.id, _profileB.id);
|
||||
expect(secondState.profile.profilePhotoUrl, _profileB.profilePhotoUrl);
|
||||
verify(() => repository.getMyProfile()).called(2);
|
||||
},
|
||||
);
|
||||
|
||||
test('errors emit ProfileAvatarStateError and do not navigate', () async {
|
||||
when(
|
||||
() => repository.getMyProfile(),
|
||||
).thenAnswer((_) async => throw Exception('profile request failed'));
|
||||
|
||||
cubit = ProfileAvatarCubit(profileRepository: repository);
|
||||
|
||||
await cubit!.getMyProfile();
|
||||
|
||||
expect(cubit!.state, isA<ProfileAvatarStateError>());
|
||||
expect(
|
||||
(cubit!.state as ProfileAvatarStateError).errorMessage,
|
||||
contains('profile request failed'),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
@@ -5,6 +6,7 @@ import 'package:eatvio_app/features/profile/repositories/profile_repository.dart
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'profile_avatar_cubit_test.mocks.dart';
|
||||
|
||||
@@ -16,6 +18,8 @@ void main() {
|
||||
late MockProfileRepository mockRepository;
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({'token': 'test-token'});
|
||||
TokenStorage.clearCache();
|
||||
mockRepository = MockProfileRepository();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user