diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f57b6c5ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,234 @@ + +# Created by https://www.gitignore.io/api/java,kotlin,intellij,androidstudio + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle +.gradle/ +build/ + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +captures/ +.navigation/ +*.ipr +*~ +*.swp + +# Android Patch +gen-external-apklibs + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# NDK +obj/ + +# IntelliJ IDEA +*.iml +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client +.idea/httpRequests + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Java ### +# Compiled class file + +# Log file + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) + +# Package Files # +*.jar +*.nar +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml + +### Kotlin ### +# Compiled class file + +# Log file + +# BlueJ files + +# Mobile Tools for Java (J2ME) + +# Package Files # + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml + + +# End of https://www.gitignore.io/api/java,kotlin,intellij,androidstudio diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..26d33521a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/README.md b/README.md index bd73feb5f..2784cbfef 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,14 @@ +#Teste Accenture + +- Para a execução será necessário clonar o projeto e executar na IDE. + +* O projeto foi separado em módulos para um melhor reaproveitamento de códigos. +* A arquitetura utilizada foi MVVM com Clean Architecture por facilitar a separação das camadas reduzindo o acoplamento e facilitando os testes. +* Utilização do framework RxJava para facilitar o trabalho com fluxos de dados, utilização de threads secundarias e tratamento de erros. +* Utilização da framework Koin por facilitar o trabalho com injeção de dependencia. +* Utilização do Junit e Mockito por facilitar os mocks e asserções dos testes unitários. + + # Show me the code Esse repositório contem todo o material necessário para realizar o teste: diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..c37e4e2d9 --- /dev/null +++ b/build.gradle @@ -0,0 +1,78 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext.kotlin_version = "1.4.31" + ext.koin_version = '2.2.2' + repositories { + mavenCentral() + google() + jcenter() + } + dependencies { + classpath "com.android.tools.build:gradle:4.1.2" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.koin:koin-gradle-plugin:$koin_version" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenCentral() + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +ext { + + compiler = [ + java : JavaVersion.VERSION_1_8, + kotlin: '1.3.61' + ] + + configuration = [ + minSdkVersion : 19, + targetSdkVersion : 30, + compileSdkVersion: 30, + buildToolsVersion: '30.0.2', + applicationId : "br.com.silas.testeandroidv2", + versionCode : 1, + versionName : "1.0", + testInstrumentationRunner: "androidx.test.runner.AndroidJUnitRunner" + + ] + + libraries = [ + appcompat: '1.2.0', + constraintlayout: '2.0.4', + koin_version: '2.0.0-GA', + ktxVersion: '1.3.2', + multidex: '1.0.3', + material: '1.3.0', + rxjavaVersion: '3.0.0', + rxKotlinVersion: '3.0.0', + rxAndroidVersion: '3.0.0', + retrofit: '2.6.2', + gson: '2.6.2', + retrofitAdapter: '2.9.0', + okHttpVersion: '3.11.0' + + ] + + testingLibraries = [ + junitVersion: '4.13.2', + espressoVersion: '3.3.0', + extJunit: '1.1.2', + mockitoVersion: '3.0.0', + mockitoInline: '2.28.2', + runnerVersion: '1.1.0', + versionTestArchCore: '1.1.1' + ] + +} \ No newline at end of file diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/build.gradle b/data/build.gradle new file mode 100644 index 000000000..0f8a02202 --- /dev/null +++ b/data/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +def cfg = rootProject.ext.configuration +def libs = rootProject.ext.libraries +def test = rootProject.ext.testingLibraries + +android { + compileSdkVersion cfg.compileSdkVersion + buildToolsVersion cfg.buildToolsVersion + + defaultConfig { + minSdkVersion cfg.minSdkVersion + targetSdkVersion cfg.targetSdkVersion + versionCode cfg.versionCode + versionName cfg.versionName + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + +// RxJava + implementation "io.reactivex.rxjava3:rxjava:$libs.rxjavaVersion" + implementation "io.reactivex.rxjava3:rxkotlin:$libs.rxKotlinVersion" + implementation "io.reactivex.rxjava3:rxandroid:$libs.rxAndroidVersion" + +// Tests + testImplementation "junit:junit:$test.junitVersion" + androidTestImplementation "androidx.test.ext:junit:$test.extJunit" + androidTestImplementation "androidx.test.espresso:espresso-core:$test.espressoVersion" + testImplementation "junit:junit:$test.junitVersion" + implementation "org.mockito:mockito-core:$test.mockitoVersion" + testImplementation "org.mockito:mockito-inline:$test.mockitoInline" + +// Retrofit + implementation "com.squareup.retrofit2:retrofit:$libs.retrofit" + implementation "com.squareup.retrofit2:converter-gson:$libs.gson" + implementation "com.squareup.retrofit2:adapter-rxjava3:$libs.retrofitAdapter" + implementation "com.squareup.okhttp3:logging-interceptor:$libs.okHttpVersion" + + + implementation project(':domain') +} \ No newline at end of file diff --git a/data/consumer-rules.pro b/data/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/data/proguard-rules.pro b/data/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/data/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/data/src/androidTest/java/br/com/silas/data/ExampleInstrumentedTest.kt b/data/src/androidTest/java/br/com/silas/data/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..a729f6a7f --- /dev/null +++ b/data/src/androidTest/java/br/com/silas/data/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package br.com.silas.data + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("br.com.silas.data.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml new file mode 100644 index 000000000..d3c3de59a --- /dev/null +++ b/data/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/data/src/main/java/br/com/silas/data/local/PreferencesRepositoryImpl.kt b/data/src/main/java/br/com/silas/data/local/PreferencesRepositoryImpl.kt new file mode 100644 index 000000000..76ee39fc1 --- /dev/null +++ b/data/src/main/java/br/com/silas/data/local/PreferencesRepositoryImpl.kt @@ -0,0 +1,45 @@ +package br.com.silas.data.local + +import android.content.SharedPreferences +import br.com.silas.domain.preferences.PreferencesRepository +import br.com.silas.domain.user.User +import io.reactivex.rxjava3.core.Completable + +class PreferencesRepositoryImpl(private val sharedPreferences: SharedPreferences) : + PreferencesRepository { + companion object { + const val USER_ID = "user_id" + const val USER_NAME = "name" + const val BANCK_ACCOUNT = "bank_account" + const val AGENCY = "agency" + const val BALANCE = "balance" + } + + override fun save(user: User?): Completable { + + return Completable.create { emitter -> + if (user?.id != null) { + val preferencesEditor = sharedPreferences.edit() + preferencesEditor.putInt(USER_ID, user.id) + preferencesEditor.putString(USER_NAME, user.name) + preferencesEditor.putString(BANCK_ACCOUNT, user.bankAccount) + preferencesEditor.putString(AGENCY, user.agency) + preferencesEditor.putFloat(BALANCE, user.balance.toFloat()) + + preferencesEditor.apply() + emitter.onComplete() + } + + emitter.onComplete() + } + } + + override fun getUser() = + User( + sharedPreferences.getInt(USER_ID, 0), + sharedPreferences.getString(USER_NAME, ""), + sharedPreferences.getString(BANCK_ACCOUNT, ""), + sharedPreferences.getString(AGENCY, ""), + sharedPreferences.getFloat(BALANCE, 2.2.toFloat()).toDouble() + ) +} diff --git a/data/src/main/java/br/com/silas/data/remote/ErrorResponse.kt b/data/src/main/java/br/com/silas/data/remote/ErrorResponse.kt new file mode 100644 index 000000000..4d24fbf71 --- /dev/null +++ b/data/src/main/java/br/com/silas/data/remote/ErrorResponse.kt @@ -0,0 +1,3 @@ +package br.com.silas.data.remote + +data class ErrorResponse(val code: Int, val message: String?) \ No newline at end of file diff --git a/data/src/main/java/br/com/silas/data/remote/api/ServiceTesteAndroidV2.kt b/data/src/main/java/br/com/silas/data/remote/api/ServiceTesteAndroidV2.kt new file mode 100644 index 000000000..8adbb2b72 --- /dev/null +++ b/data/src/main/java/br/com/silas/data/remote/api/ServiceTesteAndroidV2.kt @@ -0,0 +1,23 @@ +package br.com.silas.data.remote.api + +import br.com.silas.data.remote.login.LoginResponse +import br.com.silas.data.remote.statements.StatementsResponse +import io.reactivex.rxjava3.core.Maybe +import io.reactivex.rxjava3.core.Single +import retrofit2.http.* + + +interface ServiceTesteAndroidV2 { + + @FormUrlEncoded + @POST("login") + fun fetchUser( + @Field("user") login: String, + @Field("password") password: String + ): Single + + @GET("statements/{userId}") + fun fetchStatements( + @Path("userId") userId: Int, + ): Single +} \ No newline at end of file diff --git a/data/src/main/java/br/com/silas/data/remote/api/TesteAndroidv2ServiceImpl.kt b/data/src/main/java/br/com/silas/data/remote/api/TesteAndroidv2ServiceImpl.kt new file mode 100644 index 000000000..fba084e57 --- /dev/null +++ b/data/src/main/java/br/com/silas/data/remote/api/TesteAndroidv2ServiceImpl.kt @@ -0,0 +1,63 @@ +package br.com.silas.data.remote.api + +import br.com.silas.data.BuildConfig +import br.com.silas.data.remote.login.LoginResponse +import br.com.silas.data.remote.statements.StatementsResponse +import io.reactivex.rxjava3.core.Maybe +import io.reactivex.rxjava3.core.Single +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import okhttp3.logging.HttpLoggingInterceptor.Level.BASIC +import okhttp3.logging.HttpLoggingInterceptor.Level.BODY +import retrofit2.Retrofit +import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +class TesteAndroidv2ServiceImpl : ServiceTesteAndroidV2 { + companion object { + private const val BASE_URL = "https://bank-app-test.herokuapp.com/api/" + private const val TIMEOUT = 10L + } + private var testeAndroidV2Service: ServiceTesteAndroidV2 + + init { + + val loggingInterceptor = HttpLoggingInterceptor() + loggingInterceptor.level = if (BuildConfig.DEBUG) BODY else BASIC + + val httpClient = OkHttpClient.Builder() + .connectTimeout(TIMEOUT, TimeUnit.SECONDS) + .readTimeout(TIMEOUT, TimeUnit.SECONDS) + .addInterceptor(loggingInterceptor) + + httpClient.addInterceptor { chain -> + val requestOrigin = chain.request() + val request = requestOrigin.newBuilder() + .header("Content-Type", "application/x-www-form-urlencoded") + .method(requestOrigin.method(), requestOrigin.body()) + .build() + chain.proceed(request) + } + + val client = httpClient.build() + + val retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava3CallAdapterFactory.createSynchronous()) + .client(client) + .build() + + testeAndroidV2Service = retrofit.create(ServiceTesteAndroidV2::class.java) + + } + + override fun fetchUser(login: String, password: String): Single { + return testeAndroidV2Service.fetchUser(login, password) + } + + override fun fetchStatements(userId: Int): Single { + return testeAndroidV2Service.fetchStatements(userId) + } +} \ No newline at end of file diff --git a/data/src/main/java/br/com/silas/data/remote/login/LoginRepositoryImpl.kt b/data/src/main/java/br/com/silas/data/remote/login/LoginRepositoryImpl.kt new file mode 100644 index 000000000..382a0dfa8 --- /dev/null +++ b/data/src/main/java/br/com/silas/data/remote/login/LoginRepositoryImpl.kt @@ -0,0 +1,18 @@ +package br.com.silas.data.remote.login + +import br.com.silas.data.remote.api.ServiceTesteAndroidV2 +import br.com.silas.domain.preferences.PreferencesRepository +import br.com.silas.domain.ErrorResponse +import br.com.silas.domain.user.LoginRepository +import br.com.silas.domain.user.User +import io.reactivex.rxjava3.core.Single + +class LoginRepositoryImpl( + private val testeAndroidV2Service: ServiceTesteAndroidV2, + private val userMapper: UserMapper, +) : LoginRepository { + override fun fetchUser(login: String, password: String): Single> { + return testeAndroidV2Service.fetchUser(login, password) + .map { userMapper.mapperUserAccountResponseToUser(it) } + } +} \ No newline at end of file diff --git a/data/src/main/java/br/com/silas/data/remote/login/LoginResponse.kt b/data/src/main/java/br/com/silas/data/remote/login/LoginResponse.kt new file mode 100644 index 000000000..1f2d3f0a8 --- /dev/null +++ b/data/src/main/java/br/com/silas/data/remote/login/LoginResponse.kt @@ -0,0 +1,9 @@ +package br.com.silas.data.remote.login + +import br.com.silas.data.remote.ErrorResponse +import com.google.gson.annotations.SerializedName + +class LoginResponse( + @SerializedName("userAccount") val userEntity: UserEntity, + @SerializedName("error") val errorResponse: ErrorResponse +) diff --git a/data/src/main/java/br/com/silas/data/remote/login/UserEntity.kt b/data/src/main/java/br/com/silas/data/remote/login/UserEntity.kt new file mode 100644 index 000000000..da3ccdb2b --- /dev/null +++ b/data/src/main/java/br/com/silas/data/remote/login/UserEntity.kt @@ -0,0 +1,9 @@ +package br.com.silas.data.remote.login + +class UserEntity( + val id: Int, + val name: String, + val bankAccount: String, + val agency: String, + val balance: Double +) diff --git a/data/src/main/java/br/com/silas/data/remote/login/UserMapper.kt b/data/src/main/java/br/com/silas/data/remote/login/UserMapper.kt new file mode 100644 index 000000000..5b9562343 --- /dev/null +++ b/data/src/main/java/br/com/silas/data/remote/login/UserMapper.kt @@ -0,0 +1,21 @@ +package br.com.silas.data.remote.login + +import br.com.silas.domain.ErrorResponse +import br.com.silas.domain.user.User + +class UserMapper { + fun mapperUserAccountResponseToUser(loginResponse: LoginResponse): Pair { + + val user = User( + loginResponse.userEntity.id, + loginResponse.userEntity.name, + loginResponse.userEntity.bankAccount, + loginResponse.userEntity.agency, + loginResponse.userEntity.balance + ) + + val loginError = + ErrorResponse(loginResponse.errorResponse.code, loginResponse.errorResponse.message) + return Pair(user, loginError) + } +} \ No newline at end of file diff --git a/data/src/main/java/br/com/silas/data/remote/statements/StatementMapper.kt b/data/src/main/java/br/com/silas/data/remote/statements/StatementMapper.kt new file mode 100644 index 000000000..e90fe2f83 --- /dev/null +++ b/data/src/main/java/br/com/silas/data/remote/statements/StatementMapper.kt @@ -0,0 +1,28 @@ +package br.com.silas.data.remote.statements + +import br.com.silas.domain.ErrorResponse +import br.com.silas.domain.statements.Statements + +class StatementMapper { + fun mapperStatementsEntityToStatements(statementsResponse: StatementsResponse): Pair> { + + val statementsList = mutableListOf() + statementsResponse.statements.forEach { statementsEntity -> + statementsList.add( + Statements( + statementsEntity.title, + statementsEntity.desc, + statementsEntity.date, + statementsEntity.value + ) + ) + } + + val error = ErrorResponse( + statementsResponse.errorResponse.code, + statementsResponse.errorResponse.message + ) + + return Pair(error, statementsList) + } +} \ No newline at end of file diff --git a/data/src/main/java/br/com/silas/data/remote/statements/StatementsEntity.kt b/data/src/main/java/br/com/silas/data/remote/statements/StatementsEntity.kt new file mode 100644 index 000000000..f457fcfc0 --- /dev/null +++ b/data/src/main/java/br/com/silas/data/remote/statements/StatementsEntity.kt @@ -0,0 +1,8 @@ +package br.com.silas.data.remote.statements + +class StatementsEntity( + val title: String, + val desc: String, + val date: String, + val value: Double +) \ No newline at end of file diff --git a/data/src/main/java/br/com/silas/data/remote/statements/StatementsRepositoryImpl.kt b/data/src/main/java/br/com/silas/data/remote/statements/StatementsRepositoryImpl.kt new file mode 100644 index 000000000..4a444cb69 --- /dev/null +++ b/data/src/main/java/br/com/silas/data/remote/statements/StatementsRepositoryImpl.kt @@ -0,0 +1,19 @@ +package br.com.silas.data.remote.statements + +import br.com.silas.data.remote.api.ServiceTesteAndroidV2 +import br.com.silas.domain.ErrorResponse +import br.com.silas.domain.statements.Statements +import br.com.silas.domain.statements.StatementsRepository +import io.reactivex.rxjava3.core.Maybe +import io.reactivex.rxjava3.core.Single + +class StatementsRepositoryImpl( + private val serviceTesteAndroidv2: ServiceTesteAndroidV2, + private val statementMapper: StatementMapper +) : StatementsRepository { + override fun fetchStatements(userId: Int): Single>> { + return serviceTesteAndroidv2.fetchStatements(userId) + .map { statementMapper.mapperStatementsEntityToStatements(it) } + + } +} \ No newline at end of file diff --git a/data/src/main/java/br/com/silas/data/remote/statements/StatementsResponse.kt b/data/src/main/java/br/com/silas/data/remote/statements/StatementsResponse.kt new file mode 100644 index 000000000..ecfe8c1b6 --- /dev/null +++ b/data/src/main/java/br/com/silas/data/remote/statements/StatementsResponse.kt @@ -0,0 +1,9 @@ +package br.com.silas.data.remote.statements + +import br.com.silas.data.remote.ErrorResponse +import com.google.gson.annotations.SerializedName + +class StatementsResponse( + @SerializedName("error") val errorResponse: ErrorResponse, + @SerializedName("statementList" ) val statements: List +) diff --git a/data/src/test/java/br/com/silas/data/repositories/LoginRepositoryImplTest.kt b/data/src/test/java/br/com/silas/data/repositories/LoginRepositoryImplTest.kt new file mode 100644 index 000000000..c374857ac --- /dev/null +++ b/data/src/test/java/br/com/silas/data/repositories/LoginRepositoryImplTest.kt @@ -0,0 +1,79 @@ +package br.com.silas.data.repositories + +import br.com.silas.data.remote.api.ServiceTesteAndroidV2 +import br.com.silas.data.remote.login.LoginRepositoryImpl +import br.com.silas.data.remote.login.LoginResponse +import br.com.silas.data.remote.login.UserMapper +import br.com.silas.domain.ErrorResponse +import br.com.silas.domain.user.User +import io.reactivex.rxjava3.core.Single +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class LoginRepositoryImplTest { + @Mock + lateinit var testeAndroidV2Service: ServiceTesteAndroidV2 + + @Mock + lateinit var userMapper: UserMapper + + lateinit var loginRepositoryImpl: LoginRepositoryImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + loginRepositoryImpl = + LoginRepositoryImpl(testeAndroidV2Service, userMapper) + } + + @Test + fun `Should return an user from api when login is successful`() { + val loginResponse = mock(LoginResponse::class.java) + val user = mock(User::class.java) + val errorResponse = mock(ErrorResponse::class.java) + + val pairResponse = Pair(user, errorResponse) + + + `when`(testeAndroidV2Service.fetchUser(anyString(), anyString())).thenReturn( + Single.just( + loginResponse + ) + ) + + `when`(userMapper.mapperUserAccountResponseToUser(loginResponse)).thenReturn(pairResponse) + + val result = loginRepositoryImpl.fetchUser("test_user", "Test@1").test() + + result + .assertComplete() + .assertNoErrors() + .assertValue(pairResponse) + + verify(testeAndroidV2Service).fetchUser("test_user", "Test@1") + } + + @Test + fun `Should return an exception from api when login is unsuccessful`() { + val exception = Exception() + `when`(testeAndroidV2Service.fetchUser(anyString(), anyString())).thenReturn( + Single.error( + exception + ) + ) + val result = loginRepositoryImpl.fetchUser("test_user", "Test@1").test() + + result + .assertNotComplete() + .assertNoValues() + .assertError(exception) + + verify(testeAndroidV2Service).fetchUser("test_user", "Test@1") + } +} \ No newline at end of file diff --git a/data/src/test/java/br/com/silas/data/repositories/StatementsRepositoryImplTest.kt b/data/src/test/java/br/com/silas/data/repositories/StatementsRepositoryImplTest.kt new file mode 100644 index 000000000..8b5e5ebd3 --- /dev/null +++ b/data/src/test/java/br/com/silas/data/repositories/StatementsRepositoryImplTest.kt @@ -0,0 +1,86 @@ +package br.com.silas.data.repositories + +import br.com.silas.data.remote.api.ServiceTesteAndroidV2 +import br.com.silas.data.remote.statements.StatementMapper +import br.com.silas.data.remote.statements.StatementsRepositoryImpl +import br.com.silas.data.remote.statements.StatementsResponse +import br.com.silas.domain.ErrorResponse +import br.com.silas.domain.statements.Statements +import io.reactivex.rxjava3.core.Single +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class StatementsRepositoryImplTest { + @Mock + lateinit var serviceTesteAndroidV2: ServiceTesteAndroidV2 + + @Mock + lateinit var statementMapper: StatementMapper + + lateinit var statementsRepositoryImpl: StatementsRepositoryImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + statementsRepositoryImpl = StatementsRepositoryImpl(serviceTesteAndroidV2, statementMapper) + } + + @Test + @Throws(Exception::class) + fun `Should return a list of statements from api when fetch is successful`() { + val statementResponse = mock(StatementsResponse::class.java) + + val errorResponse = mock(ErrorResponse::class.java) + val statements = mock(Statements::class.java) + val list = listOf(statements) + + val pairResponse = Pair(errorResponse, list) + + + `when`(serviceTesteAndroidV2.fetchStatements(anyInt())).thenReturn( + Single.just( + statementResponse + ) + ) + `when`(statementMapper.mapperStatementsEntityToStatements(statementResponse)).thenReturn( + pairResponse + ) + + + val result = statementsRepositoryImpl.fetchStatements(152).test() + + result + .assertNoErrors() + .assertValue(pairResponse) + .assertComplete() + + verify(serviceTesteAndroidV2).fetchStatements(152) + } + + @Test + @Throws(Exception::class) + fun `Should return a list of statements from api when fetch is unsuccessful`() { + val exception = Exception() + + `when`(serviceTesteAndroidV2.fetchStatements(anyInt())).thenReturn( + Single.error( + exception + ) + ) + + val result = statementsRepositoryImpl.fetchStatements(152).test() + + result + .assertNoValues() + .assertError(exception) + .assertNotComplete() + + verify(serviceTesteAndroidV2).fetchStatements(152) + } +} \ No newline at end of file diff --git a/domain/.gitignore b/domain/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/build.gradle b/domain/build.gradle new file mode 100644 index 000000000..bf2025a84 --- /dev/null +++ b/domain/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java-library' + id 'kotlin' +} + +def libs = rootProject.ext.libraries +def test = rootProject.ext.testingLibraries + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + +// RxJava + implementation "io.reactivex.rxjava3:rxjava:$libs.rxjavaVersion" + implementation "io.reactivex.rxjava3:rxkotlin:$libs.rxKotlinVersion" + implementation "io.reactivex.rxjava3:rxandroid:$libs.rxAndroidVersion" + + //Test + testImplementation "junit:junit:$test.junitVersion" + implementation "org.mockito:mockito-core:$test.mockitoVersion" + testImplementation "org.mockito:mockito-inline:$test.mockitoInline" + +} \ No newline at end of file diff --git a/domain/src/main/java/br/com/silas/domain/ErrorResponse.kt b/domain/src/main/java/br/com/silas/domain/ErrorResponse.kt new file mode 100644 index 000000000..8caf9279e --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/ErrorResponse.kt @@ -0,0 +1,3 @@ +package br.com.silas.domain + +class ErrorResponse(val code: Int, val message: String?) \ No newline at end of file diff --git a/domain/src/main/java/br/com/silas/domain/InteractorCompletable.kt b/domain/src/main/java/br/com/silas/domain/InteractorCompletable.kt new file mode 100644 index 000000000..502529862 --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/InteractorCompletable.kt @@ -0,0 +1,15 @@ +package br.com.silas.domain + +import io.reactivex.rxjava3.core.Completable + +abstract class InteractorCompletable internal constructor(private val schedulers: Schedulers) { + protected abstract fun create(request: R): Completable + + fun execute(request: R): Completable { + return create(request) + .subscribeOn(schedulers.subscribeOn) + .observeOn(schedulers.observeOn) + } + + abstract class Request +} \ No newline at end of file diff --git a/domain/src/main/java/br/com/silas/domain/InteractorSingle.kt b/domain/src/main/java/br/com/silas/domain/InteractorSingle.kt new file mode 100644 index 000000000..c2d276ad8 --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/InteractorSingle.kt @@ -0,0 +1,15 @@ +package br.com.silas.domain + +import io.reactivex.rxjava3.core.Single + +abstract class InteractorSingle internal constructor(private val schedulers: Schedulers) { + protected abstract fun create(request: R): Single + + fun execute(request: R): Single { + return create(request) + .subscribeOn(schedulers.subscribeOn) + .observeOn(schedulers.observeOn) + } + + abstract class Request +} \ No newline at end of file diff --git a/domain/src/main/java/br/com/silas/domain/Schedulers.kt b/domain/src/main/java/br/com/silas/domain/Schedulers.kt new file mode 100644 index 000000000..7b9f8275c --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/Schedulers.kt @@ -0,0 +1,9 @@ +package br.com.silas.domain + +import io.reactivex.rxjava3.core.Scheduler + + +interface Schedulers { + val subscribeOn: Scheduler + val observeOn: Scheduler +} diff --git a/domain/src/main/java/br/com/silas/domain/preferences/PreferencesRepository.kt b/domain/src/main/java/br/com/silas/domain/preferences/PreferencesRepository.kt new file mode 100644 index 000000000..df6477d59 --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/preferences/PreferencesRepository.kt @@ -0,0 +1,9 @@ +package br.com.silas.domain.preferences + +import br.com.silas.domain.user.User +import io.reactivex.rxjava3.core.Completable + +interface PreferencesRepository { + fun save(user: User?) : Completable + fun getUser() : User +} \ No newline at end of file diff --git a/domain/src/main/java/br/com/silas/domain/statements/Statements.kt b/domain/src/main/java/br/com/silas/domain/statements/Statements.kt new file mode 100644 index 000000000..35dca2f4b --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/statements/Statements.kt @@ -0,0 +1,8 @@ +package br.com.silas.domain.statements + +data class Statements( + val title: String, + val desc: String, + val date: String, + val value: Double +) \ No newline at end of file diff --git a/domain/src/main/java/br/com/silas/domain/statements/StatementsInteractor.kt b/domain/src/main/java/br/com/silas/domain/statements/StatementsInteractor.kt new file mode 100644 index 000000000..2d0bdb8ab --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/statements/StatementsInteractor.kt @@ -0,0 +1,21 @@ +package br.com.silas.domain.statements + +import br.com.silas.domain.ErrorResponse +import br.com.silas.domain.InteractorCompletable +import br.com.silas.domain.InteractorSingle +import br.com.silas.domain.Schedulers +import io.reactivex.rxjava3.core.Single + +class StatementsInteractor + (private val statementsRepository: StatementsRepository, schedulers: Schedulers) : + InteractorSingle>, StatementsInteractor.Request>(schedulers) { + + + override fun create(request: Request): Single>> { + return statementsRepository.fetchStatements(request.getUserId()) + } + + inner class Request(private val userId: Int) : InteractorCompletable.Request() { + fun getUserId() = userId + } +} \ No newline at end of file diff --git a/domain/src/main/java/br/com/silas/domain/statements/StatementsRepository.kt b/domain/src/main/java/br/com/silas/domain/statements/StatementsRepository.kt new file mode 100644 index 000000000..3fa70a782 --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/statements/StatementsRepository.kt @@ -0,0 +1,9 @@ +package br.com.silas.domain.statements + +import br.com.silas.domain.ErrorResponse +import io.reactivex.rxjava3.core.Maybe +import io.reactivex.rxjava3.core.Single + +interface StatementsRepository { + fun fetchStatements(userId: Int): Single>> +} \ No newline at end of file diff --git a/domain/src/main/java/br/com/silas/domain/user/GetUserInteractor.kt b/domain/src/main/java/br/com/silas/domain/user/GetUserInteractor.kt new file mode 100644 index 000000000..7da477a2e --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/user/GetUserInteractor.kt @@ -0,0 +1,7 @@ +package br.com.silas.domain.user + +import br.com.silas.domain.preferences.PreferencesRepository + +class GetUserInteractor(private val preferencesRepository: PreferencesRepository) { + fun execute() = preferencesRepository.getUser() +} \ No newline at end of file diff --git a/domain/src/main/java/br/com/silas/domain/user/LoginInteractor.kt b/domain/src/main/java/br/com/silas/domain/user/LoginInteractor.kt new file mode 100644 index 000000000..e214c5053 --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/user/LoginInteractor.kt @@ -0,0 +1,21 @@ +package br.com.silas.domain.user + +import br.com.silas.domain.ErrorResponse +import br.com.silas.domain.InteractorSingle +import br.com.silas.domain.Schedulers +import io.reactivex.rxjava3.core.Single + +class LoginInteractor(private val loginRepository: LoginRepository, schedulers: Schedulers) : + InteractorSingle, LoginInteractor.Request>(schedulers) { + + + override fun create(request: Request): Single> { + return loginRepository.fetchUser(request.getLogin(), request.getPassword()) + } + + inner class Request(private val login: String, private val password: String) : + InteractorSingle.Request() { + fun getLogin() = login + fun getPassword() = password + } +} \ No newline at end of file diff --git a/domain/src/main/java/br/com/silas/domain/user/LoginRepository.kt b/domain/src/main/java/br/com/silas/domain/user/LoginRepository.kt new file mode 100644 index 000000000..5f92f5c3d --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/user/LoginRepository.kt @@ -0,0 +1,8 @@ +package br.com.silas.domain.user + +import br.com.silas.domain.ErrorResponse +import io.reactivex.rxjava3.core.Single + +interface LoginRepository { + fun fetchUser(login: String, password: String) : Single> +} \ No newline at end of file diff --git a/domain/src/main/java/br/com/silas/domain/user/SaveUserInteractor.kt b/domain/src/main/java/br/com/silas/domain/user/SaveUserInteractor.kt new file mode 100644 index 000000000..9723852f3 --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/user/SaveUserInteractor.kt @@ -0,0 +1,18 @@ +package br.com.silas.domain.user + +import br.com.silas.domain.InteractorCompletable +import br.com.silas.domain.Schedulers +import br.com.silas.domain.preferences.PreferencesRepository +import io.reactivex.rxjava3.core.Completable + +class SaveUserInteractor(private val preferencesRepository: PreferencesRepository, schedulers: Schedulers) : + InteractorCompletable(schedulers) { + override fun create(request: Request): Completable { + return preferencesRepository.save(request.getUser()) + } + + inner class Request(private val user: User) : InteractorCompletable.Request() { + fun getUser() = user + } + +} \ No newline at end of file diff --git a/domain/src/main/java/br/com/silas/domain/user/User.kt b/domain/src/main/java/br/com/silas/domain/user/User.kt new file mode 100644 index 000000000..704570fe8 --- /dev/null +++ b/domain/src/main/java/br/com/silas/domain/user/User.kt @@ -0,0 +1,11 @@ +package br.com.silas.domain.user + +import java.io.Serializable + +data class User ( + val id: Int, + val name: String?, + val bankAccount: String?, + val agency: String?, + val balance: Double +) : Serializable \ No newline at end of file diff --git a/domain/src/test/java/br/com/silas/domain/interactor/GetUserInteractorTest.kt b/domain/src/test/java/br/com/silas/domain/interactor/GetUserInteractorTest.kt new file mode 100644 index 000000000..8c2a39bf6 --- /dev/null +++ b/domain/src/test/java/br/com/silas/domain/interactor/GetUserInteractorTest.kt @@ -0,0 +1,35 @@ +package br.com.silas.domain.interactor + +import br.com.silas.domain.mocks.UserMock +import br.com.silas.domain.preferences.PreferencesRepository +import br.com.silas.domain.user.GetUserInteractor +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class GetUserInteractorTest { + @Mock + lateinit var preferencesRepository: PreferencesRepository + lateinit var getUserInteractor: GetUserInteractor + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + getUserInteractor = GetUserInteractor(preferencesRepository) + } + + @Test + fun `when call sharedPreferences should be return an user`() { + val user = UserMock.getUserMock() + `when`(preferencesRepository.getUser()).thenReturn(user) + val result = getUserInteractor.execute() + assertThat(result, `is`(user)) + } +} \ No newline at end of file diff --git a/domain/src/test/java/br/com/silas/domain/interactor/LoginInteractorTest.kt b/domain/src/test/java/br/com/silas/domain/interactor/LoginInteractorTest.kt new file mode 100644 index 000000000..0865825c7 --- /dev/null +++ b/domain/src/test/java/br/com/silas/domain/interactor/LoginInteractorTest.kt @@ -0,0 +1,71 @@ +package br.com.silas.domain.interactor + +import br.com.silas.domain.Schedulers +import br.com.silas.domain.mocks.UserMock +import br.com.silas.domain.ErrorResponse +import br.com.silas.domain.user.LoginInteractor +import br.com.silas.domain.user.LoginRepository +import br.com.silas.domain.util.TestScheduler +import io.reactivex.rxjava3.core.Single +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class LoginInteractorTest { + @Mock + lateinit var loginRepository: LoginRepository + lateinit var testScheduler: Schedulers + + lateinit var loginInteractor: LoginInteractor + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + testScheduler = TestScheduler() + loginInteractor = LoginInteractor(loginRepository, testScheduler) + } + + @Test + fun `When call to repository and fetch is successful should be return an user`() { + val user = UserMock.getUserMock() + val loginError = mock(ErrorResponse::class.java) + + val pairResult = Pair(user, loginError) + `when`(loginRepository.fetchUser(anyString(), anyString())).thenReturn(Single.just(pairResult)) + val result = loginInteractor + .execute(loginInteractor.Request("Teste", "1234")) + .test() + + result + .assertComplete() + .assertNoErrors() + .assertValue(pairResult) + + verify(loginRepository).fetchUser("Teste", "1234") + } + + @Test + fun `When call to repository and fetch is fail should be return an user`() { + val exception = Exception() + `when`(loginRepository.fetchUser(anyString(), anyString())).thenReturn( + Single.error( + exception + ) + ) + val result = loginInteractor + .execute(loginInteractor.Request("Teste", "1234")) + .test() + + result + .assertNotComplete() + .assertNoValues() + .assertError(exception) + + verify(loginRepository).fetchUser("Teste", "1234") + } +} \ No newline at end of file diff --git a/domain/src/test/java/br/com/silas/domain/interactor/SaveUserInteractorTest.kt b/domain/src/test/java/br/com/silas/domain/interactor/SaveUserInteractorTest.kt new file mode 100644 index 000000000..a6b382f1e --- /dev/null +++ b/domain/src/test/java/br/com/silas/domain/interactor/SaveUserInteractorTest.kt @@ -0,0 +1,62 @@ +package br.com.silas.domain.interactor + +import br.com.silas.domain.Schedulers +import br.com.silas.domain.mocks.UserMock +import br.com.silas.domain.preferences.PreferencesRepository +import br.com.silas.domain.user.SaveUserInteractor +import br.com.silas.domain.util.TestScheduler +import io.reactivex.rxjava3.core.Completable +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class SaveUserInteractorTest { + + @Mock + private lateinit var preferencesRepository: PreferencesRepository + private lateinit var schedulers: Schedulers + + + private lateinit var saveUserInteractor: SaveUserInteractor + + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + schedulers = TestScheduler() + saveUserInteractor = SaveUserInteractor(preferencesRepository, schedulers) + } + + @Test + fun `Should save an user in shared preferences`() { + val user = UserMock.getUserMock() + `when`(preferencesRepository.save(user)).thenReturn(Completable.complete()) + val result = saveUserInteractor.execute(saveUserInteractor.Request(user)).test() + + result + .assertNoErrors() + .assertComplete() + + verify(preferencesRepository).save(user) + } + + @Test + fun `When save user is unsuccessful should return an exception`() { + val user = UserMock.getUserMock() + val exception = Exception() + `when`(preferencesRepository.save(user)).thenReturn(Completable.error(exception)) + val result = saveUserInteractor.execute(saveUserInteractor.Request(user)).test() + + result + .assertError(exception) + .assertNotComplete() + + verify(preferencesRepository).save(user) + } +} \ No newline at end of file diff --git a/domain/src/test/java/br/com/silas/domain/interactor/StatementsInteractorTest.kt b/domain/src/test/java/br/com/silas/domain/interactor/StatementsInteractorTest.kt new file mode 100644 index 000000000..2eb7cdf33 --- /dev/null +++ b/domain/src/test/java/br/com/silas/domain/interactor/StatementsInteractorTest.kt @@ -0,0 +1,70 @@ +package br.com.silas.domain.interactor + +import br.com.silas.domain.ErrorResponse +import br.com.silas.domain.Schedulers +import br.com.silas.domain.mocks.StatementsMock +import br.com.silas.domain.statements.StatementsInteractor +import br.com.silas.domain.statements.StatementsRepository +import br.com.silas.domain.util.TestScheduler +import io.reactivex.rxjava3.core.Maybe +import io.reactivex.rxjava3.core.Single +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class StatementsInteractorTest { + + @Mock + lateinit var statementsRepository: StatementsRepository + + lateinit var scheduler: Schedulers + + lateinit var statementsInteractor: StatementsInteractor + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + scheduler = TestScheduler() + statementsInteractor = StatementsInteractor(statementsRepository, scheduler) + } + + @Test + fun `When fetch statements is successful should be return a pair of list statements or error statement`() { + + val statementsList = StatementsMock.getListStatements() + val errorResponse = mock(ErrorResponse::class.java) + val pairResponse = Pair(errorResponse, statementsList) + + `when`(statementsRepository.fetchStatements(anyInt())).thenReturn(Single.just(pairResponse)) + val result = statementsInteractor.execute(statementsInteractor.Request(152)).test() + + result + .assertNoErrors() + .assertValue(pairResponse) + .assertComplete() + + verify(statementsRepository).fetchStatements(152) + } + + @Test + fun `When fetch statements is unsuccessful should be return an exception`() { + + val exception = Exception() + + `when`(statementsRepository.fetchStatements(anyInt())).thenReturn(Single.error(exception)) + val result = statementsInteractor.execute(statementsInteractor.Request(152)).test() + + result + .assertNoValues() + .assertError(exception) + .assertNotComplete() + + verify(statementsRepository).fetchStatements(152) + } +} \ No newline at end of file diff --git a/domain/src/test/java/br/com/silas/domain/mocks/StatementsMock.kt b/domain/src/test/java/br/com/silas/domain/mocks/StatementsMock.kt new file mode 100644 index 000000000..6b444234f --- /dev/null +++ b/domain/src/test/java/br/com/silas/domain/mocks/StatementsMock.kt @@ -0,0 +1,10 @@ +package br.com.silas.domain.mocks + +import br.com.silas.domain.statements.Statements + +class StatementsMock { + companion object { + fun getListStatements() = + listOf(Statements("Pagamento", "Conta de Luz", "2018-08-15", 745.03)) + } +} \ No newline at end of file diff --git a/domain/src/test/java/br/com/silas/domain/mocks/UserMock.kt b/domain/src/test/java/br/com/silas/domain/mocks/UserMock.kt new file mode 100644 index 000000000..8086e5f9f --- /dev/null +++ b/domain/src/test/java/br/com/silas/domain/mocks/UserMock.kt @@ -0,0 +1,9 @@ +package br.com.silas.domain.mocks + +import br.com.silas.domain.user.User + +class UserMock { + companion object { + fun getUserMock() = User(1, "Jose da Silva Teste", "2050", "012314564", 3.3445) + } +} \ No newline at end of file diff --git a/domain/src/test/java/br/com/silas/domain/util/MockitoKotlinHelpers.kt b/domain/src/test/java/br/com/silas/domain/util/MockitoKotlinHelpers.kt new file mode 100644 index 000000000..362465891 --- /dev/null +++ b/domain/src/test/java/br/com/silas/domain/util/MockitoKotlinHelpers.kt @@ -0,0 +1,54 @@ + +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package br.com.silas.testeandroidv2 + +/** + * Helper functions that are workarounds to kotlin Runtime Exceptions when using kotlin. + */ + +import org.mockito.ArgumentCaptor +import org.mockito.Mockito + +/** + * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun eq(obj: T): T = Mockito.eq(obj) + + +/** + * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + */ +fun any(): T = Mockito.any() + + +/** + * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException + * when null is returned. + */ +fun capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture() + + +/** + * Helper function for creating an argumentCaptor in kotlin. + */ +inline fun argumentCaptor(): ArgumentCaptor = + ArgumentCaptor.forClass(T::class.java) \ No newline at end of file diff --git a/domain/src/test/java/br/com/silas/domain/util/TestScheduler.kt b/domain/src/test/java/br/com/silas/domain/util/TestScheduler.kt new file mode 100644 index 000000000..28e166898 --- /dev/null +++ b/domain/src/test/java/br/com/silas/domain/util/TestScheduler.kt @@ -0,0 +1,11 @@ +package br.com.silas.domain.util + +import br.com.silas.domain.Schedulers +import io.reactivex.rxjava3.core.Scheduler + +class TestScheduler : Schedulers { + override val subscribeOn: Scheduler + get() = io.reactivex.rxjava3.schedulers.Schedulers.trampoline() + override val observeOn: Scheduler + get() = io.reactivex.rxjava3.schedulers.Schedulers.trampoline() +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..98bed167d --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..ae1cd3d53 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Mar 02 23:51:20 BRT 2021 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..cccdd3d51 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/presentation/.gitignore b/presentation/.gitignore new file mode 100644 index 000000000..f57b6c5ed --- /dev/null +++ b/presentation/.gitignore @@ -0,0 +1,234 @@ + +# Created by https://www.gitignore.io/api/java,kotlin,intellij,androidstudio + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle +.gradle/ +build/ + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +captures/ +.navigation/ +*.ipr +*~ +*.swp + +# Android Patch +gen-external-apklibs + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# NDK +obj/ + +# IntelliJ IDEA +*.iml +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client +.idea/httpRequests + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Java ### +# Compiled class file + +# Log file + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) + +# Package Files # +*.jar +*.nar +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml + +### Kotlin ### +# Compiled class file + +# Log file + +# BlueJ files + +# Mobile Tools for Java (J2ME) + +# Package Files # + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml + + +# End of https://www.gitignore.io/api/java,kotlin,intellij,androidstudio diff --git a/presentation/build.gradle b/presentation/build.gradle new file mode 100644 index 000000000..d37b8583f --- /dev/null +++ b/presentation/build.gradle @@ -0,0 +1,83 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' +} + +def cfg = rootProject.ext.configuration +def libs = rootProject.ext.libraries +def test = rootProject.ext.testingLibraries + +android { + compileSdkVersion cfg.compileSdkVersion + buildToolsVersion cfg.buildToolsVersion + + defaultConfig { + applicationId cfg.applicationId + minSdkVersion cfg.minSdkVersion + targetSdkVersion cfg.targetSdkVersion + versionCode cfg.versionCode + versionName cfg.versionName + multiDexEnabled true + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + + buildFeatures { + viewBinding { + enabled = true + } + dataBinding true + } +} + +dependencies { + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "androidx.appcompat:appcompat:$libs.appcompat" + implementation "androidx.core:core-ktx:$libs.ktxVersion" + implementation "com.google.android.material:material:$libs.material" + implementation "androidx.constraintlayout:constraintlayout:$libs.constraintlayout" + implementation "com.android.support:multidex:$libs.multidex" + +// Koin + implementation "org.koin:koin-androidx-scope:$koin_version" + implementation "org.koin:koin-androidx-viewmodel:$koin_version" + implementation "org.koin:koin-androidx-fragment:$koin_version" + implementation "org.koin:koin-androidx-ext:$koin_version" + +// RxJava + implementation "io.reactivex.rxjava3:rxjava:$libs.rxjavaVersion" + implementation "io.reactivex.rxjava3:rxkotlin:$libs.rxKotlinVersion" + implementation "io.reactivex.rxjava3:rxandroid:$libs.rxAndroidVersion" + +// Tests + testImplementation "junit:junit:$test.junitVersion" + androidTestImplementation "androidx.test.ext:junit:$test.extJunit" + androidTestImplementation "androidx.test.espresso:espresso-core:$test.espressoVersion" + testImplementation "org.mockito:mockito-core:$test.mockitoVersion" + testImplementation "org.mockito:mockito-inline:$test.mockitoInline" + testImplementation "android.arch.core:core-testing:$test.versionTestArchCore" + +// Retrofit + implementation "com.squareup.retrofit2:retrofit:$libs.retrofit" + implementation "com.squareup.retrofit2:converter-gson:$libs.gson" + implementation "com.squareup.retrofit2:adapter-rxjava2:$libs.retrofitAdapter" + + implementation project(':domain') + implementation project(':data') +} \ No newline at end of file diff --git a/presentation/proguard-rules.pro b/presentation/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/presentation/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/presentation/src/androidTest/java/br/com/silas/testeandroidv2/ExampleInstrumentedTest.kt b/presentation/src/androidTest/java/br/com/silas/testeandroidv2/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..b6ae136bb --- /dev/null +++ b/presentation/src/androidTest/java/br/com/silas/testeandroidv2/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package br.com.silas.testeandroidv2 + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("br.com.silas.testeandroidv2", appContext.packageName) + } +} \ No newline at end of file diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml new file mode 100644 index 000000000..1dc178cb7 --- /dev/null +++ b/presentation/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/java/br/com/silas/testeandroidv2/BaseViewModel.kt b/presentation/src/main/java/br/com/silas/testeandroidv2/BaseViewModel.kt new file mode 100644 index 000000000..50028ac1a --- /dev/null +++ b/presentation/src/main/java/br/com/silas/testeandroidv2/BaseViewModel.kt @@ -0,0 +1,19 @@ +package br.com.silas.testeandroidv2 + +import androidx.lifecycle.ViewModel +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.kotlin.plusAssign + +abstract class BaseViewModel : ViewModel() { + private val disposables = CompositeDisposable() + + fun addDisposable(disposable: Disposable) { + disposables += disposable + } + + override fun onCleared() { + super.onCleared() + disposables.dispose() + } +} \ No newline at end of file diff --git a/presentation/src/main/java/br/com/silas/testeandroidv2/MainApplication.kt b/presentation/src/main/java/br/com/silas/testeandroidv2/MainApplication.kt new file mode 100644 index 000000000..bb717e8c1 --- /dev/null +++ b/presentation/src/main/java/br/com/silas/testeandroidv2/MainApplication.kt @@ -0,0 +1,29 @@ +package br.com.silas.testeandroidv2 + +import android.app.Application +import android.content.Context +import androidx.multidex.MultiDex +import br.com.silas.testeandroidv2.di.dataModule +import br.com.silas.testeandroidv2.di.domainModule +import br.com.silas.testeandroidv2.di.presentaionModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin + +class MainApplication : Application() { + override fun onCreate() { + super.onCreate() + startKoin { + androidContext(this@MainApplication) + modules( + presentaionModule, + domainModule, + dataModule + ) + } + } + + override fun attachBaseContext(base: Context?) { + super.attachBaseContext(base) + MultiDex.install(this) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/br/com/silas/testeandroidv2/di/Modules.kt b/presentation/src/main/java/br/com/silas/testeandroidv2/di/Modules.kt new file mode 100644 index 000000000..aadb66e65 --- /dev/null +++ b/presentation/src/main/java/br/com/silas/testeandroidv2/di/Modules.kt @@ -0,0 +1,45 @@ +package br.com.silas.testeandroidv2.di + +import android.content.Context +import br.com.silas.data.local.PreferencesRepositoryImpl +import br.com.silas.data.remote.api.ServiceTesteAndroidV2 +import br.com.silas.data.remote.api.TesteAndroidv2ServiceImpl +import br.com.silas.data.remote.login.LoginRepositoryImpl +import br.com.silas.data.remote.login.UserMapper +import br.com.silas.data.remote.statements.StatementMapper +import br.com.silas.data.remote.statements.StatementsRepositoryImpl +import br.com.silas.domain.Schedulers +import br.com.silas.domain.preferences.PreferencesRepository +import br.com.silas.domain.statements.StatementsInteractor +import br.com.silas.domain.statements.StatementsRepository +import br.com.silas.domain.user.GetUserInteractor +import br.com.silas.domain.user.LoginInteractor +import br.com.silas.domain.user.LoginRepository +import br.com.silas.domain.user.SaveUserInteractor +import br.com.silas.testeandroidv2.scheduler.AppScheduler +import br.com.silas.testeandroidv2.ui.statements.StatementsViewModel +import br.com.silas.testeandroidv2.ui.user.LoginViewModel +import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val presentaionModule = module { + viewModel { StatementsViewModel(get()) } + viewModel { LoginViewModel(get(), get(), get()) } +} +val domainModule = module { + single { AppScheduler() } + single { LoginInteractor(get(), get()) } + single { StatementsInteractor(get(), get()) } + single { GetUserInteractor(get()) } + single { SaveUserInteractor(get(), get()) } +} +val dataModule = module { + factory { TesteAndroidv2ServiceImpl() } + single { UserMapper() } + single { StatementMapper() } + single { androidContext().getSharedPreferences("teste-android-v2", Context.MODE_PRIVATE) } + single { PreferencesRepositoryImpl(get()) } + single { LoginRepositoryImpl(get(), get()) } + single { StatementsRepositoryImpl(get(), get()) } +} \ No newline at end of file diff --git a/presentation/src/main/java/br/com/silas/testeandroidv2/scheduler/AppScheduler.kt b/presentation/src/main/java/br/com/silas/testeandroidv2/scheduler/AppScheduler.kt new file mode 100644 index 000000000..d00f0dc1c --- /dev/null +++ b/presentation/src/main/java/br/com/silas/testeandroidv2/scheduler/AppScheduler.kt @@ -0,0 +1,14 @@ +package br.com.silas.testeandroidv2.scheduler + +import br.com.silas.domain.Schedulers +import io.reactivex.rxjava3.core.Scheduler + +//import io.reactivex.Scheduler + +class AppScheduler : Schedulers { + override val subscribeOn: Scheduler + get() = io.reactivex.rxjava3.schedulers.Schedulers.io() + override val observeOn: Scheduler + get() = io.reactivex.rxjava3.android.schedulers.AndroidSchedulers.mainThread() + +} \ No newline at end of file diff --git a/presentation/src/main/java/br/com/silas/testeandroidv2/ui/statements/StatementsActivity.kt b/presentation/src/main/java/br/com/silas/testeandroidv2/ui/statements/StatementsActivity.kt new file mode 100644 index 000000000..5eee96d90 --- /dev/null +++ b/presentation/src/main/java/br/com/silas/testeandroidv2/ui/statements/StatementsActivity.kt @@ -0,0 +1,108 @@ +package br.com.silas.testeandroidv2.ui.statements + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import br.com.silas.domain.statements.Statements +import br.com.silas.domain.user.User +import br.com.silas.testeandroidv2.R +import br.com.silas.testeandroidv2.databinding.ActivityStatementsBinding +import br.com.silas.testeandroidv2.util.Validate +import org.koin.androidx.viewmodel.ext.android.viewModel + + +class StatementsActivity : AppCompatActivity() { + private lateinit var binding: ActivityStatementsBinding + private lateinit var statementsAdapter: StatementsAdapter + private val statementsViewModel: StatementsViewModel by viewModel() + + companion object { + private const val EXTRA_USER = "extra_user" + fun start(context: Context, user: User) { + val intent = Intent(context, StatementsActivity::class.java).apply { + putExtra(EXTRA_USER, user) + } + context.startActivity(intent) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_statements) +// Validate.statusBarTranslucent(this.window) + +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { +// val w: Window = window +// w.setFlags( +// WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, +// WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS +// ) +// } + + initializeItensAppBar() + fetchStatements() + observerStatements() + observerErrorStatement() + observerLoading() + logout() + + } + + private fun getUserExtra() = intent.getSerializableExtra(EXTRA_USER) as User + + private fun initializeItensAppBar() { + val user = getUserExtra() + + binding.itemStatements.textViewUserName.text = user.name + binding.itemStatements.textViewAccount.text = + user.bankAccount.plus(" / " + Validate.formatAgency(user.agency.toString())) + binding.itemStatements.textviewBalance.text = + Validate.formatDoubleToString(user.balance, true) + } + + private fun fetchStatements() { + statementsViewModel.loadStatements(getUserExtra().id) + } + + private fun observerStatements() { + statementsViewModel.result.observe(this, { + initializeRecyclerView(it) + }) + } + + private fun observerErrorStatement() { + statementsViewModel.errorStatements.observe(this, Observer { + Toast.makeText(this, it.message, Toast.LENGTH_LONG).show() + }) + } + + private fun initializeRecyclerView(statementsList: List) { + + val linearLayoutManager = LinearLayoutManager(this) + binding.recyclerViewStatements.layoutManager = linearLayoutManager + statementsAdapter = StatementsAdapter(statementsList) + + binding.recyclerViewStatements.adapter = statementsAdapter + } + + private fun observerLoading() { + statementsViewModel.loading.observe(this, { + when (it) { + true -> binding.progressBarStatements.visibility = View.VISIBLE + false -> binding.progressBarStatements.visibility = View.GONE + } + }) + } + + private fun logout() { + binding.itemStatements.imageButtonLogout.setOnClickListener { + this.finish() + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/br/com/silas/testeandroidv2/ui/statements/StatementsAdapter.kt b/presentation/src/main/java/br/com/silas/testeandroidv2/ui/statements/StatementsAdapter.kt new file mode 100644 index 000000000..8f985c2a8 --- /dev/null +++ b/presentation/src/main/java/br/com/silas/testeandroidv2/ui/statements/StatementsAdapter.kt @@ -0,0 +1,41 @@ +package br.com.silas.testeandroidv2.ui.statements + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.RecyclerView +import br.com.silas.domain.statements.Statements +import br.com.silas.testeandroidv2.R +import br.com.silas.testeandroidv2.databinding.StatementsItemListBinding +import br.com.silas.testeandroidv2.util.Validate +import br.com.silas.testeandroidv2.util.Validate.formatDate +import java.util.* + +class StatementsAdapter(private val statementsList: List) : + RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val bind: StatementsItemListBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.statements_item_list, + parent, + false + ) + return ViewHolder(bind) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.binding.statement = statementsList[position] + holder.binding.textViewValue.text = Validate.formatDoubleToString(statementsList[position].value, true) + holder.binding.textViewDate.text = Validate.formatDate(statementsList[position].date) + holder.binding.executePendingBindings() + } + + override fun getItemCount() = statementsList.size + + + inner class ViewHolder(val binding: StatementsItemListBinding) : + RecyclerView.ViewHolder(binding.root) + + +} \ No newline at end of file diff --git a/presentation/src/main/java/br/com/silas/testeandroidv2/ui/statements/StatementsViewModel.kt b/presentation/src/main/java/br/com/silas/testeandroidv2/ui/statements/StatementsViewModel.kt new file mode 100644 index 000000000..729f43841 --- /dev/null +++ b/presentation/src/main/java/br/com/silas/testeandroidv2/ui/statements/StatementsViewModel.kt @@ -0,0 +1,51 @@ +package br.com.silas.testeandroidv2.ui.statements + +import androidx.lifecycle.MutableLiveData +import br.com.silas.domain.ErrorResponse +import br.com.silas.domain.statements.Statements +import br.com.silas.domain.statements.StatementsInteractor +import br.com.silas.testeandroidv2.BaseViewModel +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.observers.DisposableMaybeObserver +import io.reactivex.rxjava3.observers.DisposableSingleObserver + +class StatementsViewModel(private val statementsInteractor: StatementsInteractor) : + BaseViewModel() { + + var loading = MutableLiveData() + var error = MutableLiveData() + var errorStatements = MutableLiveData() + var result = MutableLiveData>() + + fun loadStatements(userId: Int) = addDisposable(fetchStatements(userId)) + + private fun fetchStatements(userId: Int): Disposable { + + return statementsInteractor + .execute(statementsInteractor.Request(userId)).subscribeWith(object : + DisposableSingleObserver>>() { + override fun onStart() { + loading.value = true + } + + override fun onSuccess(statementsResponse: Pair>) { + if (!statementsResponse.first.message.isNullOrBlank()) { + errorStatements.value = statementsResponse.first + } + + if (statementsResponse.second.isNotEmpty()) { + result.value = statementsResponse.second + } + + loading.value = false + } + + override fun onError(e: Throwable?) { + error.value = e + loading.value = false + + } + + }) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/br/com/silas/testeandroidv2/ui/user/LoginActivity.kt b/presentation/src/main/java/br/com/silas/testeandroidv2/ui/user/LoginActivity.kt new file mode 100644 index 000000000..bf6d49450 --- /dev/null +++ b/presentation/src/main/java/br/com/silas/testeandroidv2/ui/user/LoginActivity.kt @@ -0,0 +1,145 @@ +package br.com.silas.testeandroidv2.ui.user + +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import br.com.silas.testeandroidv2.R +import br.com.silas.testeandroidv2.databinding.ActivityLoginBinding +import br.com.silas.testeandroidv2.ui.statements.StatementsActivity +import br.com.silas.testeandroidv2.util.Validate +import br.com.silas.testeandroidv2.util.Validate.clearErrorText +import org.koin.androidx.viewmodel.ext.android.viewModel + +class LoginActivity : AppCompatActivity() { + private val loginViewModel: LoginViewModel by viewModel() + lateinit var binding: ActivityLoginBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_login) + + fetchLogin() + observerSaveUser() + observerUser() + observerLoading() + observerErrorLogin() + observerUserLogged() + } + + override fun onResume() { + super.onResume() + getLastUserLogged() + } + + private fun fetchLogin() { + binding.buttonLogin.setOnClickListener { + if (!validateLogin(binding.textInputLogin.text.toString())) { + clearErrorText(binding.textInputLogin, binding.textInputLayoutLogin) + return@setOnClickListener + } + + if (!validatePassword(binding.textInputPassword.text.toString())) { + clearErrorText(binding.textInputPassword, binding.textInputLayoutPassword) + return@setOnClickListener + } + + loginViewModel.fetUser( + binding.textInputLogin.text.toString(), + binding.textInputPassword.text.toString() + ) + } + } + + + private fun observerUser() { + loginViewModel.user.observe(this, { + loginViewModel.saveUser(it) + }) + } + + private fun observerSaveUser() { + loginViewModel.userSaved.observe(this, { + when (it) { + true -> + loginViewModel.user.value?.let { user -> StatementsActivity.start(this, user) } + false -> Toast.makeText( + this,getString(R.string.msg_error_save_user), + Toast.LENGTH_SHORT + ).show() + + } + }) + } + + private fun observerLoading() { + loginViewModel.loading.observe(this, { + when (it) { + true -> { + binding.buttonLogin.visibility = View.GONE + binding.progressBarLogin.visibility = View.VISIBLE + binding.textViewUserLogged.visibility = View.INVISIBLE + } + false -> { + binding.buttonLogin.visibility = View.VISIBLE + binding.progressBarLogin.visibility = View.GONE + binding.textViewUserLogged.visibility = View.VISIBLE + } + } + }) + } + + private fun observerErrorLogin() { + loginViewModel.errorLogin.observe(this, { + Toast.makeText(this, it.message, Toast.LENGTH_LONG).show() + }) + } + + private fun getLastUserLogged() { + loginViewModel.getLastLoggedUser() + } + + private fun observerUserLogged() { + loginViewModel.lastUserLogged.observe(this, { + binding.textViewUserLogged.text = it.name + }) + } + + private fun validateLogin(login: String): Boolean { + val isEmail = Validate.validateEmail(login) + val isCpf = Validate.validateCpf(login) + + if (!isEmail && !isCpf) { + binding.textInputLayoutLogin.error = getString(R.string.msg_invalid_email_or_cpf) + return false + } + + if (isEmail) { + if (!Validate.validateEmail(login)) { + binding.textInputLayoutLogin.error = getString(R.string.msg_invalid_email) + + return false + } + } + + if (isCpf) { + if (!Validate.validateCpf(login)) { + binding.textInputLayoutLogin.error = getString(R.string.msg_invalid_cpf) + return false + } + } + + return true + } + + private fun validatePassword(password: String): Boolean { + if (!Validate.validatePassword(password)) { + binding.textInputLayoutPassword.error = getString(R.string.msg_invalid_password) + return false + } + + return true + } + +} diff --git a/presentation/src/main/java/br/com/silas/testeandroidv2/ui/user/LoginViewModel.kt b/presentation/src/main/java/br/com/silas/testeandroidv2/ui/user/LoginViewModel.kt new file mode 100644 index 000000000..ebcfae5d3 --- /dev/null +++ b/presentation/src/main/java/br/com/silas/testeandroidv2/ui/user/LoginViewModel.kt @@ -0,0 +1,89 @@ +package br.com.silas.testeandroidv2.ui.user + +import androidx.lifecycle.MutableLiveData +import br.com.silas.domain.ErrorResponse +import br.com.silas.domain.user.GetUserInteractor +import br.com.silas.domain.user.LoginInteractor +import br.com.silas.domain.user.SaveUserInteractor +import br.com.silas.domain.user.User +import br.com.silas.testeandroidv2.BaseViewModel +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.observers.DisposableCompletableObserver +import io.reactivex.rxjava3.observers.DisposableSingleObserver + +class LoginViewModel( + private val loginInteractor: LoginInteractor, + private val getUserInteractor: GetUserInteractor, + private val saveUserInteractor: SaveUserInteractor, +) : BaseViewModel() { + + var loading = MutableLiveData() + var error = MutableLiveData() + var errorLogin = MutableLiveData() + var user = MutableLiveData() + var lastUserLogged = MutableLiveData() + var userSaved = MutableLiveData() + + + fun getLastLoggedUser() = getUserPreferences() + + private fun getUserPreferences() { + val user = getUserInteractor.execute() + lastUserLogged.value = user + } + + fun fetUser(login: String, password: String) { + addDisposable(login(login, password)) + } + + fun saveUser(user: User) = saveUserInPreferences(user) + + private fun login(login: String, password: String): Disposable { + + return loginInteractor.execute(loginInteractor.Request(login, password)).subscribeWith( + object : DisposableSingleObserver>() { + override fun onStart() { + loading.value = true + } + + override fun onSuccess(login: Pair) { + if (login.first?.name != null) { + user.value = login.first + } + + if (!login.second.message.isNullOrBlank()) { + errorLogin.value = login.second + + } + loading.value = false + } + + override fun onError(throwable: Throwable) { + error.value = throwable + loading.value = false + } + + }) + } + + private fun saveUserInPreferences(user: User) { + return saveUserInteractor.execute(saveUserInteractor.Request(user)) + .subscribe(object : + DisposableCompletableObserver() { + override fun onStart() { + loading.value = true + } + override fun onComplete() { + userSaved.value = true + loading.value = false + } + + override fun onError(e: Throwable?) { + error.value = e + userSaved.value = false + loading.value = false + } + }) + } + +} \ No newline at end of file diff --git a/presentation/src/main/java/br/com/silas/testeandroidv2/util/Validate.kt b/presentation/src/main/java/br/com/silas/testeandroidv2/util/Validate.kt new file mode 100644 index 000000000..e1b469b9b --- /dev/null +++ b/presentation/src/main/java/br/com/silas/testeandroidv2/util/Validate.kt @@ -0,0 +1,84 @@ +package br.com.silas.testeandroidv2.util + +import android.text.Editable +import android.text.TextWatcher +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import java.text.NumberFormat +import java.text.SimpleDateFormat +import java.util.* +import java.util.regex.Pattern + +object Validate { + + fun formatDoubleToString(number: Double, showSymbol: Boolean = false): String { + val numberFormat = NumberFormat.getCurrencyInstance( + Locale("pt", "BR") + ) + val symbol = numberFormat.currency!!.symbol + return if (!showSymbol) { + numberFormat.format(number).replace(symbol, "") + } else { + numberFormat.format(number) + } + } + + fun formatAgency(agency: String): String { + var value = agency + if (value.length > 7) { + value = StringBuilder(value).insert(value.length - 1, "-") + .insert(value.length - 7, ".") + .toString() + } + return value + } + + fun validateEmail(value: String): Boolean { + val emailPattern = Pattern.compile( + "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + + "\\@" + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + + "(" + + "\\." + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + + ")+" + ) + + return emailPattern.matcher(value).matches() + } + + fun validatePassword(password: String): Boolean { + val passwordPattern = "((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[_@#%!*:;?$.,%]).{4,20})" + + return Pattern.compile(passwordPattern).matcher(password).matches() + } + + fun validateCpf(value: String): Boolean { + val cpfPattern = Pattern.compile("[0-9]{3}\\.?[0-9]{3}\\.?[0-9]{3}\\-?[0-9]{2}") + + return cpfPattern.matcher(value).matches() + } + + fun clearErrorText(TextInput: TextInputEditText, textInputLayout: TextInputLayout) { + TextInput.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + textInputLayout.error = null + } + + override fun afterTextChanged(s: Editable?) { + } + }) + } + + fun formatDate(dateString: String): String { + val locale = Locale("pt", "BR") + val dataEntry = SimpleDateFormat("yyyy-MM-dd", locale) + val dateFormat = SimpleDateFormat("dd/MM/yyyy", locale) + val date = dataEntry.parse(dateString) + return dateFormat.format(date) + } + +} \ No newline at end of file diff --git a/presentation/src/main/res/drawable-v24/ic_launcher_foreground.xml b/presentation/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/presentation/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_launcher_background.xml b/presentation/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/presentation/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/drawable/logo.png b/presentation/src/main/res/drawable/logo.png new file mode 100644 index 000000000..66bdc8d5d Binary files /dev/null and b/presentation/src/main/res/drawable/logo.png differ diff --git a/presentation/src/main/res/drawable/logout.png b/presentation/src/main/res/drawable/logout.png new file mode 100644 index 000000000..de1e4ae3c Binary files /dev/null and b/presentation/src/main/res/drawable/logout.png differ diff --git a/presentation/src/main/res/layout/activity_login.xml b/presentation/src/main/res/layout/activity_login.xml new file mode 100644 index 000000000..2e3bd0aab --- /dev/null +++ b/presentation/src/main/res/layout/activity_login.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/layout/activity_statements.xml b/presentation/src/main/res/layout/activity_statements.xml new file mode 100644 index 000000000..f699bef2c --- /dev/null +++ b/presentation/src/main/res/layout/activity_statements.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/statements_item_appbar.xml b/presentation/src/main/res/layout/statements_item_appbar.xml new file mode 100644 index 000000000..73ad42fda --- /dev/null +++ b/presentation/src/main/res/layout/statements_item_appbar.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/layout/statements_item_list.xml b/presentation/src/main/res/layout/statements_item_list.xml new file mode 100644 index 000000000..dd2729a09 --- /dev/null +++ b/presentation/src/main/res/layout/statements_item_list.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/presentation/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/presentation/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/presentation/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/presentation/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/mipmap-hdpi/ic_launcher.png b/presentation/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..a571e6009 Binary files /dev/null and b/presentation/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/presentation/src/main/res/mipmap-hdpi/ic_launcher_round.png b/presentation/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..61da551c5 Binary files /dev/null and b/presentation/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/presentation/src/main/res/mipmap-mdpi/ic_launcher.png b/presentation/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..c41dd2853 Binary files /dev/null and b/presentation/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/presentation/src/main/res/mipmap-mdpi/ic_launcher_round.png b/presentation/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..db5080a75 Binary files /dev/null and b/presentation/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/presentation/src/main/res/mipmap-xhdpi/ic_launcher.png b/presentation/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..6dba46dab Binary files /dev/null and b/presentation/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/presentation/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/presentation/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..da31a871c Binary files /dev/null and b/presentation/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/presentation/src/main/res/mipmap-xxhdpi/ic_launcher.png b/presentation/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..15ac68172 Binary files /dev/null and b/presentation/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/presentation/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/presentation/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..b216f2d31 Binary files /dev/null and b/presentation/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..f25a41974 Binary files /dev/null and b/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..e96783ccc Binary files /dev/null and b/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/presentation/src/main/res/values-night/themes.xml b/presentation/src/main/res/values-night/themes.xml new file mode 100644 index 000000000..782123a03 --- /dev/null +++ b/presentation/src/main/res/values-night/themes.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/values/colors.xml b/presentation/src/main/res/values/colors.xml new file mode 100644 index 000000000..405e4c603 --- /dev/null +++ b/presentation/src/main/res/values/colors.xml @@ -0,0 +1,9 @@ + + + #3B48EE + #3B48EE + #3B48EE + #ffffff + #485465 + #A8B4C4 + \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml new file mode 100644 index 000000000..84149292a --- /dev/null +++ b/presentation/src/main/res/values/strings.xml @@ -0,0 +1,14 @@ + + TesteAndroidv2 + User + Password + Login + Recentes + Conta + Saldo + Informe um email valido. + A senha deve conter, uma letra maiuscula, um caracter especial e um caracter alfanumérico. + Informe um cpf valido. + Informe um email ou cpf valido. + Ocorreu um erro ao salvar o usuario + \ No newline at end of file diff --git a/presentation/src/main/res/values/themes.xml b/presentation/src/main/res/values/themes.xml new file mode 100644 index 000000000..177aba8a7 --- /dev/null +++ b/presentation/src/main/res/values/themes.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/presentation/src/test/java/MockitoKotlinHelpers.kt b/presentation/src/test/java/MockitoKotlinHelpers.kt new file mode 100644 index 000000000..1516a0625 --- /dev/null +++ b/presentation/src/test/java/MockitoKotlinHelpers.kt @@ -0,0 +1,72 @@ + +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package br.com.silas.testeandroidv2 + +/** + * Helper functions that are workarounds to kotlin Runtime Exceptions when using kotlin. + */ + +import org.mockito.AdditionalMatchers +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.mockito.internal.util.Primitives + +/** + * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun eq(obj: T): T = Mockito.eq(obj) + + +/** + * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + */ +fun any(): T = Mockito.any() + +/** + * Argument that is either null or of the given type. + * + *

