diff --git a/app/build.gradle b/app/build.gradle index 151fd47..7ba178e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,6 +49,13 @@ dependencies { implementation Dependencies.retrofitGsonConverter implementation Dependencies.okHttpLogger implementation Dependencies.ok2curl + implementation Dependencies.arrowCore + implementation Dependencies.arrowTypeclasses + implementation Dependencies.arrowEffects + implementation Dependencies.arrowEffectsKotlinxCoroutines + implementation Dependencies.kotlinxCoroutinesCore + implementation Dependencies.kotlinxCoroutinesAndroid + testImplementation Dependencies.jUnit testImplementation Dependencies.mockito testImplementation Dependencies.mockitoKotlin @@ -56,6 +63,7 @@ dependencies { testImplementation Dependencies.kluent testImplementation Dependencies.wiremock testImplementation Dependencies.mockwebserver + androidTestImplementation Dependencies.AndroidTestRunner androidTestImplementation Dependencies.espresso } diff --git a/app/src/main/java/com/jcminarro/authexample/internal/interactor/FPAsyncInteractor.kt b/app/src/main/java/com/jcminarro/authexample/internal/interactor/FPAsyncInteractor.kt new file mode 100644 index 0000000..f9974cb --- /dev/null +++ b/app/src/main/java/com/jcminarro/authexample/internal/interactor/FPAsyncInteractor.kt @@ -0,0 +1,13 @@ +package com.jcminarro.authexample.internal.interactor + +import arrow.core.Either +import arrow.effects.DeferredK +import arrow.effects.unsafeRunSync + +abstract class FPAsyncInteractor { + + fun execute(input: I, onError: (E) -> Unit = {}, onSuccess: (O) -> Unit): Unit = + DeferredK{run(input)}.unsafeRunSync().fold(onError, onSuccess) + + protected abstract fun run(input: I): Either +} \ No newline at end of file diff --git a/app/src/main/java/com/jcminarro/authexample/internal/network/ApiClient.java b/app/src/main/java/com/jcminarro/authexample/internal/network/ApiClient.java deleted file mode 100644 index 2373f4e..0000000 --- a/app/src/main/java/com/jcminarro/authexample/internal/network/ApiClient.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.jcminarro.authexample.internal.network; - -import java.io.IOException; - -import retrofit2.Call; -import retrofit2.Response; - -public class ApiClient { - - protected final T endpoint; - - public ApiClient(T endpoint) { - this.endpoint = endpoint; - } - - protected U evaluateCall(Call call) throws IOException { - Response response = call.execute(); - if (!response.isSuccessful()) { - throw new APIIOException(response.raw()); - } - return response.body(); - } -} diff --git a/app/src/main/java/com/jcminarro/authexample/internal/network/ApiClient.kt b/app/src/main/java/com/jcminarro/authexample/internal/network/ApiClient.kt new file mode 100644 index 0000000..c4780af --- /dev/null +++ b/app/src/main/java/com/jcminarro/authexample/internal/network/ApiClient.kt @@ -0,0 +1,19 @@ +package com.jcminarro.authexample.internal.network + +import arrow.core.Try +import retrofit2.Call +import java.io.IOException + +open class ApiClient(protected val endpoint: T) { + + @Throws(IOException::class) + protected fun evaluateCall(call: Call): U { + val response = call.execute() + if (!response.isSuccessful) { + throw APIIOException(response.raw()) + } + return response.body() + } + + protected fun evaluateCallFP(call: Call): Try = Try {evaluateCall(call)} +} diff --git a/app/src/main/java/com/jcminarro/authexample/internal/network/login/LoginApiClient.kt b/app/src/main/java/com/jcminarro/authexample/internal/network/login/LoginApiClient.kt index 4261dd7..21e7458 100644 --- a/app/src/main/java/com/jcminarro/authexample/internal/network/login/LoginApiClient.kt +++ b/app/src/main/java/com/jcminarro/authexample/internal/network/login/LoginApiClient.kt @@ -1,5 +1,6 @@ package com.jcminarro.authexample.internal.network.login +import arrow.core.Try import com.jcminarro.authexample.internal.network.APIIOException import com.jcminarro.authexample.internal.network.ApiClient import com.jcminarro.authexample.internal.network.OAuth @@ -10,5 +11,10 @@ constructor(endpoint: LoginEndpoint) : ApiClient(endpoint) { @Throws(APIIOException::class) fun login(username: String, password: String): OAuth = - map(evaluateCall(endpoint.login(LoginBody(username, password)))) + map(evaluateCall(loginCall(username, password))) + + fun loginFP(username: String, password: String): Try = + evaluateCallFP(loginCall(username, password)).map {map(it)} + + private fun loginCall(username: String, password: String) = endpoint.login(LoginBody(username, password)) } diff --git a/app/src/main/java/com/jcminarro/authexample/internal/repository/SessionRepository.java b/app/src/main/java/com/jcminarro/authexample/internal/repository/SessionRepository.java deleted file mode 100644 index 97d4f86..0000000 --- a/app/src/main/java/com/jcminarro/authexample/internal/repository/SessionRepository.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.jcminarro.authexample.internal.repository; - -import com.jcminarro.authexample.internal.localdatasource.SessionDatasource; -import com.jcminarro.authexample.internal.network.APIIOException; -import com.jcminarro.authexample.internal.network.OAuth; -import com.jcminarro.authexample.internal.network.login.LoginApiClient; -import com.jcminarro.authexample.internal.network.refresh.RefreshApiClient; - -import javax.inject.Inject; - -public class SessionRepository { - - private final LoginApiClient loginApiClient; - private final RefreshApiClient refreshApiClient; - private final SessionDatasource sessionDatasource; - - @Inject - public SessionRepository( - LoginApiClient loginApiClient, - RefreshApiClient refreshApiClient, - SessionDatasource sessionDatasource) { - this.loginApiClient = loginApiClient; - this.refreshApiClient = refreshApiClient; - this.sessionDatasource = sessionDatasource; - } - - public boolean login(String username, String password) throws APIIOException { - sessionDatasource.storeOAuthSession(loginApiClient.login(username, password)); - return true; - } - - public boolean refreshSession() { - OAuth oAuth = sessionDatasource.getOAuthSession(); - if (oAuth != null) { - try { - sessionDatasource.storeOAuthSession(refreshApiClient.refresh(oAuth.getRefreshToken())); - return true; - } catch (Exception e) { - e.printStackTrace(); - } - } - return false; - } -} diff --git a/app/src/main/java/com/jcminarro/authexample/internal/repository/SessionRepository.kt b/app/src/main/java/com/jcminarro/authexample/internal/repository/SessionRepository.kt new file mode 100644 index 0000000..94fe259 --- /dev/null +++ b/app/src/main/java/com/jcminarro/authexample/internal/repository/SessionRepository.kt @@ -0,0 +1,40 @@ +package com.jcminarro.authexample.internal.repository + +import arrow.core.Try +import com.jcminarro.authexample.internal.localdatasource.SessionDatasource +import com.jcminarro.authexample.internal.network.APIIOException +import com.jcminarro.authexample.internal.network.login.LoginApiClient +import com.jcminarro.authexample.internal.network.refresh.RefreshApiClient +import javax.inject.Inject + +class SessionRepository @Inject +constructor( + private val loginApiClient: LoginApiClient, + private val refreshApiClient: RefreshApiClient, + private val sessionDatasource: SessionDatasource) { + + @Throws(APIIOException::class) + fun login(username: String, password: String): Boolean { + sessionDatasource.storeOAuthSession(loginApiClient.login(username, password)) + return true + } + + fun loginFP(username: String, password: String): Try = + loginApiClient.loginFP(username, password).map { + sessionDatasource.storeOAuthSession(it) + true + } + + fun refreshSession(): Boolean { + val oAuth = sessionDatasource.oAuthSession + if (oAuth != null) { + try { + sessionDatasource.storeOAuthSession(refreshApiClient.refresh(oAuth.refreshToken)) + return true + } catch (e: Exception) { + e.printStackTrace() + } + } + return false + } +} diff --git a/app/src/main/java/com/jcminarro/authexample/login/LoginActivity.java b/app/src/main/java/com/jcminarro/authexample/login/LoginActivity.java index 1a7e22a..b09be06 100644 --- a/app/src/main/java/com/jcminarro/authexample/login/LoginActivity.java +++ b/app/src/main/java/com/jcminarro/authexample/login/LoginActivity.java @@ -62,7 +62,7 @@ protected void initDI() { } private void onLogin() { - presenter.login(username.getText().toString(), + presenter.loginFP(username.getText().toString(), password.getText().toString()); } diff --git a/app/src/main/java/com/jcminarro/authexample/login/LoginFPInteractor.kt b/app/src/main/java/com/jcminarro/authexample/login/LoginFPInteractor.kt new file mode 100644 index 0000000..3e60ad6 --- /dev/null +++ b/app/src/main/java/com/jcminarro/authexample/login/LoginFPInteractor.kt @@ -0,0 +1,18 @@ +package com.jcminarro.authexample.login + +import arrow.core.Either +import arrow.core.Left +import arrow.core.Right +import com.jcminarro.authexample.internal.interactor.FPAsyncInteractor +import com.jcminarro.authexample.internal.repository.SessionRepository +import javax.inject.Inject + +class LoginFPInteractor @Inject constructor(private val sessionRepository: SessionRepository) + : FPAsyncInteractor() { + override fun run(input: Input): Either = with(input) { + sessionRepository.loginFP(username, password).fold({Left(CredentialError)}, {Right(it)}) + } +} + +data class Input(val username: String, val password: String) +object CredentialError \ No newline at end of file diff --git a/app/src/main/java/com/jcminarro/authexample/login/LoginPresenter.java b/app/src/main/java/com/jcminarro/authexample/login/LoginPresenter.java deleted file mode 100644 index 35512f8..0000000 --- a/app/src/main/java/com/jcminarro/authexample/login/LoginPresenter.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.jcminarro.authexample.login; - -import android.text.TextUtils; - -import com.jcminarro.authexample.internal.interactor.InteractorExecutor; -import com.jcminarro.authexample.internal.navigator.Navigator; -import com.jcminarro.authexample.internal.presenter.BasePresenter; - -import javax.inject.Inject; - -public class LoginPresenter extends BasePresenter { - - private final LoginInteractor loginInteractor; - private final Navigator navigator; - - @Inject - public LoginPresenter(LoginInteractor loginInteractor, Navigator navigator) { - this.loginInteractor = loginInteractor; - this.navigator = navigator; - } - - @Override - public void update() { - super.update(); - showLoginStatus(); - } - - private void showLoginStatus() { - getView().showLogin(); - getView().hideLoading(); - } - - public void login(String username, String password) { - if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) { - onLoginError(); - } else { - performLogin(username, password); - } - } - - private void performLogin(String username, String password) { - showLoadingStatus(); - execute(loginInteractor, - new LoginInteractor.Input(username, password), - new InteractorExecutor.Callback() { - @Override - public void onSuccess(Boolean isLoggedIn) { - if (isLoggedIn) { - onLoggedIn(); - } else { - onLoginError(); - } - } - - @Override - public void onError(Exception error) { - onLoginError(); - } - }); - } - - private void showLoadingStatus() { - getView().showLoading(); - getView().hideLogin(); - } - - private void onLoginError() { - showLoginStatus(); - getView().showError(); - } - - private void onLoggedIn() { - getView().close(); - navigator.navigateToQuote(); - } - - interface View extends BasePresenter.View { - - void close(); - - void showError(); - - void showLogin(); - - void hideLogin(); - - void showLoading(); - - void hideLoading(); - } -} diff --git a/app/src/main/java/com/jcminarro/authexample/login/LoginPresenter.kt b/app/src/main/java/com/jcminarro/authexample/login/LoginPresenter.kt new file mode 100644 index 0000000..df70cfe --- /dev/null +++ b/app/src/main/java/com/jcminarro/authexample/login/LoginPresenter.kt @@ -0,0 +1,84 @@ +package com.jcminarro.authexample.login + +import com.jcminarro.authexample.internal.interactor.InteractorExecutor +import com.jcminarro.authexample.internal.navigator.Navigator +import com.jcminarro.authexample.internal.presenter.BasePresenter +import javax.inject.Inject + +class LoginPresenter @Inject constructor( + private val loginInteractor: LoginInteractor, + private val loginFPInteractor: LoginFPInteractor, + private val navigator: Navigator) : BasePresenter() { + + override fun update() { + super.update() + showLoginStatus() + } + + private fun showLoginStatus() { + view.showLogin() + view.hideLoading() + } + + fun login(username: String, password: String) = + performLoginIfNotEmptyCredential(username, password, this::performLogin) + + fun loginFP(username: String, password: String) = + performLoginIfNotEmptyCredential(username, password, this::performLoginFP) + + private fun performLoginFP(username: String, password: String) { + showLoadingStatus() + loginFPInteractor.execute(Input(username, password), {onLoginError()}, this::onLoginSuccess) + } + + private fun performLogin(username: String, password: String) { + showLoadingStatus() + execute(loginInteractor, + LoginInteractor.Input(username, password), + object : InteractorExecutor.Callback { + override fun onSuccess(isLoggedIn: Boolean) = onLoginSuccess(isLoggedIn) + override fun onError(error: Exception) = onLoginError() + }) + } + + private fun onLoginSuccess(isLoggedIn: Boolean) = if (isLoggedIn) { + onLoggedIn() + } else { + onLoginError() + } + + private fun performLoginIfNotEmptyCredential( + username: String, + password: String, + loginFunction: (String, String) -> Unit) = + if (username.isBlank() || password.isBlank()) { + onLoginError() + } else { + loginFunction(username, password) + } + + private fun showLoadingStatus() { + view.showLoading() + view.hideLogin() + } + + private fun onLoginError() { + showLoginStatus() + view.showError() + } + + private fun onLoggedIn() { + view.close() + navigator.navigateToQuote() + } + + interface View : BasePresenter.View { + + fun close() + fun showError() + fun showLogin() + fun hideLogin() + fun showLoading() + fun hideLoading() + } +} diff --git a/app/src/test/java/com/jcminarro/authexample/internal/network/login/LoginApiClientTest.kt b/app/src/test/java/com/jcminarro/authexample/internal/network/login/LoginApiClientTest.kt index d62a952..934c0ee 100644 --- a/app/src/test/java/com/jcminarro/authexample/internal/network/login/LoginApiClientTest.kt +++ b/app/src/test/java/com/jcminarro/authexample/internal/network/login/LoginApiClientTest.kt @@ -1,5 +1,7 @@ package com.jcminarro.authexample.internal.network.login +import arrow.core.Failure +import arrow.core.Success import com.github.tomakehurst.wiremock.client.WireMock.aResponse import com.github.tomakehurst.wiremock.client.WireMock.equalToJson import com.github.tomakehurst.wiremock.client.WireMock.post @@ -13,7 +15,10 @@ import com.jcminarro.authexample.createLoginEndpoint import com.jcminarro.authexample.createLoginResponse import com.jcminarro.authexample.createLoginResponseJson import com.jcminarro.authexample.internal.network.APIIOException +import com.jcminarro.authexample.internal.network.OAuth +import org.amshove.kluent.`should be instance of` import org.amshove.kluent.`should equal to` +import org.amshove.kluent.`should equal` import org.junit.Before import org.junit.Rule import org.junit.Test @@ -34,6 +39,7 @@ class LoginApiClientTest { @Before fun setUp() { + val invalidLoginResponse = aResponse().withStatus(401) loginApiClient = LoginApiClient( createLoginEndpoint(EndpointMother.DEFAULT_API_HOST + wiremockRule.port())) stubFor(post(EndpointPath.LOGIN) @@ -43,10 +49,10 @@ class LoginApiClientTest { createLoginResponse())))) stubFor(post(EndpointPath.LOGIN) .withRequestBody(equalToJson(createRequestBodyJson(VALID_USERNAME, INVALID_PASSWORD))) - .willReturn(aResponse().withStatus(401))) + .willReturn(invalidLoginResponse)) stubFor(post(EndpointPath.LOGIN) .withRequestBody(equalToJson(createRequestBodyJson(INVALID_USERNAME, VALID_PASSWORD))) - .willReturn(aResponse().withStatus(401))) + .willReturn(invalidLoginResponse)) } @Test @@ -67,6 +73,29 @@ class LoginApiClientTest { loginApiClient.login(INVALID_USERNAME, VALID_PASSWORD) } + @Test + fun `Should return a Success of OAuth when login with valid credential`() { + val oAuth = loginApiClient.loginFP(VALID_USERNAME, VALID_PASSWORD) + + oAuth `should equal` Success(OAuth(ResponseMother.LOGIN_MOTHERR_accessToken, ResponseMother.LOGIN_MOTHERR_refreshToken)) + } + + @Test() + fun `Should return Failure of OAuth when try to login with an invalid password`() { + val oAuth = loginApiClient.loginFP(VALID_USERNAME, INVALID_PASSWORD) + + oAuth `should be instance of` Failure::class + (oAuth as Failure).exception `should be instance of` APIIOException::class + } + + @Test() + fun `Should return Failure of OAuth when try to login with an invalid username`() { + val oAuth = loginApiClient.loginFP(INVALID_USERNAME, VALID_PASSWORD) + + oAuth `should be instance of` Failure::class + (oAuth as Failure).exception `should be instance of` APIIOException::class + } + fun createRequestBodyJson(username: String, password: String) = """ { diff --git a/app/src/test/java/com/jcminarro/authexample/internal/network/login/LoginApiClientWithMockWebServerTest.kt b/app/src/test/java/com/jcminarro/authexample/internal/network/login/LoginApiClientWithMockWebServerTest.kt index b2d540f..5fbd465 100644 --- a/app/src/test/java/com/jcminarro/authexample/internal/network/login/LoginApiClientWithMockWebServerTest.kt +++ b/app/src/test/java/com/jcminarro/authexample/internal/network/login/LoginApiClientWithMockWebServerTest.kt @@ -1,17 +1,22 @@ package com.jcminarro.authexample.internal.network.login +import arrow.core.Failure +import arrow.core.Success import com.jcminarro.authexample.EndpointPath import com.jcminarro.authexample.ResponseMother import com.jcminarro.authexample.createLoginEndpoint import com.jcminarro.authexample.createLoginResponse import com.jcminarro.authexample.createLoginResponseJson import com.jcminarro.authexample.internal.network.APIIOException +import com.jcminarro.authexample.internal.network.OAuth import com.jcminarro.authexample.removeAllSpaces import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.RecordedRequest +import org.amshove.kluent.`should be instance of` import org.amshove.kluent.`should equal to` +import org.amshove.kluent.`should equal` import org.junit.Before import org.junit.Test @@ -76,6 +81,29 @@ class LoginApiClientWithMockWebServerTest { loginApiClient.login(INVALID_USERNAME, VALID_PASSWORD) } + @Test + fun `Should return a Success of OAuth when login with valid credential`() { + val oAuth = loginApiClient.loginFP(VALID_USERNAME, VALID_PASSWORD) + + oAuth `should equal` Success(OAuth(ResponseMother.LOGIN_MOTHERR_accessToken, ResponseMother.LOGIN_MOTHERR_refreshToken)) + } + + @Test() + fun `Should return Failure of OAuth when try to login with an invalid password`() { + val oAuth = loginApiClient.loginFP(VALID_USERNAME, INVALID_PASSWORD) + + oAuth `should be instance of` Failure::class + (oAuth as Failure).exception `should be instance of` APIIOException::class + } + + @Test() + fun `Should return Failure of OAuth when try to login with an invalid username`() { + val oAuth = loginApiClient.loginFP(INVALID_USERNAME, VALID_PASSWORD) + + oAuth `should be instance of` Failure::class + (oAuth as Failure).exception `should be instance of` APIIOException::class + } + fun createRequestBodyJson(username: String, password: String) = """ { diff --git a/app/src/test/java/com/jcminarro/authexample/internal/repository/SessionRepositoryTest.kt b/app/src/test/java/com/jcminarro/authexample/internal/repository/SessionRepositoryTest.kt index 7c52a0b..146e6f2 100644 --- a/app/src/test/java/com/jcminarro/authexample/internal/repository/SessionRepositoryTest.kt +++ b/app/src/test/java/com/jcminarro/authexample/internal/repository/SessionRepositoryTest.kt @@ -1,5 +1,7 @@ package com.jcminarro.authexample.internal.repository +import arrow.core.Failure +import arrow.core.Success import com.jcminarro.authexample.internal.localdatasource.SessionDatasource import com.jcminarro.authexample.internal.network.APIIOException import com.jcminarro.authexample.internal.network.OAuth @@ -7,7 +9,19 @@ import com.jcminarro.authexample.internal.network.login.LoginApiClient import com.jcminarro.authexample.internal.network.refresh.RefreshApiClient import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.doThrow -import org.amshove.kluent.* +import org.amshove.kluent.Verify +import org.amshove.kluent.When +import org.amshove.kluent.`Verify no further interactions` +import org.amshove.kluent.`Verify no interactions` +import org.amshove.kluent.`should be false` +import org.amshove.kluent.`should be true` +import org.amshove.kluent.`should equal` +import org.amshove.kluent.called +import org.amshove.kluent.calling +import org.amshove.kluent.mock +import org.amshove.kluent.on +import org.amshove.kluent.that +import org.amshove.kluent.was import org.junit.Before import org.junit.Test @@ -33,6 +47,9 @@ class SessionRepositoryTest { When calling loginApiClient.login(VALID_USERNAME, VALID_PASSWORD) doReturn validOAuth When calling loginApiClient.login(INVALID_USERNAME, VALID_PASSWORD) doThrow apiIoException When calling loginApiClient.login(VALID_USERNAME, INVALID_PASSWORD) doThrow apiIoException + When calling loginApiClient.loginFP(VALID_USERNAME, VALID_PASSWORD) doReturn Success(validOAuth) + When calling loginApiClient.loginFP(INVALID_USERNAME, VALID_PASSWORD) doReturn Failure(apiIoException) + When calling loginApiClient.loginFP(VALID_USERNAME, INVALID_PASSWORD) doReturn Failure(apiIoException) When calling refreshApiClient.refresh(VALID_REFRESH_TOKEN) doReturn validOAuth When calling refreshApiClient.refresh(INVALID_REFRESH_TOKEN) doThrow apiIoException } @@ -59,6 +76,30 @@ class SessionRepositoryTest { `Verify no further interactions` on sessionDatasource } + @Test + fun `Should store an OAuth when loginFP with valid credential`() { + val result = sessionRepository.loginFP(VALID_USERNAME, VALID_PASSWORD) + + result `should equal` Success(true) + Verify on sessionDatasource that sessionDatasource.storeOAuthSession(validOAuth) was called + } + + @Test + fun `Should throw an exception when try to loginFP with an invalid password`() { + val result = sessionRepository.loginFP(VALID_USERNAME, INVALID_PASSWORD) + + result `should equal` Failure(apiIoException) + `Verify no further interactions` on sessionDatasource + } + + @Test + fun `Should throw an exception when try to loginFP with an invalid username`() { + val result = sessionRepository.loginFP(INVALID_USERNAME, VALID_PASSWORD) + + result `should equal` Failure(apiIoException) + `Verify no further interactions` on sessionDatasource + } + @Test fun `Should store an OAuth when refreshing with a valid refresh token receive an OAuth`() { When calling sessionDatasource.oAuthSession doReturn validOAuth diff --git a/app/src/test/java/com/jcminarro/authexample/login/LoginFPInteractorTest.kt b/app/src/test/java/com/jcminarro/authexample/login/LoginFPInteractorTest.kt new file mode 100644 index 0000000..feaf015 --- /dev/null +++ b/app/src/test/java/com/jcminarro/authexample/login/LoginFPInteractorTest.kt @@ -0,0 +1,68 @@ +package com.jcminarro.authexample.login + +import arrow.core.Failure +import arrow.core.Success +import com.jcminarro.authexample.internal.network.APIIOException +import com.jcminarro.authexample.internal.repository.SessionRepository +import com.nhaarman.mockito_kotlin.doReturn +import org.amshove.kluent.Verify +import org.amshove.kluent.When +import org.amshove.kluent.`Verify no further interactions` +import org.amshove.kluent.called +import org.amshove.kluent.calling +import org.amshove.kluent.mock +import org.amshove.kluent.on +import org.amshove.kluent.that +import org.amshove.kluent.was +import org.junit.Before +import org.junit.Test + +class LoginFPInteractorTest { + + private val VALID_USERNAME = "Jc Miñarro" + private val VALID_PASSWORD = "password" + private val INVALID_USERNAME = "username" + private val INVALID_PASSWORD = "1234" + private val sessionRepository: SessionRepository = mock() + private val loginFPInteractor = LoginFPInteractor(sessionRepository) + private val apiIoException = APIIOException(mock()) + private val onError: (CredentialError) -> Unit = mock() + private val onSuccess: (Boolean) -> Unit = mock() + + @Before + fun setUp() { + When calling sessionRepository.loginFP(VALID_USERNAME, VALID_PASSWORD) doReturn Success(true) + When calling sessionRepository.loginFP(INVALID_USERNAME, VALID_PASSWORD) doReturn Failure(apiIoException) + When calling sessionRepository.loginFP(VALID_USERNAME, INVALID_PASSWORD) doReturn Failure(apiIoException) + } + + @Test + fun `Should login successful with valid credential`() { + loginFPInteractor.execute(Input(VALID_USERNAME, VALID_PASSWORD), onError, onSuccess) + + Verify on sessionRepository that sessionRepository.loginFP(VALID_USERNAME, VALID_PASSWORD) was called + Verify on onSuccess that onSuccess(true) was called + `Verify no further interactions` on sessionRepository + `Verify no further interactions` on onError + } + + @Test + fun `Should notify an error when try to login with an invalid password`() { + loginFPInteractor.execute(Input(VALID_USERNAME, INVALID_PASSWORD), onError, onSuccess) + + Verify on sessionRepository that sessionRepository.loginFP(VALID_USERNAME, INVALID_PASSWORD) was called + Verify on onError that onError(CredentialError) was called + `Verify no further interactions` on sessionRepository + `Verify no further interactions` on onSuccess + } + + @Test + fun `Should notify an error when try to login with an invalid username`() { + loginFPInteractor.execute(Input(INVALID_USERNAME, VALID_PASSWORD), onError, onSuccess) + + Verify on sessionRepository that sessionRepository.loginFP(INVALID_USERNAME, VALID_PASSWORD) was called + Verify on onError that onError(CredentialError) was called + `Verify no further interactions` on sessionRepository + `Verify no further interactions` on onSuccess + } +} \ No newline at end of file diff --git a/app/src/test/java/com/jcminarro/authexample/login/LoginPresenterTest.kt b/app/src/test/java/com/jcminarro/authexample/login/LoginPresenterTest.kt new file mode 100644 index 0000000..4f22aa1 --- /dev/null +++ b/app/src/test/java/com/jcminarro/authexample/login/LoginPresenterTest.kt @@ -0,0 +1,78 @@ +package com.jcminarro.authexample.login + +import arrow.core.Failure +import arrow.core.Success +import com.jcminarro.authexample.internal.navigator.Navigator +import com.jcminarro.authexample.internal.network.APIIOException +import com.jcminarro.authexample.internal.repository.SessionRepository +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.spy +import org.amshove.kluent.Verify +import org.amshove.kluent.When +import org.amshove.kluent.`Verify no further interactions` +import org.amshove.kluent.called +import org.amshove.kluent.calling +import org.amshove.kluent.mock +import org.amshove.kluent.on +import org.amshove.kluent.that +import org.amshove.kluent.was +import org.junit.Before +import org.junit.Test + +class LoginPresenterTest { + + private val VALID_USERNAME = "Jc Miñarro" + private val VALID_PASSWORD = "password" + private val INVALID_USERNAME = "username" + private val INVALID_PASSWORD = "1234" + private val apiIoException = APIIOException(mock()) + private val loginInteractor: LoginInteractor = mock() + private val sessionRepository: SessionRepository = mock() + private val loginFPInteractor = LoginFPInteractor(sessionRepository) + private val navigator: Navigator = mock() + private val view: LoginPresenter.View = spy() + private val loginPresenter = LoginPresenter(loginInteractor, loginFPInteractor, navigator) + + @Before + fun setUp() { + loginPresenter.view = view + When calling sessionRepository.loginFP(VALID_USERNAME, VALID_PASSWORD) doReturn Success(true) + When calling sessionRepository.loginFP(INVALID_USERNAME, VALID_PASSWORD) doReturn Failure(apiIoException) + When calling sessionRepository.loginFP(VALID_USERNAME, INVALID_PASSWORD) doReturn Failure(apiIoException) + } + + @Test + fun `Should login successful with valid credential`() { + loginPresenter.loginFP(VALID_USERNAME, VALID_PASSWORD) + + Verify on view that view.showLoading() was called + Verify on view that view.hideLogin() was called + Verify on view that view.close() was called + `Verify no further interactions` on view + Verify on navigator that navigator.navigateToQuote() was called + } + + @Test + fun `Should notify an error when try to login with an invalid password`() { + loginPresenter.loginFP(VALID_USERNAME, INVALID_PASSWORD) + + Verify on view that view.showLoading() was called + Verify on view that view.hideLogin() was called + Verify on view that view.showLogin() was called + Verify on view that view.hideLoading() was called + Verify on view that view.showError() was called + `Verify no further interactions` on view + } + + @Test + fun `Should notify an error when try to login with an invalid username`() { + loginPresenter.loginFP(INVALID_USERNAME, VALID_PASSWORD) + + Verify on view that view.showLoading() was called + Verify on view that view.hideLogin() was called + Verify on view that view.showLogin() was called + Verify on view that view.hideLoading() was called + Verify on view that view.showError() was called + `Verify no further interactions` on view + } +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/com/jcminarro/Dependencies.groovy b/buildSrc/src/main/groovy/com/jcminarro/Dependencies.groovy index bf07152..cca43a8 100644 --- a/buildSrc/src/main/groovy/com/jcminarro/Dependencies.groovy +++ b/buildSrc/src/main/groovy/com/jcminarro/Dependencies.groovy @@ -3,6 +3,7 @@ package com.jcminarro; class Dependencies { private static String KOTLIN_VERSION = '1.1.51' + private static String KOTLINX_VERSION = '0.22.5' private static String ANDROID_BUILD_TOOL_VERSION = '3.0.0' private static String APP_COMPAT_VERSION = '26.1.0' private static String JUNIT_VERSION = '4.12' @@ -19,6 +20,7 @@ class Dependencies { private static String WIREMOCK_VERSION = '2.8.0' private static String MOCKWEBSERVER_VERSION = OK_HTTP_VERSION private static String ANDROID_SUPPORT_VERSION = '26.1.0' + private static String ARROW_VERSION = '0.7.2' static String kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION" static String androidBuildToolGradlePlugin = "com.android.tools.build:gradle:$ANDROID_BUILD_TOOL_VERSION" @@ -42,4 +44,10 @@ class Dependencies { static String wiremock = "com.github.tomakehurst:wiremock:$WIREMOCK_VERSION" static String mockwebserver = "com.squareup.okhttp3:mockwebserver:$MOCKWEBSERVER_VERSION" static String supportDesign = "com.android.support:design:$ANDROID_SUPPORT_VERSION" + static String arrowCore = "io.arrow-kt:arrow-core:$ARROW_VERSION" + static String arrowTypeclasses = "io.arrow-kt:arrow-typeclasses:$ARROW_VERSION" + static String arrowEffects = "io.arrow-kt:arrow-effects:$ARROW_VERSION" + static String arrowEffectsKotlinxCoroutines = "io.arrow-kt:arrow-effects-kotlinx-coroutines:$ARROW_VERSION" + static String kotlinxCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$KOTLINX_VERSION" + static String kotlinxCoroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$KOTLINX_VERSION" } \ No newline at end of file