How to test the effect of FirebaseAuth.instance.signOut() that should make FirebaseAuth.instance.userChanges() invoke its listener? - flutter

I'm trying to write the code of the ApplicationState in Get to know Firebase for Flutter codelab while practicing Test Driven Development.
The method signOut in the codelab should be like this:
void signOut() {
// The question is about how to test the effect of this invocation
FirebaseAuth.instance.signOut();
}
If I understand it right, FirebaseAuth.instance.signOut() should make FirebaseAuth.instance.userChanges() invoke its listener with Stream<User?> that contains a null User. So the effect of invoking FirebaseAuth.instance.signOut() is not direct. I don't know how to mock this. According to Test Driven Development, I should never write a code unless I do that to let (a failing test) pass.
The problem is how to write a failing test that forces me to write the following code:
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loginState = ApplicationLoginState.loggedIn;
}
////////////////////////////////////////////////////////////
else {
_loginState = ApplicationLoginState.loggedOut; ////
}
////////////////////////////////////////////////////////////
notifyListeners();
});
}
I can mock FirebaseAuth.instance.signOut() like this:
// firebaseAuth is a mocked object
when(firebaseAuth.signOut())
.thenAnswer((realInvocation) => Completer<void>().future);
// sut (system under test)
sut.signOut();
verify(firebaseAuth.signOut()).called(1);
And this forces me to invoke FirebaseAuth.instance.signOut(). But this doesn't force me to write the aforementioned code to let it pass.
I test this code:
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FirebaseAuth.instance.userChanges().listen((user) {
/////////////////////////////////////////////////////////////
if (user != null) {
_loginState = ApplicationLoginState.loggedIn; ////
}
/////////////////////////////////////////////////////////////
else {
_loginState = ApplicationLoginState.loggedOut;
}
notifyListeners();
});
}
By mocking FirebaseAuth.instance.signInWithEmailAndPassword():
final userCredential = MockUserCredential();
// firebaseAuth is a mocked object
when(firebaseAuth.signInWithEmailAndPassword(
email: validEmail, password: password))
.thenAnswer((realInvocation) => Future.value(userCredential));
// sut (system under test)
await sut.signInWithEmailAndPassword(
validEmail, password, firebaseAuthExceptionCallback);
// This is the direct effect on my class, that will happen by the aforementioned code
expect(sut.loginState, ApplicationLoginState.loggedIn);
Please, be patient with me. I'm new to doing testing and Test Driven Development.

Here is what I did.
Instead of calling FirebaseAuth.instance.userChanges().listen() once in the init() method as in the codelab, I called it twice, one time on the signInWithEmailAndPassword() method, and one time on the signOut() method.
Future<void> signInWithEmailAndPassword(String email, String password,
void Function(FirebaseAuthException exception) errorCallback) async {
///////////////////////////////////////////////////////////////////
firebaseAuth.userChanges().listen(_whenNotNullUser); ////
///////////////////////////////////////////////////////////////////
try {
await firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
} on FirebaseAuthException catch (exception) {
errorCallback(exception);
}
}
Future<void> signOut() async {
///////////////////////////////////////////////////////////////////
firebaseAuth.userChanges().listen(_whenNullUser); ////
///////////////////////////////////////////////////////////////////
await firebaseAuth.signOut();
}
void _whenNullUser(User? user) {
if (user == null) {
_loginState = ApplicationLoginState.loggedOut;
notifyListeners();
}
}
void _whenNotNullUser(User? user) {
if (user != null) {
_loginState = ApplicationLoginState.loggedIn;
notifyListeners();
}
}
And this is my test:
test("""
$given $workingWithApplicationState
$wheN Calling signInWithEmailAndPassword()
$and Calling loginState returns ApplicationLoginState.loggedIn
$and Calling signOut()
$and Calling loginState returns ApplicationLoginState.loggedOut
$and Calling signInWithEmailAndPassword()
$then Calling loginState should return ApplicationLogginState.loggedIn
$and $notifyListenersCalled
""", () async {
when(firebaseAuth.signInWithEmailAndPassword(
email: validEmail, password: password))
.thenAnswer((realInvocation) => Future.value(userCredential));
await sut.signInWithEmailAndPassword(
validEmail, password, firebaseAuthExceptionCallback);
expect(sut.loginState, ApplicationLoginState.loggedIn);
reset(notifyListenerCall);
prepareUserChangesForTest(nullUser);
await sut.signOut();
verify(firebaseAuth.signOut()).called(1);
expect(sut.loginState, ApplicationLoginState.loggedOut);
reset(notifyListenerCall);
prepareUserChangesForTest(notNullUser);
when(firebaseAuth.signInWithEmailAndPassword(
email: validEmail, password: password))
.thenAnswer((realInvocation) => Future.value(userCredential));
await sut.signInWithEmailAndPassword(
validEmail, password, firebaseAuthExceptionCallback);
expect(sut.loginState, ApplicationLoginState.loggedIn);
verify(notifyListenerCall()).called(1);
});
Now I'm forced to write the login in both _whenNullUser() and _whenNotNullUser() methods to pass my test.