+ * See examples in javadoc for {@link ArgumentMatchers} class + *

+ * + * @param clazz Type to avoid casting + * @return null. + */ +fun nullable(clazz: Class?): T { + AdditionalMatchers.or(ArgumentMatchers.isNull(), ArgumentMatchers.isA(clazz)) + return Primitives.defaultValue(clazz) as T +} + + +/** + * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException + * when null is returned. + */ +fun capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture() + + +/** + * Helper function for creating an argumentCaptor in kotlin. + */ +inline fun argumentCaptor(): ArgumentCaptor = + ArgumentCaptor.forClass(T::class.java) \ No newline at end of file diff --git a/presentation/src/test/java/br/com/silas/testeandroidv2/mocks/ErrorResponseMock.kt b/presentation/src/test/java/br/com/silas/testeandroidv2/mocks/ErrorResponseMock.kt new file mode 100644 index 000000000..09dcc73fa --- /dev/null +++ b/presentation/src/test/java/br/com/silas/testeandroidv2/mocks/ErrorResponseMock.kt @@ -0,0 +1,10 @@ +package br.com.silas.testeandroidv2.br.com.silas.testeandroidv2.mocks + +import br.com.silas.domain.ErrorResponse + +class ErrorResponseMock { + companion object { + fun getErrorResponseIsEmpty() = ErrorResponse(0,"") + fun getErrorResponseIsNotEmpty() = ErrorResponse(0,"Teste") + } +} \ No newline at end of file diff --git a/presentation/src/test/java/br/com/silas/testeandroidv2/mocks/StatementsMock.kt b/presentation/src/test/java/br/com/silas/testeandroidv2/mocks/StatementsMock.kt new file mode 100644 index 000000000..0e77423f2 --- /dev/null +++ b/presentation/src/test/java/br/com/silas/testeandroidv2/mocks/StatementsMock.kt @@ -0,0 +1,10 @@ +package br.com.silas.testeandroidv2.br.com.silas.testeandroidv2.mocks + +import br.com.silas.domain.statements.Statements + +class StatementsMock { + companion object { + fun getListStatements() = + listOf(Statements("Pagamento", "Conta de Luz", "2018-08-15", 745.03)) + } +} \ No newline at end of file diff --git a/presentation/src/test/java/br/com/silas/testeandroidv2/mocks/UserMock.kt b/presentation/src/test/java/br/com/silas/testeandroidv2/mocks/UserMock.kt new file mode 100644 index 000000000..8faf9bc37 --- /dev/null +++ b/presentation/src/test/java/br/com/silas/testeandroidv2/mocks/UserMock.kt @@ -0,0 +1,9 @@ +package br.com.silas.testeandroidv2.br.com.silas.testeandroidv2.mocks + +import br.com.silas.domain.user.User + +class UserMock { + companion object { + fun getUserMock() = User(1, "Jose da Silva Teste", "2050", "012314564", 3.3445) + } +} \ No newline at end of file diff --git a/presentation/src/test/java/br/com/silas/testeandroidv2/viewModels/LoginViewModelTest.kt b/presentation/src/test/java/br/com/silas/testeandroidv2/viewModels/LoginViewModelTest.kt new file mode 100644 index 000000000..c7e39f51f --- /dev/null +++ b/presentation/src/test/java/br/com/silas/testeandroidv2/viewModels/LoginViewModelTest.kt @@ -0,0 +1,109 @@ +package br.com.silas.testeandroidv2.br.com.silas.testeandroidv2.viewModels + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import br.com.silas.domain.user.GetUserInteractor +import br.com.silas.domain.user.LoginInteractor +import br.com.silas.domain.user.SaveUserInteractor +import br.com.silas.domain.user.User +import br.com.silas.testeandroidv2.any +import br.com.silas.testeandroidv2.br.com.silas.testeandroidv2.mocks.ErrorResponseMock +import br.com.silas.testeandroidv2.br.com.silas.testeandroidv2.mocks.UserMock +import br.com.silas.testeandroidv2.ui.user.LoginViewModel +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.core.Is.`is` +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +class LoginViewModelTest { + @Mock + private lateinit var loginInteractor: LoginInteractor + @Mock + private lateinit var saveUserInteractor: SaveUserInteractor + + @Mock + lateinit var getUserInteractor: GetUserInteractor + + lateinit var userMock: User + lateinit var loginViewModel: LoginViewModel + + @get:Rule + var rule: TestRule = InstantTaskExecutorRule() + + @Before + @Throws(Exception::class) + fun setUp() { + MockitoAnnotations.initMocks(this) + + userMock = UserMock.getUserMock() + loginViewModel = LoginViewModel(loginInteractor, getUserInteractor, saveUserInteractor) + } + + @Test + @Throws(Exception::class) + fun `when fetch user is successful should be return an user`() { + val loginError = ErrorResponseMock.getErrorResponseIsEmpty() + val pairResult = Pair(userMock, loginError) + + `when`(loginInteractor.execute(any())).thenReturn(Single.just(pairResult)) + loginViewModel.fetUser("teste@teste.com", "Teste#01") + + assertThat(loginViewModel.user.value, `is`(pairResult.first)) + } + + @Test + @Throws(Exception::class) + fun `when fetch user is unsuccessful should be return a message of error`() { + val loginError = ErrorResponseMock.getErrorResponseIsNotEmpty() + val pairResult = Pair(userMock, loginError) + + `when`(loginInteractor.execute(any())).thenReturn(Single.just(pairResult)) + loginViewModel.fetUser("teste@teste.com", "Teste#01") + + assertThat(loginViewModel.errorLogin.value, `is`(pairResult.second)) + } + + @Test + @Throws(Exception::class) + fun `when fetch user is fail should be return an exception`() { + val exception = Exception() + `when`(loginInteractor.execute(any())).thenReturn(Single.error(exception)) + loginViewModel.fetUser("user", "123") + + assertThat(loginViewModel.error.value, `is`(exception)) + } + + @Test + fun `When fetch for user logged, should be return an user`() { + val user = UserMock.getUserMock() + `when`(getUserInteractor.execute()).thenReturn(user) + loginViewModel.getLastLoggedUser() + + assertThat(loginViewModel.lastUserLogged.value, `is`(user)) + } + + @Test + fun `When save user in preferences is success should be return true`() { + val user = UserMock.getUserMock() + `when`(saveUserInteractor.execute(any())).thenReturn(Completable.complete()) + loginViewModel.saveUser(user) + + assertThat(loginViewModel.userSaved.value, `is`(true)) + } + + @Test + fun `When save user in preferences is unsuccessful should be return an exception`() { + val user = UserMock.getUserMock() + val exception = Exception() + `when`(saveUserInteractor.execute(any())).thenReturn(Completable.error(exception)) + loginViewModel.saveUser(user) + + assertThat(loginViewModel.userSaved.value, `is`(false)) + } +} \ No newline at end of file diff --git a/presentation/src/test/java/br/com/silas/testeandroidv2/viewModels/StatementsViewModelTest.kt b/presentation/src/test/java/br/com/silas/testeandroidv2/viewModels/StatementsViewModelTest.kt new file mode 100644 index 000000000..3a157fb66 --- /dev/null +++ b/presentation/src/test/java/br/com/silas/testeandroidv2/viewModels/StatementsViewModelTest.kt @@ -0,0 +1,63 @@ +package br.com.silas.testeandroidv2.br.com.silas.testeandroidv2.viewModels + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import br.com.silas.domain.statements.StatementsInteractor +import br.com.silas.testeandroidv2.any +import br.com.silas.testeandroidv2.br.com.silas.testeandroidv2.mocks.ErrorResponseMock +import br.com.silas.testeandroidv2.br.com.silas.testeandroidv2.mocks.StatementsMock +import br.com.silas.testeandroidv2.ui.statements.StatementsViewModel +import io.reactivex.rxjava3.core.Maybe +import io.reactivex.rxjava3.core.Single +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class StatementsViewModelTest { + + @Mock + lateinit var statementsInteractor: StatementsInteractor + + lateinit var statementsViewModel: StatementsViewModel + + @get:Rule + var rule: TestRule = InstantTaskExecutorRule() + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + statementsViewModel = StatementsViewModel(statementsInteractor) + } + + @Test + fun `When fetch statements is successful should be return a list of statements`() { + val statementsList = StatementsMock.getListStatements() + val statementsError = ErrorResponseMock.getErrorResponseIsEmpty() + val pairResponse = Pair(statementsError, statementsList) + + `when`(statementsInteractor.execute(any())).thenReturn(Single.just(pairResponse)) + statementsViewModel.loadStatements(152) + + assertThat(statementsViewModel.result.value, `is`(pairResponse.second)) + } + + @Test + fun `When fetch statements is unsuccessful should be return an error response`() { + val statementsList = StatementsMock.getListStatements() + val statementsError = ErrorResponseMock.getErrorResponseIsNotEmpty() + val pairResponse = Pair(statementsError, statementsList) + + `when`(statementsInteractor.execute(any())).thenReturn(Single.just(pairResponse)) + statementsViewModel.loadStatements(152) + + assertThat(statementsViewModel.errorStatements.value, `is`(pairResponse.first)) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..1f270603b --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +include ':data' +include ':domain' +include ':presentation' +rootProject.name = "TesteAndroidv2" \ No newline at end of file