Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lulin/task/add userusecase tests #70

Merged
merged 4 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions lib/app/injection_container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ import 'package:domain/usecases/auth_usecase.dart';
import 'package:domain/usecases/function_tools_usecase.dart';
import 'package:domain/usecases/user_usercase.dart';
import 'package:get_it/get_it.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;
import 'package:infrastructure/api_environment.dart';
import 'package:infrastructure/apple_sign_in_service.dart';
import 'package:infrastructure/authenticated_http_client.dart';
import 'package:infrastructure/feature_flag_provider.dart';
import 'package:infrastructure/google_sign_in_service.dart';
import 'package:infrastructure/token_provider.dart';
import 'package:swiftcomp/presentation/settings/viewModels/forget_password_view_model.dart';
import 'package:swiftcomp/presentation/settings/viewModels/login_view_model.dart';
Expand All @@ -32,11 +35,9 @@ final sl = GetIt.instance;
void initInjection() {
// ViewModels
sl.registerFactory<ChatViewModel>(() => ChatViewModel(
chatUseCase: sl(),
chatSessionUseCase: sl(),
functionToolsUseCase: sl(),
authUseCase: sl()));
sl.registerFactory<LoginViewModel>(() => LoginViewModel(authUseCase: sl()));
chatUseCase: sl(), chatSessionUseCase: sl(), functionToolsUseCase: sl(), authUseCase: sl()));
sl.registerFactory<LoginViewModel>(
() => LoginViewModel(authUseCase: sl(), appleSignInService: sl(), googleSignInService: sl()));
sl.registerFactory<SignupViewModel>(() => SignupViewModel(authUseCase: sl()));
sl.registerFactory<SettingsViewModel>(() => SettingsViewModel(
authUseCase: sl(), userUserCase: sl(), featureFlagProvider: sl()));
Expand Down Expand Up @@ -89,4 +90,6 @@ void initInjection() {
() => APIEnvironment());
sl.registerLazySingleton<TokenProvider>(() => TokenProvider());
sl.registerLazySingleton<FeatureFlagProvider>(() => FeatureFlagProvider());
sl.registerLazySingleton<AppleSignInService>(() => AppleSignInServiceImpl());
sl.registerLazySingleton<GoogleSignInService>(() => GoogleSignInServiceImpl());
}
44 changes: 25 additions & 19 deletions lib/presentation/settings/viewModels/login_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import 'package:flutter/foundation.dart';
import 'package:domain/usecases/auth_usecase.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:infrastructure/apple_sign_in_service.dart';
import 'package:infrastructure/google_sign_in_service.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:google_sign_in/google_sign_in.dart';