Related

Flutter FirebaseAuth unit testing

I'm trying to test my whole AuthManager class which use FirebaseAuth to signin, login. Here my file:
class AuthManager extends ChangeNotifier {
final FirebaseAuth auth;
Stream<User?> get user => auth.authStateChanges();
static Future<FirebaseApp> initializeFirebase({
required BuildContext context,
}) async {
FirebaseApp firebaseApp = await Firebase.initializeApp();
return firebaseApp;
}
AuthManager({required this.auth});
Future<String> signup(String email, String password) async {
try {
final credential = await auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
return "Success";
} on FirebaseAuthException catch (e) {
rethrow;
}
}
Future<String> signInWithEmailAndPassword(
String email, String password) async {
try {
final userCredential = await auth.signInWithEmailAndPassword(
email: email, password: password);
return "Success";
} on FirebaseAuthException catch (e) {
return "Failed";
} catch (e) {
rethrow;
}
}
static Future<String> signOut() async {
try {
await FirebaseAuth.instance.signOut();
return "Success";
} catch (e) {
rethrow;
}
}
}
I used to return the usercredential but wanted to try test a simple string return for the test, following this tutorial: https://www.youtube.com/watch?v=4d6hEaUVvuU, here is my test file
import 'package:firebase_auth/firebase_auth.dart';
import 'package:notes_firebase_app/data/models/auth_manager.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
class MockFirebaseAuth extends Mock implements FirebaseAuth {
#override
Stream<User> authStateChanges() {
return Stream.fromIterable([
_mockUser,
]);
}
}
class MockUser extends Mock implements User {}
final MockUser _mockUser = MockUser();
class MockUserCredential extends Mock implements Future<UserCredential> {}
void main() {
final MockFirebaseAuth mockFirebaseAuth = MockFirebaseAuth();
final AuthManager authManager = AuthManager(auth: mockFirebaseAuth);
final MockUserCredential mockUserCredential = MockUserCredential();
setUp(() {});
test("emit occurs", () async {
expectLater(authManager.user, emitsInOrder([_mockUser]));
});
test("create account", () async {
when(mockFirebaseAuth.createUserWithEmailAndPassword(
email: "tadas#gmail.com", password: "123456"))
.thenAnswer((realInvocation) => null);
expect(
await authManager.signInWithEmailAndPassword(
"tadas#gmail.com", "123456"),
"Success");
});
}
I face two problems here, cannot pass null because we need to handle it now or this throw this error
The return type 'Null' isn't a 'Future<UserCredential>', as required by the closure's context
Then I tried to mock UserCredential like this.
final MockUserCredential mockUserCredential = MockUserCredential();
when(mockFirebaseAuth.createUserWithEmailAndPassword(
email: "tadas#gmail.com", password: "123456"))
.thenAnswer((realInvocation) => mockUserCredential);
but I'm getting this error:
type 'Null' is not a subtype of type 'Future<UserCredential>'
What am I doing wrong ? Help will be much appreciated.
I am not totally sure but mockito package may need a generator. Try mocktail package and use
when(()=> mockFirebaseAuth.createUserWithEmailAndPassword(
email: "tadas#gmail.com", password: "123456")).thenAnswer((realInvocation) => null);
use callback function in when().

How can a method which accepts parameters be called without passing it's needed values?

