Firebase Sign In

So it's been a few weeks since I've had a chance to work on this app.

Before we get into UI, we need to be able to login and register. The Sign-in and on boarding will be done in the next installment. We need to build the services and repositories, and of course tests.

Setting Up Firebase

I won't give an exhaustive tutorial on how to set up Firebase with flutter. Detailed documentation can be found at https://firebase.google.com/docs/flutter/setup along with plenty of other tutorials on YouTube and Medium.

One thing to note is we don't want to commit our GoogleService-Info.plist and google-services.json in git. This would allow anyone to use our service for their things. We will add those files to our .gitingore.

The Interfaces

All of our services and repositories will be interfaces. This will allow us to swap components out easily for testing. Let's start with our repositories. The auth repository defines the following. It will live under lib/domain/repository/interface/auth_repository.dart

abstract class AuthRepository {
  Future<bool> signInWithEmailAndPassword(String email, String password);
  Future<bool> registerWithEmailAndPassword(String email, String password);
  Future<User> getUser();
}

We're not focusing on how these things are done, just what needs to be done, and make it someone else's problem.

In our service layer we have the AuthSerivce which looks like... (under /lib/data/service/interface/auth_service.dart of course)

abstract class AuthService {
  Future<bool> signInWithEmailAndPassword(String email, String password);
  Future<bool> registerUserWithEmailAndPassword(
      String email, String password, String confirmPassword);
  Future<bool> isSignedIn();
}

We'll notice that not everything lines up one to one, and that's because the service will do some work.

The modules

We're using some modules with get_it and injection. We want to be able to inject third party dependencies. We're going to start with Firebase and FirebaseAuth. Under the lib/injectable/module directory, we're going to create the following.  First up is firebase_module.dart

@module
abstract class FirebaseModule {
  @preResolve
  Future<FirebaseApp> get firebaseApp => Firebase.initializeApp();
}

Notice we have @preResolve. This is telling injectable to initialize this module at the start, rather than when needed. Firebase needs to be configured at start, and this kicks that off.

Next up we have firebase_auth_module.dart

@lazySingleton
class FirebaseAuthModule {
  final FirebaseApp firebaseApp;

  FirebaseAuthModule(this.firebaseApp);

  FirebaseAuth get firebaseAuth {
    return FirebaseAuth.instance;
  }
}

Here we need a singleton. We only want to have one FirebaseAuth going around.

Implementations

Once again starting at the repository

@Injectable(as: AuthRepository)
class FirebaseAuthRepository implements AuthRepository {
  final FirebaseAuthModule firebaseAuth;

  FirebaseAuthRepository(this.firebaseAuth);

  @override
  Future<bool> signInWithEmailAndPassword(String email, String password) async {
    try {
      var result = await firebaseAuth.firebaseAuth
          .signInWithEmailAndPassword(email: email, password: password);
      return result.user != null;
    } catch (_) {
      return false;
    }
  }

  @override
  Future<bool> registerWithEmailAndPassword(
      String email, String password) async {
    try {
      var result = await firebaseAuth.firebaseAuth
          .createUserWithEmailAndPassword(email: email, password: password);
      return result.user != null;
    } catch (_) {
      return false;
    }
  }

  @override
  Future<User> getUser() async {
    return firebaseAuth.firebaseAuth.currentUser;
  }
}

Now you see why I stopped to talk about the modules. We need that module to actually use this repository. We'll also notice the injectable notation. We use (as: AuthRepository). This will tell injectable to use this implementation whenever we ask for the interface. The last note I want to make is that the try/catch is to prevent PlatformExceptions. This happens when firebase has a problem so we'll handle it here.

Next up is lib/data/service/impl/auth_service_impl.dart

@Injectable(as: AuthService)
class AuthServiceImpl implements AuthService {
  final AuthRepository authRepository;

  AuthServiceImpl(this.authRepository);

  @override
  Future<bool> signInWithEmailAndPassword(String email, String password) {
    return authRepository.signInWithEmailAndPassword(email, password);
  }

  @override
  Future<bool> registerUserWithEmailAndPassword(
      String email, String password, String confirmPassword) async {
    if (password != confirmPassword) {
      return false;
    }
    return authRepository.registerWithEmailAndPassword(email, password);
  }

  @override
  Future<bool> isSignedIn() async {
    var user = await authRepository.getUser();
    return user != null;
  }
}

Our service here does a bit of logic. We'll take the inputs, and check that they're valid. Our isSignedIn method also just looks to see if the current user is null.

Test

If code isn't tested, does it work? Let's not take any chances.

We'll mirror the impl directories under tests/. First up is the auth repository

class MockFirebaseAuthModule extends Mock implements FirebaseAuthModule {}

class MockFirebaseAuth extends Mock implements FirebaseAuth {}

class MockUserCredential extends Mock implements UserCredential {}

class MockUser extends Mock implements User {}