class LoginViewModel extends ChangeNotifier {
final AuthUseCase authUseCase;
final AppleSignInService appleSignInService;
final GoogleSignInService googleSignInService;

LoginViewModel({required this.authUseCase});
LoginViewModel({required this.authUseCase, required this.appleSignInService, required this.googleSignInService});

bool _isLoading = false;

Expand Down Expand Up @@ -67,17 +72,16 @@ class LoginViewModel extends ChangeNotifier {

try {
// Initialize GoogleSignIn instance
final GoogleSignIn googleSignIn = kIsWeb
? GoogleSignIn(
clientId: GOOGLE_SIGNIN_CLIENT_ID_WEB,
scopes: <String>['email', 'openid', 'profile'],
)
: GoogleSignIn(
scopes: <String>['email', 'openid', 'profile'],
);
final GoogleSignInUser? user = kIsWeb
? await googleSignInService.signIn(
clientId: GOOGLE_SIGNIN_CLIENT_ID_WEB,
scopes: <String>['email', 'openid', 'profile'],
)
: await googleSignInService.signIn(
scopes: <String>['email', 'openid', 'profile'],
);

// Sign in the user
final GoogleSignInAccount? user = await googleSignIn.signIn();
print(user);

if (user == null) {
// User canceled the sign-in
Expand All @@ -89,15 +93,15 @@ class LoginViewModel extends ChangeNotifier {
await syncUser(user.displayName, user.email, user.photoUrl);
} else {
// For non-web platforms, retrieve authentication details
final GoogleSignInAuthentication auth = await user.authentication;
final idToken = user.idToken;

// Ensure ID token is present
if (auth.idToken == null) {
if (idToken == null) {
throw Exception('Unable to retrieve ID token. Please try again.');
}

// Validate the ID token with your backend
final bool isValid = await authUseCase.validateGoogleToken(auth.idToken!);
final bool isValid = await authUseCase.validateGoogleToken(idToken);
if (!isValid) {
throw Exception('Google token validation failed.');
}
Expand All @@ -109,6 +113,7 @@ class LoginViewModel extends ChangeNotifier {
} catch (error) {
// Handle any errors during the process
print('Error during Google Sign-In: $error');
_errorMessage = error.toString();
} finally {
// Notify listeners regardless of success or failure
notifyListeners();
Expand All @@ -117,16 +122,16 @@ class LoginViewModel extends ChangeNotifier {

// Function to handle Google Sign-Out

Future<String> syncUser(String? displayName, String email, String? photoUrl) async {
Future<void> syncUser(String? displayName, String email, String? photoUrl) async {
final accessToken = await authUseCase.syncUser(displayName, email, photoUrl);
return accessToken;
}

Future<void> signInWithApple() async {
try {
_isSigningIn = false;
_errorMessage = null;
// Request credentials from Apple
final credential = await SignInWithApple.getAppleIDCredential(
final credential = await appleSignInService.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
Expand Down Expand Up @@ -158,8 +163,9 @@ class LoginViewModel extends ChangeNotifier {
// Notify listeners for UI update
notifyListeners();
} catch (e) {
print('Sign in with Apple failed: $e');
rethrow; // Optionally rethrow for higher-level error handling
_errorMessage = 'Sign in with Apple failed: $e';
_isSigningIn = false; // Reset signing in state
notifyListeners();// Optionally rethrow for higher-level error handling
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class UserProfileViewModel extends ChangeNotifier {
User? user;
bool isSignedIn = false;
bool isLoggedIn = false;
String errorMessage = '';
String? _errorMessage;
String? get errorMessage => _errorMessage;

UserProfileViewModel({required this.authUseCase, required this.userUseCase}) {
fetchAuthSessionNew();
Expand Down Expand Up @@ -84,13 +85,16 @@ class UserProfileViewModel extends ChangeNotifier {

Future<void> deleteUser() async {
setLoading(true);
_errorMessage = null;
try {
await userUseCase.deleteAccount();
print("Account deleted successfully");
} catch (e) {
print("Account deletion failed: $e");
_errorMessage = 'Delete failed: ${e.toString()}';
return null;
} finally {
setLoading(false);
}
setLoading(false);
}

void setLoading(bool value) {
Expand Down
4 changes: 2 additions & 2 deletions lib/presentation/settings/views/login_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class _LoginPageState extends State<LoginPage> {
// Display failure Snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Google Sign-In failed"),
content: Text(viewModel.errorMessage ?? "Google Sign-In failed"),
duration: Duration(seconds: 2),
backgroundColor: Colors.red,
),
Expand All @@ -132,7 +132,7 @@ class _LoginPageState extends State<LoginPage> {
// Display failure Snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Apple Sign-In failed"),
content: Text(viewModel.errorMessage ?? "Apple Sign in failed"),
duration: Duration(seconds: 2),
backgroundColor: Colors.red,
),
Expand Down
26 changes: 23 additions & 3 deletions lib/presentation/settings/views/user_profile_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,35 @@ class UserProfilePage extends StatelessWidget {
content: Text('Are you sure you want to delete your account?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text('Cancel'),
),
TextButton(
onPressed: () async {
Navigator.of(dialogContext).pop(); // Close the dialog first
await viewModel.deleteUser();
// Check if the widget is still mounted before navigating back
if (context.mounted) {

if (viewModel.errorMessage != null) {
// Display error dialog if there's an error
if (context.mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Error"),
content: Text(viewModel.errorMessage!),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Close the error dialog
},
child: Text('OK'),
),
],
),
);
}
} else if (context.mounted) {
// Navigate back with "refresh" if no error
Navigator.of(context).pop("refresh");
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/domain/lib/mocks/auth_usecase_mock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class MockAuthUseCase extends Mock implements AuthUseCase {
returnValueForMissingStub: Future.value(true));

@override
Future<String> syncUser(String? displayName, String email, String? photoUrl) =>
Future<void> syncUser(String? displayName, String email, String? photoUrl) =>
super.noSuchMethod(Invocation.method(#syncUser, [displayName, email, photoUrl]),
returnValue: Future.value(''),
returnValueForMissingStub: Future.value(''));
Expand Down
33 changes: 29 additions & 4 deletions packages/domain/lib/mocks/user_usecase_mock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,35 @@ import '../usecases/user_usercase.dart';

class MockUserUseCase extends Mock implements UserUseCase {
@override
Future<String> login(String email, String password) =>
super.noSuchMethod(Invocation.method(#login, [email, password]),
returnValue: Future.value(''),
returnValueForMissingStub: Future.value(''));
Future<User> fetchMe() =>
super.noSuchMethod(
Invocation.method(#fetchMe, []),
returnValue: Future.value(User(
username: 'mock_username',
email: '[email protected]',
name: 'Mock User',
description: 'This is a mock description',
avatarUrl: 'https://example.com/mock_avatar.png',
),),
returnValueForMissingStub: Future.value( User(
username: 'default',
email: '[email protected]',
)));

@override
Future<void> updateMe(String newName) =>
super.noSuchMethod(
Invocation.method(#updateMe, [newName]),
returnValue: Future.value(),
returnValueForMissingStub: Future.value(),
);

@override
Future<void> deleteAccount() =>
super.noSuchMethod(
Invocation.method(#deleteAccount, []),
returnValue: Future.value(),
returnValueForMissingStub: Future.value(),
);

}
6 changes: 3 additions & 3 deletions packages/domain/lib/usecases/auth_usecase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ abstract class AuthUseCase {
Future<String> resetPassword(String email, String newPassword, String confirmationCode);
Future<void> sendSignupVerificationCode(String email);
Future<String> updatePassword(String newPassword);
Future<String> syncUser(String? displayName, String email, String? photoUrl);
Future<void> syncUser(String? displayName, String email, String? photoUrl);
Future<String> validateAppleToken(String identityToken);
Future<bool> validateGoogleToken(String idToken);
Future<bool> isLoggedIn();
Expand Down Expand Up @@ -75,10 +75,10 @@ class AuthUseCaseImpl implements AuthUseCase {
return message;
}

Future<String> syncUser(String? displayName, String email, String? photoUrl) async {
Future<void> syncUser(String? displayName, String email, String? photoUrl) async {
String accessToken = await repository.syncUser(displayName, email, photoUrl);
await tokenProvider.saveToken(accessToken);
return accessToken;
return;
}

Future<String> validateAppleToken(String identityToken) async {
Expand Down
19 changes: 19 additions & 0 deletions packages/infrastructure/lib/apple_sign_in_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:sign_in_with_apple/sign_in_with_apple.dart';

abstract class AppleSignInService {
Future<AuthorizationCredentialAppleID> getAppleIDCredential({
required List<AppleIDAuthorizationScopes> scopes,
WebAuthenticationOptions? webAuthenticationOptions,
});
}

class AppleSignInServiceImpl implements AppleSignInService {
@override
Future<AuthorizationCredentialAppleID> getAppleIDCredential({
required List<AppleIDAuthorizationScopes> scopes,
WebAuthenticationOptions? webAuthenticationOptions,
}) {
return SignInWithApple.getAppleIDCredential(
scopes: scopes, webAuthenticationOptions: webAuthenticationOptions);
}
}
53 changes: 53 additions & 0 deletions packages/infrastructure/lib/google_sign_in_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'package:google_sign_in/google_sign_in.dart';

// Abstract service interface
abstract class GoogleSignInService {
Future<GoogleSignInUser?> signIn({
List<String> scopes = const <String>[],
String? hostedDomain,
String? clientId,
String? serverClientId,
bool forceCodeForRefreshToken = false,
});
}

class GoogleSignInUser {
String email;
String? displayName;
String? photoUrl;
String? idToken;

// Constructor
GoogleSignInUser(
{required this.email, this.displayName, this.photoUrl, required this.idToken});
}

// Implementation of the service
class GoogleSignInServiceImpl implements GoogleSignInService {
Future<GoogleSignInUser?> signIn({
List<String> scopes = const <String>[],
String? hostedDomain,
String? clientId,
String? serverClientId,
bool forceCodeForRefreshToken = false,
}) async {
final GoogleSignIn googleSignIn = GoogleSignIn(
scopes: scopes,
hostedDomain: hostedDomain,
clientId: clientId,
serverClientId: serverClientId,
forceCodeForRefreshToken: forceCodeForRefreshToken,
);
GoogleSignInAccount? user = await googleSignIn.signIn();
final GoogleSignInAuthentication? auth = await user?.authentication;
if (user != null) {
return GoogleSignInUser(
email: user.email,
displayName: user.displayName,
photoUrl: user.photoUrl,
idToken: auth?.idToken ?? '');
} else {
return null;
}
}
}
Loading
Loading