I found this code snippet below on GitHub:
import 'package:flutter/widgets.dart';
import 'package:firebase_auth/firebase_auth.dart';
enum Status { Uninitialized, Authenticated, Authenticating, Unauthenticated }
class UserRepository with ChangeNotifier {
final FirebaseAuth auth;
FirebaseUser _user;
Status _status = Status.Uninitialized;
UserRepository.instance({this.auth}) {
auth.onAuthStateChanged.listen(onAuthStateChanged);
}
Status get status => _status;
FirebaseUser get user => _user;
Future<bool> signIn(String email, String password) async {
try {
_status = Status.Authenticating;
notifyListeners();
await auth.signInWithEmailAndPassword(email: email, password: password);
return true;
} catch (e) {
_status = Status.Unauthenticated;
notifyListeners();
return false;
}
}
Future signOut() async {
auth.signOut();
_status = Status.Unauthenticated;
notifyListeners();
return Future.delayed(Duration.zero);
}
Future<void> onAuthStateChanged(FirebaseUser firebaseUser) async {
if (firebaseUser == null) {
_status = Status.Unauthenticated;
} else {
_user = firebaseUser;
_status = Status.Authenticated;
}
notifyListeners();
}
}
At the top where UserRepository was instantiated,
UserRepository.instance({this.auth}) {
auth.onAuthStateChanged.listen(onAuthStateChanged);
}
on auth.onAuthStateChanged.listen he passes the onAuthStatechanged method. This method as you can see below the code snippet takes in parameter FirebaseUser firebaseUser but this is never passed when called.
My question is, how can this work then if it receives no value when called?
Full disclosure: This code isn't mine, it was/is available on GitHub. I only posted it here for whoever has an answer to my question to fully understand.
"auth.onAuthStateChanged.listen" itself is a function which takes
Future<void> Function(FirebaseUser) as an argument. Function eating function thats all.
OG author could've passed a unnamed function right there like this
auth.onAuthStateChanged.listen((FirebaseUser firebaseUser){});
but that would be less readable

Flutter bloc registration trouble

