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:
@@ -12,6 +12,7 @@ import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../../../fixtures/doubles/session_doubles.dart';
|
||||
import 'auth_cubit_test.mocks.dart';
|
||||
|
||||
@GenerateNiceMocks([
|
||||
@@ -75,6 +76,7 @@ void main() {
|
||||
profileRepository: mockProfileRepository,
|
||||
firebaseApi: mockFirebaseApi,
|
||||
prefs: mockPrefs,
|
||||
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||
);
|
||||
|
||||
expect(cubit!.state, isA<AuthState>());
|
||||
@@ -86,6 +88,7 @@ void main() {
|
||||
profileRepository: mockProfileRepository,
|
||||
firebaseApi: mockFirebaseApi,
|
||||
prefs: mockPrefs,
|
||||
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||
);
|
||||
|
||||
const email = 'test@example.com';
|
||||
@@ -116,6 +119,7 @@ void main() {
|
||||
profileRepository: mockProfileRepository,
|
||||
firebaseApi: mockFirebaseApi,
|
||||
prefs: mockPrefs,
|
||||
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||
);
|
||||
|
||||
const email = 'test@example.com';
|
||||
@@ -140,6 +144,7 @@ void main() {
|
||||
profileRepository: mockProfileRepository,
|
||||
firebaseApi: mockFirebaseApi,
|
||||
prefs: mockPrefs,
|
||||
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||
);
|
||||
|
||||
when(mockPrefs.getBool(any)).thenReturn(false);
|
||||
@@ -159,6 +164,7 @@ void main() {
|
||||
profileRepository: mockProfileRepository,
|
||||
firebaseApi: mockFirebaseApi,
|
||||
prefs: mockPrefs,
|
||||
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||
);
|
||||
|
||||
when(mockPrefs.getBool(any)).thenReturn(true);
|
||||
@@ -174,6 +180,7 @@ void main() {
|
||||
profileRepository: mockProfileRepository,
|
||||
firebaseApi: mockFirebaseApi,
|
||||
prefs: mockPrefs,
|
||||
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||
);
|
||||
|
||||
when(mockFirebaseApi.registerPushToken())
|
||||
@@ -190,6 +197,7 @@ void main() {
|
||||
profileRepository: mockProfileRepository,
|
||||
firebaseApi: mockFirebaseApi,
|
||||
prefs: mockPrefs,
|
||||
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||
);
|
||||
|
||||
// TokenStorage.haveTokenInStorage() вернет false
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../../fixtures/doubles/auth_doubles.dart';
|
||||
import '../../../../fixtures/doubles/profile_doubles.dart';
|
||||
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||
import '../../../../fixtures/fixture_reader.dart';
|
||||
import 'auth_cubit_test.mocks.dart';
|
||||
|
||||
@@ -26,6 +27,7 @@ void main() {
|
||||
late FixtureProfileRepository profileRepository;
|
||||
late MockPrefs mockPrefs;
|
||||
late MockFirebaseApi mockFirebaseApi;
|
||||
late FakeSessionLifecycleService fakeSession;
|
||||
late Map<String, dynamic> credentialsFixture;
|
||||
|
||||
late TokenModel tokenFixture;
|
||||
@@ -37,6 +39,7 @@ void main() {
|
||||
profileRepository = FixtureProfileRepository();
|
||||
mockPrefs = MockPrefs();
|
||||
mockFirebaseApi = MockFirebaseApi();
|
||||
fakeSession = FakeSessionLifecycleService();
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
// Mock Fluttertoast plugin
|
||||
@@ -59,12 +62,12 @@ void main() {
|
||||
});
|
||||
|
||||
test('начальное состояние - AuthState', () {
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||
expect(authCubit!.state, const AuthState());
|
||||
});
|
||||
|
||||
test('getTokenByCredentials - успешная авторизация', () async {
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||
|
||||
final result = await authCubit!.getTokenByCredentials(credentialsFixture['email'] as String, credentialsFixture['password'] as String);
|
||||
|
||||
@@ -77,7 +80,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('getTokenByCredentials - переход в состояние loading', () async {
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||
authRepository.responseDelay = const Duration(milliseconds: 50);
|
||||
|
||||
final future = authCubit!.getTokenByCredentials(credentialsFixture['email'] as String, credentialsFixture['password'] as String);
|
||||
@@ -88,7 +91,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('getTokenByCredentials - обработка ошибки авторизации', () async {
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||
authRepository.error = Exception('Неверный адрес электронной почты или пароль');
|
||||
|
||||
final result = await authCubit!.getTokenByCredentials('wrong@example.com', 'wrongpassword');
|
||||
@@ -102,7 +105,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('getTokenByCredentials - обработка различных типов ошибок', () async {
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||
authRepository.error = const FormatException('Invalid credentials format');
|
||||
|
||||
final result = await authCubit!.getTokenByCredentials('invalid', 'pass');
|
||||
@@ -115,7 +118,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('getTokenByCredentials - проверка вызова с правильными параметрами', () async {
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||
|
||||
await authCubit!.getTokenByCredentials('user@test.com', 'securePassword123');
|
||||
|
||||
@@ -124,7 +127,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('getTokenByCredentials - множественные вызовы', () async {
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||
|
||||
await authCubit!.getTokenByCredentials('first@test.com', 'password1');
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
@@ -138,7 +141,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('registerAnonymousPushToken - регистрирует токен если не был отправлен', () async {
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||
when(mockPrefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent)).thenReturn(false);
|
||||
when(mockFirebaseApi.registerAnonymousPushToken()).thenAnswer((_) async => {});
|
||||
when(mockPrefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true)).thenAnswer((_) async => {});
|
||||
@@ -150,7 +153,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('registerAnonymousPushToken - не регистрирует токен если уже был отправлен', () async {
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||
when(mockPrefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent)).thenReturn(true);
|
||||
|
||||
await authCubit!.registerAnonymousPushToken();
|
||||
@@ -159,7 +162,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('registerPushToken - регистрирует push токен', () async {
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||
when(mockFirebaseApi.registerPushToken()).thenAnswer((_) async => {});
|
||||
|
||||
await authCubit!.registerPushToken();
|
||||
@@ -168,7 +171,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('registerAnonymousPushToken - обрабатывает null значение hasAnonymousTokenBeenSent', () async {
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession);
|
||||
when(mockPrefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent)).thenReturn(null);
|
||||
when(mockFirebaseApi.registerAnonymousPushToken()).thenAnswer((_) async => {});
|
||||
when(mockPrefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true)).thenAnswer((_) async => {});
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../../../fixtures/doubles/session_doubles.dart';
|
||||
import '../../../../../fixtures/fixture_reader.dart';
|
||||
import 'oauth_cubit_test.mocks.dart';
|
||||
|
||||
@@ -37,6 +38,7 @@ void main() {
|
||||
late MockAuthRepository mockAuthRepository;
|
||||
late MockPrefs mockPrefs;
|
||||
late _RecordingFirebaseApi firebaseApi;
|
||||
late FakeSessionLifecycleService fakeSession;
|
||||
late TokenModel tokenFixture;
|
||||
late Map<String, dynamic> signInRequestFixture;
|
||||
late Map<String, dynamic> signUpRequestFixture;
|
||||
@@ -55,6 +57,7 @@ void main() {
|
||||
mockAuthRepository = MockAuthRepository();
|
||||
mockPrefs = MockPrefs();
|
||||
firebaseApi = _RecordingFirebaseApi();
|
||||
fakeSession = FakeSessionLifecycleService();
|
||||
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
@@ -87,7 +90,7 @@ void main() {
|
||||
test('начальное состояние - OAuthState', () {
|
||||
oauthCubit = OAuthCubit(
|
||||
authRepository: mockAuthRepository,
|
||||
firebaseApi: firebaseApi,
|
||||
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||
prefs: mockPrefs,
|
||||
);
|
||||
expect(oauthCubit!.state, const OAuthState());
|
||||
@@ -96,7 +99,7 @@ void main() {
|
||||
test('signInViaSocialMedia - успешный вход через соцсеть', () async {
|
||||
oauthCubit = OAuthCubit(
|
||||
authRepository: mockAuthRepository,
|
||||
firebaseApi: firebaseApi,
|
||||
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||
prefs: mockPrefs,
|
||||
);
|
||||
|
||||
@@ -129,7 +132,7 @@ void main() {
|
||||
test('signInViaSocialMedia - переход в состояние loading', () async {
|
||||
oauthCubit = OAuthCubit(
|
||||
authRepository: mockAuthRepository,
|
||||
firebaseApi: firebaseApi,
|
||||
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||
prefs: mockPrefs,
|
||||
);
|
||||
|
||||
@@ -160,7 +163,7 @@ void main() {
|
||||
test('signInViaSocialMedia - обработка ошибки', () async {
|
||||
oauthCubit = OAuthCubit(
|
||||
authRepository: mockAuthRepository,
|
||||
firebaseApi: firebaseApi,
|
||||
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||
prefs: mockPrefs,
|
||||
);
|
||||
|
||||
@@ -189,7 +192,7 @@ void main() {
|
||||
test('signUpViaSocialMedia - успешная регистрация через соцсеть', () async {
|
||||
oauthCubit = OAuthCubit(
|
||||
authRepository: mockAuthRepository,
|
||||
firebaseApi: firebaseApi,
|
||||
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||
prefs: mockPrefs,
|
||||
);
|
||||
|
||||
@@ -237,8 +240,7 @@ void main() {
|
||||
final loadedState = oauthCubit!.state as OAuthStateLoaded;
|
||||
expect(loadedState.token, tokenFixture.token);
|
||||
expect(await TokenStorage.getTokenFromStorage(), tokenFixture.token);
|
||||
expect(firebaseApi.registerPushTokenCalls, 1);
|
||||
expect(firebaseApi.hadAuthToken, isTrue);
|
||||
expect(fakeSession.onAuthenticatedCalls, 1);
|
||||
});
|
||||
|
||||
test(
|
||||
@@ -246,7 +248,7 @@ void main() {
|
||||
() async {
|
||||
oauthCubit = OAuthCubit(
|
||||
authRepository: mockAuthRepository,
|
||||
firebaseApi: firebaseApi,
|
||||
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||
prefs: mockPrefs,
|
||||
);
|
||||
|
||||
@@ -289,7 +291,7 @@ void main() {
|
||||
test('signUpViaSocialMedia - обработка ошибки валидации email', () async {
|
||||
oauthCubit = OAuthCubit(
|
||||
authRepository: mockAuthRepository,
|
||||
firebaseApi: firebaseApi,
|
||||
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||
prefs: mockPrefs,
|
||||
);
|
||||
|
||||
@@ -350,7 +352,7 @@ void main() {
|
||||
() async {
|
||||
oauthCubit = OAuthCubit(
|
||||
authRepository: mockAuthRepository,
|
||||
firebaseApi: firebaseApi,
|
||||
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||
prefs: mockPrefs,
|
||||
);
|
||||
|
||||
@@ -379,7 +381,7 @@ void main() {
|
||||
test('requestOAuthVerification - обработка ошибки', () async {
|
||||
oauthCubit = OAuthCubit(
|
||||
authRepository: mockAuthRepository,
|
||||
firebaseApi: firebaseApi,
|
||||
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||
prefs: mockPrefs,
|
||||
);
|
||||
|
||||
@@ -403,7 +405,7 @@ void main() {
|
||||
() async {
|
||||
oauthCubit = OAuthCubit(
|
||||
authRepository: mockAuthRepository,
|
||||
firebaseApi: firebaseApi,
|
||||
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||
prefs: mockPrefs,
|
||||
);
|
||||
|
||||
@@ -458,15 +460,14 @@ void main() {
|
||||
final loadedState = oauthCubit!.state as OAuthStateLoaded;
|
||||
expect(loadedState.token, tokenFixture.token);
|
||||
expect(await TokenStorage.getTokenFromStorage(), tokenFixture.token);
|
||||
expect(firebaseApi.registerPushTokenCalls, 1);
|
||||
expect(firebaseApi.hadAuthToken, isTrue);
|
||||
expect(fakeSession.onAuthenticatedCalls, 1);
|
||||
},
|
||||
);
|
||||
|
||||
test('confirmOAuthVerification - обработка ошибки', () async {
|
||||
oauthCubit = OAuthCubit(
|
||||
authRepository: mockAuthRepository,
|
||||
firebaseApi: firebaseApi,
|
||||
firebaseApi: firebaseApi, sessionLifecycleService: fakeSession,
|
||||
prefs: mockPrefs,
|
||||
);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../../fixtures/doubles/auth_doubles.dart';
|
||||
import '../../../../fixtures/doubles/profile_doubles.dart';
|
||||
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||
import '../../../../fixtures/fixture_reader.dart';
|
||||
import 'basic_widget_test.mocks.dart';
|
||||
|
||||
@@ -36,7 +37,7 @@ void main() {
|
||||
mockFirebaseApi = MockFirebaseApi();
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: FakeSessionLifecycleService());
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
@@ -55,7 +56,7 @@ void main() {
|
||||
|
||||
testWidgets('should create AuthCubit successfully', (WidgetTester tester) async {
|
||||
// Arrange & Act
|
||||
final cubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi);
|
||||
final cubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: FakeSessionLifecycleService());
|
||||
|
||||
// Assert
|
||||
expect(cubit, isA<AuthCubit>());
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ 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 '../../../../fixtures/doubles/session_doubles.dart';
|
||||
import 'menu_cubit_test.mocks.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<ProfileRepository>()])
|
||||
@@ -25,12 +27,12 @@ void main() {
|
||||
});
|
||||
|
||||
test('начальное состояние - MenuState', () {
|
||||
menuCubit = MenuCubit(mockProfileRepository);
|
||||
menuCubit = MenuCubit(mockProfileRepository, FakeSessionLifecycleService());
|
||||
expect(menuCubit!.state, isA<MenuState>());
|
||||
});
|
||||
|
||||
test('makePassKey - успешно создает passKey', () async {
|
||||
menuCubit = MenuCubit(mockProfileRepository);
|
||||
menuCubit = MenuCubit(mockProfileRepository, FakeSessionLifecycleService());
|
||||
const expectedPassKey = 'test-pass-key';
|
||||
|
||||
when(mockProfileRepository.getPassKey(
|
||||
@@ -48,7 +50,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('makePassKey - возвращает null при ошибке', () async {
|
||||
menuCubit = MenuCubit(mockProfileRepository);
|
||||
menuCubit = MenuCubit(mockProfileRepository, FakeSessionLifecycleService());
|
||||
|
||||
when(mockProfileRepository.getPassKey(
|
||||
routeName: 'product_nutritional_value.index',
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||
import 'menu_screen_smoke_test.mocks.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<ProfileRepository>()])
|
||||
@@ -138,7 +139,7 @@ void main() {
|
||||
|
||||
testWidgets('renders menu screen for user', (tester) async {
|
||||
final repository = MockProfileRepository();
|
||||
final cubit = MenuCubit(repository);
|
||||
final cubit = MenuCubit(repository, FakeSessionLifecycleService());
|
||||
|
||||
when(repository.getPassKey(routeName: anyNamed('routeName'), routeParams: anyNamed('routeParams'))).thenAnswer((_) async => 'test-passkey');
|
||||
when(repository.getMyProfile()).thenAnswer((_) async => mockUserProfile);
|
||||
@@ -155,7 +156,7 @@ void main() {
|
||||
|
||||
testWidgets('renders menu screen for consultant', (tester) async {
|
||||
final repository = MockProfileRepository();
|
||||
final cubit = MenuCubit(repository);
|
||||
final cubit = MenuCubit(repository, FakeSessionLifecycleService());
|
||||
|
||||
when(repository.getPassKey(routeName: anyNamed('routeName'), routeParams: anyNamed('routeParams'))).thenAnswer((_) async => 'test-passkey');
|
||||
when(repository.getMyProfile()).thenAnswer((_) async => mockConsultantProfile);
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
|
||||
import '../../fixtures/doubles/session_doubles.dart';
|
||||
import 'basic_widget_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([RegistrationRepository, Prefs])
|
||||
@@ -19,7 +20,7 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
mockRegistrationRepository = MockRegistrationRepository();
|
||||
registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs());
|
||||
registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs(), sessionLifecycleService: FakeSessionLifecycleService());
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
@@ -38,7 +39,7 @@ void main() {
|
||||
|
||||
testWidgets('should create RegistrationCubit successfully', (WidgetTester tester) async {
|
||||
// Arrange & Act
|
||||
final cubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs());
|
||||
final cubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs(), sessionLifecycleService: FakeSessionLifecycleService());
|
||||
|
||||
// Assert
|
||||
expect(cubit, isA<RegistrationCubit>());
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
|
||||
import '../../fixtures/doubles/session_doubles.dart';
|
||||
import 'integration_widget_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([RegistrationRepository, Prefs])
|
||||
@@ -17,7 +18,7 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
mockRegistrationRepository = MockRegistrationRepository();
|
||||
registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs());
|
||||
registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs(), sessionLifecycleService: FakeSessionLifecycleService());
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
|
||||
@@ -7,6 +7,8 @@ import 'package:eatvio_app/features/registration/repositories/registration_repos
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../../../../fixtures/doubles/session_doubles.dart';
|
||||
|
||||
class MockRegistrationRepository extends Mock implements RegistrationRepository {}
|
||||
|
||||
class MockPrefs extends Mock implements Prefs {}
|
||||
@@ -26,7 +28,7 @@ void main() {
|
||||
setUp(() {
|
||||
mockRepository = MockRegistrationRepository();
|
||||
mockPrefs = MockPrefs();
|
||||
cubit = RegistrationCubit(registrationRepository: mockRepository, prefs: mockPrefs);
|
||||
cubit = RegistrationCubit(registrationRepository: mockRepository, prefs: mockPrefs, sessionLifecycleService: FakeSessionLifecycleService());
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
|
||||
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||
import 'registration_screen_smoke_test.mocks.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<RegistrationRepository>(), MockSpec<Prefs>()])
|
||||
@@ -44,7 +45,7 @@ void main() {
|
||||
testWidgets('renders registration screen with initial step', (tester) async {
|
||||
final repository = MockRegistrationRepository();
|
||||
final prefs = MockPrefs();
|
||||
final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs);
|
||||
final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs, sessionLifecycleService: FakeSessionLifecycleService());
|
||||
|
||||
sl.registerLazySingleton<RegistrationRepository>(() => repository);
|
||||
sl.registerLazySingleton<Prefs>(() => prefs);
|
||||
@@ -59,7 +60,7 @@ void main() {
|
||||
testWidgets('renders phone screen as first step', (tester) async {
|
||||
final repository = MockRegistrationRepository();
|
||||
final prefs = MockPrefs();
|
||||
final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs);
|
||||
final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs, sessionLifecycleService: FakeSessionLifecycleService());
|
||||
|
||||
sl.registerLazySingleton<RegistrationRepository>(() => repository);
|
||||
sl.registerLazySingleton<Prefs>(() => prefs);
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../fixtures/doubles/session_doubles.dart';
|
||||
import 'registration_cubit_v2_test.mocks.dart';
|
||||
|
||||
class _RecordingFirebaseApi extends Fake implements FirebaseApi {
|
||||
@@ -30,6 +31,7 @@ void main() {
|
||||
late RegistrationCubitV2 cubit;
|
||||
late MockRegistrationRepositoryV2 mockRepository;
|
||||
late _RecordingFirebaseApi firebaseApi;
|
||||
late FakeSessionLifecycleService fakeSession;
|
||||
const MethodChannel passwordGeneratorChannel = MethodChannel(
|
||||
'flutter_password_generator',
|
||||
);
|
||||
@@ -37,9 +39,11 @@ void main() {
|
||||
setUp(() {
|
||||
mockRepository = MockRegistrationRepositoryV2();
|
||||
firebaseApi = _RecordingFirebaseApi();
|
||||
fakeSession = FakeSessionLifecycleService();
|
||||
cubit = RegistrationCubitV2(
|
||||
registrationRepository: mockRepository,
|
||||
firebaseApi: firebaseApi,
|
||||
sessionLifecycleService: fakeSession,
|
||||
);
|
||||
|
||||
// Инициализируем SharedPreferences для тестов
|
||||
@@ -212,8 +216,7 @@ void main() {
|
||||
expect(result.$1, isTrue);
|
||||
expect(result.$2, isFalse);
|
||||
expect(await TokenStorage.getTokenFromStorage(), token);
|
||||
expect(firebaseApi.registerPushTokenCalls, 1);
|
||||
expect(firebaseApi.hadAuthToken, isTrue);
|
||||
expect(fakeSession.onAuthenticatedCalls, 1);
|
||||
verify(
|
||||
mockRepository.register(
|
||||
registrationModel: anyNamed('registrationModel'),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
|
||||
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
@@ -7,6 +8,7 @@ import 'package:eatvio_app/features/settings/repositories/service_repository.dar
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||
import '../../../../fixtures/settings_fixtures.dart';
|
||||
import 'settings_cubit_test.mocks.dart';
|
||||
|
||||
@@ -23,6 +25,7 @@ void main() {
|
||||
late MockSettingsRepository mockSettingsRepository;
|
||||
late MockConsultantRepository mockConsultantRepository;
|
||||
late MockConsultationsRepository mockConsultationsRepository;
|
||||
late FakeSessionLifecycleService fakeSession;
|
||||
|
||||
|
||||
setUp(() {
|
||||
@@ -30,6 +33,7 @@ void main() {
|
||||
mockSettingsRepository = MockSettingsRepository();
|
||||
mockConsultantRepository = MockConsultantRepository();
|
||||
mockConsultationsRepository = MockConsultationsRepository();
|
||||
fakeSession = FakeSessionLifecycleService();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
@@ -46,6 +50,7 @@ void main() {
|
||||
settingsRepository: mockSettingsRepository,
|
||||
consultantRepository: mockConsultantRepository,
|
||||
consultationsRepository: mockConsultationsRepository,
|
||||
sessionLifecycleService: fakeSession,
|
||||
);
|
||||
expect(settingsCubit!.state, const SettingsState.loading());
|
||||
});
|
||||
@@ -56,6 +61,7 @@ void main() {
|
||||
settingsRepository: mockSettingsRepository,
|
||||
consultantRepository: mockConsultantRepository,
|
||||
consultationsRepository: mockConsultationsRepository,
|
||||
sessionLifecycleService: fakeSession,
|
||||
);
|
||||
|
||||
when(mockProfileRepository.getMyProfile())
|
||||
@@ -86,6 +92,7 @@ void main() {
|
||||
settingsRepository: mockSettingsRepository,
|
||||
consultantRepository: mockConsultantRepository,
|
||||
consultationsRepository: mockConsultationsRepository,
|
||||
sessionLifecycleService: fakeSession,
|
||||
);
|
||||
|
||||
// Ошибка происходит при загрузке настроек профиля (внутри try-catch блока)
|
||||
@@ -114,6 +121,7 @@ void main() {
|
||||
settingsRepository: mockSettingsRepository,
|
||||
consultantRepository: mockConsultantRepository,
|
||||
consultationsRepository: mockConsultationsRepository,
|
||||
sessionLifecycleService: fakeSession,
|
||||
);
|
||||
|
||||
when(mockProfileRepository.getMyProfile())
|
||||
@@ -145,6 +153,7 @@ void main() {
|
||||
settingsRepository: mockSettingsRepository,
|
||||
consultantRepository: mockConsultantRepository,
|
||||
consultationsRepository: mockConsultationsRepository,
|
||||
sessionLifecycleService: fakeSession,
|
||||
);
|
||||
|
||||
when(mockProfileRepository.getMyProfile())
|
||||
@@ -176,6 +185,7 @@ void main() {
|
||||
settingsRepository: mockSettingsRepository,
|
||||
consultantRepository: mockConsultantRepository,
|
||||
consultationsRepository: mockConsultationsRepository,
|
||||
sessionLifecycleService: fakeSession,
|
||||
);
|
||||
|
||||
when(mockProfileRepository.deleteAccount())
|
||||
@@ -184,6 +194,8 @@ void main() {
|
||||
final result = await settingsCubit!.deleteAccount();
|
||||
|
||||
expect(result, true);
|
||||
expect(fakeSession.endSessionCalls, 1);
|
||||
expect(fakeSession.lastEndReason, SessionEndReason.accountDeleted);
|
||||
verify(mockProfileRepository.deleteAccount()).called(1);
|
||||
});
|
||||
|
||||
@@ -193,6 +205,7 @@ void main() {
|
||||
settingsRepository: mockSettingsRepository,
|
||||
consultantRepository: mockConsultantRepository,
|
||||
consultationsRepository: mockConsultationsRepository,
|
||||
sessionLifecycleService: fakeSession,
|
||||
);
|
||||
|
||||
when(mockProfileRepository.deleteAccount())
|
||||
@@ -209,6 +222,7 @@ void main() {
|
||||
settingsRepository: mockSettingsRepository,
|
||||
consultantRepository: mockConsultantRepository,
|
||||
consultationsRepository: mockConsultationsRepository,
|
||||
sessionLifecycleService: fakeSession,
|
||||
);
|
||||
|
||||
when(mockProfileRepository.getMyProfile())
|
||||
@@ -244,6 +258,7 @@ void main() {
|
||||
settingsRepository: mockSettingsRepository,
|
||||
consultantRepository: mockConsultantRepository,
|
||||
consultationsRepository: mockConsultationsRepository,
|
||||
sessionLifecycleService: fakeSession,
|
||||
);
|
||||
|
||||
when(mockProfileRepository.getMyProfile())
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/se
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||
import '../../../../fixtures/settings_fixtures.dart';
|
||||
import '../bloc/settings_cubit_test.mocks.dart';
|
||||
|
||||
@@ -27,6 +28,7 @@ void main() {
|
||||
settingsRepository: mockSettingsRepository,
|
||||
consultantRepository: mockConsultantRepository,
|
||||
consultationsRepository: mockConsultationsRepository,
|
||||
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import '../../../../fixtures/doubles/session_doubles.dart';
|
||||
import 'settings_screen_smoke_test.mocks.dart';
|
||||
|
||||
@GenerateNiceMocks([
|
||||
@@ -57,6 +58,7 @@ void main() {
|
||||
settingsRepository: sl(),
|
||||
consultantRepository: sl(),
|
||||
consultationsRepository: sl(),
|
||||
sessionLifecycleService: FakeSessionLifecycleService(),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user