void main() {
  MockFirebaseAuthModule mockFirebaseAuthModule;
  MockFirebaseAuth mockFirebaseAuth;
  FirebaseAuthRepository authRepositoryImpl;
  MockUserCredential userCredential;
  MockUser mockUser;

  setUp(() {
    mockFirebaseAuthModule = MockFirebaseAuthModule();
    mockFirebaseAuth = MockFirebaseAuth();
    when(mockFirebaseAuthModule.firebaseAuth).thenReturn(mockFirebaseAuth);
    authRepositoryImpl = FirebaseAuthRepository(mockFirebaseAuthModule);
    userCredential = MockUserCredential();
    mockUser = MockUser();
  });

  group("sign in with email and password", () {
    test("When user is returned from firebase, true is returned", () async {
      //arrange
      var email = "test@test.test";
      var password = "TestPass";
      when(mockFirebaseAuth.signInWithEmailAndPassword(
              email: email, password: password))
          .thenAnswer((_) => Future.value(userCredential));
      when(userCredential.user).thenReturn(mockUser);

      //act
      var result =
          await authRepositoryImpl.signInWithEmailAndPassword(email, password);

      //assert
      expect(result, true);
    });
    test("When null is returned from firebase, false is returned", () async {
      //arrange
      var email = "test@test.test";
      var password = "TestPass";
      when(mockFirebaseAuth.signInWithEmailAndPassword(
              email: email, password: password))
          .thenAnswer((_) => Future.value(userCredential));
      when(userCredential.user).thenReturn(null);

      //act
      var result =
          await authRepositoryImpl.signInWithEmailAndPassword(email, password);

      //assert
      expect(result, false);
    });
  });

  group("register with email and password", () {
    test("When user is returned from firebase, true is returned", () async {
      //arrange
      var email = "test@test.test";
      var password = "TestPass";
      when(mockFirebaseAuth.createUserWithEmailAndPassword(
              email: email, password: password))
          .thenAnswer((_) => Future.value(userCredential));
      when(userCredential.user).thenReturn(mockUser);

      //act
      var result = await authRepositoryImpl.registerWithEmailAndPassword(
          email, password);

      //assert
      expect(result, true);
    });
    test("When null is returned from firebase, false is returned", () async {
      //arrange
      var email = "test@test.test";
      var password = "TestPass";
      when(mockFirebaseAuth.createUserWithEmailAndPassword(
              email: email, password: password))
          .thenAnswer((_) => Future.value(userCredential));
      when(userCredential.user).thenReturn(null);

      //act
      var result = await authRepositoryImpl.registerWithEmailAndPassword(
          email, password);

      //assert
      expect(result, false);
    });
  });
}

That was a doosy. I like to follow AAA testing. Arrange, Act, Assert. At the top we have our mocks. This is a simple class that extends Mock and implements an interface.

In each group, we go through a single function, and test all of it's logic. Most of it is that we called the Firebase auth module, and that it returns what we expect. We'll do a similar thing for auth_service_impl_test.dart

class MockAuthRepository extends Mock implements AuthRepository {}

class MockUser extends Mock implements User {}

void main() {
  MockAuthRepository mockAuthRepository;
  AuthServiceImpl authServiceImpl;

  setUp(() {
    mockAuthRepository = MockAuthRepository();
    authServiceImpl = AuthServiceImpl(mockAuthRepository);
  });

  group("sign in with email and password", () {
    test(
        "When sign in with email and password is called, and true is returned from auth repository, true is returned",
        () async {
      //arrange
      var email = "test@test.test";
      var password = "TestPass";
      var returnValue = true;
      when(mockAuthRepository.signInWithEmailAndPassword(email, password))
          .thenAnswer((_) => Future.value(returnValue));

      //act
      var result =
          await authServiceImpl.signInWithEmailAndPassword(email, password);

      //assert
      expect(result, returnValue);
    });
    test(
        "When sign in with email and password is called, and false is returned from auth repository, false is returned",
        () async {
      //arrange
      var email = "test@test.test";
      var password = "TestPass";
      var returnValue = false;
      when(mockAuthRepository.signInWithEmailAndPassword(email, password))
          .thenAnswer((_) => Future.value(returnValue));

      //act
      var result =
          await authServiceImpl.signInWithEmailAndPassword(email, password);

      //assert
      expect(result, returnValue);
    });
  });

  group("register with email and password", () {
    test(
        "When register with email and password returns false, false is returned",
        () async {
      //arrange
      var email = "test@test.test";
      var password = "TestPass";
      var returnValue = false;
      when(mockAuthRepository.registerWithEmailAndPassword(email, password))
          .thenAnswer((_) => Future.value(returnValue));

      //act
      var result = await authServiceImpl.registerUserWithEmailAndPassword(
          email, password, password);

      //assert
      expect(result, returnValue);
    });

    test("When register with email and password returns true, true is returned",
        () async {
      //arrange
      var email = "test@test.test";
      var password = "TestPass";
      var returnValue = true;
      when(mockAuthRepository.registerWithEmailAndPassword(email, password))
          .thenAnswer((_) => Future.value(returnValue));

      //act
      var result = await authServiceImpl.registerUserWithEmailAndPassword(
          email, password, password);

      //assert
      expect(result, returnValue);
    });

    test("When password and confirm password don't match, false is returned",
        () async {
      //arrange
      var email = "test@test.test";
      var password = "TestPass";
      var confirmPassword = "TestPass1";

      //act
      var result = await authServiceImpl.registerUserWithEmailAndPassword(
          email, password, confirmPassword);

      //assert
      expect(result, false);
    });
  });

  group("is user signed in", () {
    test("When user is null, false is returned", () async {
      //arrange
      when(mockAuthRepository.getUser()).thenAnswer((_) => Future.value(null));

      //act
      var result = await authServiceImpl.isSignedIn();

      //assert
      expect(result, false);
    });

    test("when user is found, true is returned", () async {
      //arrange
      when(mockAuthRepository.getUser())
          .thenAnswer((_) => Future.value(MockUser()));

      //act
      var result = await authServiceImpl.isSignedIn();

      //assert
      expect(result, true);
    });
  });
}

Another doosy, but should be straight forward to follow. The only thing left to do is run flutter test to make sure everything passes, and it does. Now we commit!

What's next

Next up we'll do the UI for the signin. We'll talk about navigators, cubit, and state.

Casey Daniel

Casey Daniel

Canada
Tracy Pyett

Tracy Pyett