I have bloc, which listen to changes of currentUser state from firebase.
AuthBloc({#required AuthService authService})
: assert(authService != null),
_authService = authService,
super(AuthInitial()) {
_userSubscription = _authService.currentUser
.listen((user) => add(AuthenticationUserChanged(user)));
}
Then add event and the event call this function
Stream<AuthState> _mapAuthenticationUserChangedToState(
AuthenticationUserChanged event) async* {
if (event.user != null) {
// TO GET CUSTOM USER FROM FIRESTORE because of expiration, etc.
var user = await _authService.getUser(event.user.uid);
if (user.expiration == "") {
yield NotAuthorized(user);
} else {
var isAfter =
DateTime.now().toUtc().isAfter(DateTime.fromMillisecondsSinceEpoch(
user.expiration.millisecondsSinceEpoch,
isUtc: false,
).toUtc());
if (isAfter) {
yield NotAuthorized(user);
} else {
yield Authenticated(event.user);
}
}
} else {
yield Unautheticated();
}
But in my RegisterCubit I have signUpSubmitted method, which creates Firebase user and immediately after this is called my AuthBloc, which make sense.
But in my Bloc i need my custom Firestore user, which i have to create after FirebaseUser creation(because I need UID and email).
And this is the problem.
Future<void> signUpFormSubmitted() async {
if (!state.status.isValidated) return;
emit(state.copyWith(status: FormzStatus.submissionInProgress));
try {
await _authService
.registerUser(state.email.value, state.password.value)
.then((value) async {
// THIS IS CALLED AFTER BLOC
// I NEED TO CALL IT AFTER _registerUser() but in front of BLOC
var user =
ApplicationUser(uid: value.user.uid, email: value.user.email);
await _authService.addUserToDocument(user);
});
emit(state.copyWith(status: FormzStatus.submissionSuccess));
} on Exception {
print(Exception);
emit(state.copyWith(status: FormzStatus.submissionFailure));
}
}
My AuthService methods
Future<UserCredential> registerUser(email, password) =>
_auth.createUserWithEmailAndPassword(email: email, password: password);
Future<void> addUserToDocument(ApplicationUser user) {
return _db.collection('users').doc(user.uid).set({
'uid': user.uid,
'firstname': '',
'lastname': '',
'age': '',
'expiration': '',
'email': user.email
});
}

Firebase documentation for flutter does not work for deleting user

The following documentation on deleting a user does not work:
try {
await FirebaseAuth.instance.currentUser.delete();
} catch on FirebaseAuthException (e) {
if (e.code == 'requires-recent-login') {
print('The user must reauthenticate before this operation can be executed.');
}
}
"delete()" is not a function recognized by Flutter. "FirebaseAuthException" is also not recognized by Flutter.
How do I delete a user? Where do I find this information?
Using flutter, if you want to delete firebase accounts together with the associated firestore user collection document, the following method works fine. (documents in user collection named by the firebase uid).
Database Class
class DatabaseService {
final String uid;
DatabaseService({this.uid});
final CollectionReference userCollection =
Firestore.instance.collection('users');
Future deleteuser() {
return userCollection.document(uid).delete();
}
}
Use Firebase version 0.15.0 or above otherwise, Firebase reauthenticateWithCredential() method throw an error like { noSuchMethod: was called on null }.
Authentication Class
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
Future deleteUser(String email, String password) async {
try {
FirebaseUser user = await _auth.currentUser();
AuthCredential credentials =
EmailAuthProvider.getCredential(email: email, password: password);
print(user);
AuthResult result = await user.reauthenticateWithCredential(credentials);
await DatabaseService(uid: result.user.uid)
.deleteuser(); // called from database class
await result.user.delete();
return true;
} catch (e) {
print(e.toString());
return null;
}
}
}
Then use the following code inside the clickable event of a flutter widget tree to achieve the goal:
onTap: () async {
await AuthService().deleteUser(email, password);
}

Flutter api login using riverpod

I'm trying to use riverpod for login with a laravel backend. Right now I'm just returning true or false from the repository. I've set a form that accepts email and password. The isLoading variable is just to show a circle indicator. I've run the code and it works but not sure if I'm using riverpod correctly. Is there a better way to do it ?
auth_provider.dart
class Auth{
final bool isLogin;
Auth(this.isLogin);
}
class AuthNotifier extends StateNotifier<Auth>{
AuthNotifier() : super(Auth(false));
void isLogin(bool data){
state = new Auth(data);
}
}
final authProvider = StateNotifierProvider((ref) => new AuthNotifier());
auth_repository.dart
class AuthRepository{
static String url = "http://10.0.2.2:8000/api/";
final Dio _dio = Dio();
Future<bool> login(data) async {
try {
Response response = await _dio.post(url+'sanctum/token',data:json.encode(data));
return true;
} catch (error) {
return false;
}
}
}
login_screen.dart
void login() async{
if(formKey.currentState.validate()){
setState((){this.isLoading = true;});
var data = {
'email':this.email,
'password':this.password,
'device_name':'mobile_phone'
};
var result = await AuthRepository().login(data);
if(result){
context.read(authProvider).isLogin(true);
setState((){this.isLoading = false;});
}else
setState((){this.isLoading = false;});
}
}
Since I'm not coming from mobile background and just recently use flutter+riverpod in my recent project, I cannot say this is the best practice. But there are some points I'd like to note:
Use interface such IAuthRepository for repository. Riverpod can act as a dependency injection.
final authRepository = Provider<IAuthRepository>((ref) => AuthRepository());
Build data to send in repository. You should separate presentation, business logic, and explicit implementation for external resource if possible.
Future<bool> login(String email, String password) async {
try {
var data = {
'email': email,
'password': password,
'device_name':'mobile_phone'
};
Response response = await _dio.post(url+'sanctum/token',data:json.encode(data));
return true;
} catch (error) {
return false;
}
}
Do not call repository directly from presentation/screen. You can use the provider for your logic, which call the repository
class AuthNotifier extends StateNotifier<Auth>{
final ProviderReference ref;
IAuthRepository _authRepository;
AuthNotifier(this.ref) : super(Auth(false)) {
_authRepository = ref.watch(authRepository);
}
Future<void> login(String email, String password) async {
final loginResult = await_authRepository.login(email, password);
state = Auth(loginResult);
}
}
final authProvider = StateNotifierProvider((ref) => new AuthNotifier(ref));
On screen, you can call provider's login method
login() {
context.read(authProvider).login(this.email, this.password);
}
Use Consumer or ConsumerWidget to watch the state and decide what to build.
It also helps that instead of Auth with isLogin for the state, you can create some other state. At the very least, I usually create an abstract BaseAuthState, which derives to AuthInitialState, AuthLoadingState, AuthLoginState, AuthErrorState, etc.
class AuthNotifier extends StateNotifier<BaseAuthState>{
...
AuthNotifier(this.ref) : super(AuthInitialState()) { ... }
...
}
Consumer(builder: (context, watch, child) {
final state = watch(authProvider.state);
if (state is AuthLoginState) ...
else if (state is AuthLoadingState) ...
...
})
Instead of using a bool, I like to use enums or class for auth state
enum AuthState { initialize, authenticated, unauthenticated }
and for login state
enum LoginStatus { initialize, loading, success, failed }