Merge pull request #1 from fknives/sanity-check

Sanity check
This commit is contained in:
Gergely Hegedis 2021-09-18 17:32:41 +03:00 committed by GitHub
commit 3562a8c900
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 803 additions and 469 deletions

10
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: gradle
directory: "/"
target-branch: "develop"
schedule:
interval: "weekly"
day: "monday"
time: "12:00"
open-pull-requests-limit: 15

75
.github/workflows/pull-request-jobs.yml vendored Normal file
View file

@ -0,0 +1,75 @@
name: Verify Pull request is publishable
on:
pull_request:
branches:
- develop
env:
GITHUB_USERNAME: "fknives"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
run-code-analysis:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: Run clean
run: ./gradlew clean
- name: Run detekt
run: ./gradlew detekt
- name: Upload Detekt Results
uses: actions/upload-artifact@v2
if: always()
with:
name: Detekt Results
path: build/reports/detekt.html
retention-days: 1
- name: Run ktlint
run: ./gradlew ktlintCheck
- name: Upload ktLint Results
uses: actions/upload-artifact@v2
if: always()
with:
name: ktLint Results
path: ./**/build/reports/ktlint/**/*ktlint*Check.txt
retention-days: 1
- name: Run Lint
run: ./gradlew lint
- name: Upload Lint Results
uses: actions/upload-artifact@v2
if: always()
with:
name: Lint Results
path: ./**/build/reports/*lint-results*.html
retention-days: 1
run-tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: Run Unit Tests
run: ./gradlew unitTests
- name: Upload Test Results
uses: actions/upload-artifact@v2
if: always()
with:
name: Unit Test Results
path: ./**/build/reports/tests/**/index.html
retention-days: 1

View file

@ -6,11 +6,11 @@ plugins {
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "org.fnives.test.showcase"
minSdkVersion 21
//noinspection OldTargetApi // todo
targetSdkVersion 30
versionCode 1
versionName "1.0"
@ -25,16 +25,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures {
viewBinding true
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
androidTest {
@ -46,19 +40,6 @@ android {
}
}
testOptions.unitTests.all {
useJUnitPlatform()
testLogging {
events 'started', 'passed', 'skipped', 'failed'
exceptionFormat "full"
showStandardStreams true
}
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
// needed for androidTest
packagingOptions {
exclude 'META-INF/LGPL2.1'
@ -66,14 +47,6 @@ android {
exclude 'META-INF/LICENSE.md'
exclude 'META-INF/LICENSE-notice.md'
}
lintOptions {
warningsAsErrors true
abortOnError true
textReport true
ignore 'Overdraw'
textOutput "stdout"
}
}
afterEvaluate {
@ -83,20 +56,17 @@ afterEvaluate {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.core:core-ktx:$androidx_core_version"
implementation "androidx.appcompat:appcompat:$androidx_appcompat_version"
implementation "com.google.android.material:material:$androidx_material_version"
implementation "androidx.constraintlayout:constraintlayout:$androidx_constraintlayout_version"
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:$androidx_livedata_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidx_livedata_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidx_livedata_version"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version"
// 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 "io.insert-koin:koin-android:$koin_version"
implementation "androidx.room:room-runtime:$androidx_room_version"
kapt "androidx.room:room-compiler:$androidx_room_version"
@ -113,7 +83,7 @@ dependencies {
testImplementation "org.mockito.kotlin:mockito-kotlin:$testing_kotlin_mockito_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
testImplementation "com.jraska.livedata:testing-ktx:$testing_livedata_version"
testImplementation "org.koin:koin-test:$koin_version"
testImplementation "io.insert-koin:koin-test-junit5:$koin_version"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version"
// robolectric specific
@ -130,7 +100,7 @@ dependencies {
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
androidTestImplementation "org.koin:koin-test:$koin_version"
androidTestImplementation "io.insert-koin:koin-test-junit4:$koin_version"
androidTestImplementation "junit:junit:$testing_junit4_version"
androidTestImplementation "androidx.test:core:$testing_androidx_code_version"
androidTestImplementation "androidx.test:runner:$testing_androidx_code_version"

View file

@ -22,7 +22,7 @@ object AndroidTestServerTypeConfiguration : ServerTypeConfiguration, KoinTest {
.build()
loadKoinModules(
module {
single(qualifier = sessionless, override = true) { okHttpClientWithCertificate }
single(qualifier = sessionless) { okHttpClientWithCertificate }
}
)
}

View file

@ -14,7 +14,8 @@
android:name=".TestShowcaseApplication"
android:theme="@style/Theme.TestShowCase"
tools:ignore="AllowBackup">
<activity android:name=".ui.splash.SplashActivity">
<activity android:name=".ui.splash.SplashActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -32,7 +32,9 @@ class MainActivity : AppCompatActivity() {
val adapter = FavouriteContentAdapter(viewModel.mapToAdapterListener())
binding.recycler.layoutManager = LinearLayoutManager(this)
binding.recycler.addItemDecoration(VerticalSpaceItemDecoration(resources.getDimensionPixelOffset(R.dimen.padding)))
binding.recycler.addItemDecoration(
VerticalSpaceItemDecoration(resources.getDimensionPixelOffset(R.dimen.padding))
)
binding.recycler.adapter = adapter
viewModel.content.observe(this) {

View file

@ -1,6 +1,7 @@
package org.fnives.test.showcase.favourite
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.take
@ -20,6 +21,7 @@ import org.koin.test.KoinTest
import org.koin.test.inject
@Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
internal class FavouriteContentLocalStorageImplTest : KoinTest {
@ -48,21 +50,23 @@ internal class FavouriteContentLocalStorageImplTest : KoinTest {
}
@Test
fun GIVEN_content_id_added_WHEN_removed_to_Favourite_THEN_it_no_longer_can_be_read_out() = runBlocking {
val expected = listOf<ContentId>()
sut.markAsFavourite(ContentId("b"))
fun GIVEN_content_id_added_WHEN_removed_to_Favourite_THEN_it_no_longer_can_be_read_out() =
runBlocking {
val expected = listOf<ContentId>()
sut.markAsFavourite(ContentId("b"))
sut.deleteAsFavourite(ContentId("b"))
val actual = sut.observeFavourites().first()
sut.deleteAsFavourite(ContentId("b"))
val actual = sut.observeFavourites().first()
Assert.assertEquals(expected, actual)
}
Assert.assertEquals(expected, actual)
}
@Test
fun GIVEN_empty_database_WHILE_observing_content_WHEN_favourite_added_THEN_change_is_emitted() =
runBlocking<Unit> {
val expected = listOf(listOf(), listOf(ContentId("a")))
val testDispatcher = TestCoroutineDispatcher()
val actual = async(testDispatcher) {
sut.observeFavourites().take(2).toList()
}
@ -79,6 +83,7 @@ internal class FavouriteContentLocalStorageImplTest : KoinTest {
val expected = listOf(listOf(ContentId("a")), listOf())
sut.markAsFavourite(ContentId("a"))
val testDispatcher = TestCoroutineDispatcher()
val actual = async(testDispatcher) {
sut.observeFavourites().take(2).toList()
}

View file

@ -4,11 +4,13 @@ import org.fnives.test.showcase.core.login.LoginUseCase
import org.fnives.test.showcase.testutils.InstantExecutorExtension
import org.fnives.test.showcase.testutils.TestMainDispatcher
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.kotlin.mock
@Disabled("CodeKata")
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class)
class CodeKataAuthViewModel {
@ -25,24 +27,20 @@ class CodeKataAuthViewModel {
@DisplayName("GIVEN_initialized_viewModel_WHEN_observed_THEN_loading_false_other_fields_are_empty")
@Test
fun initialSetup() {
}
@DisplayName("GIVEN_password_text_WHEN_onPasswordChanged_is_called_THEN_password_livedata_is_updated")
@Test
fun whenPasswordChangedLiveDataIsUpdated() {
}
@DisplayName("GIVEN_username_text_WHEN_onUsernameChanged_is_called_THEN_username_livedata_is_updated")
@Test
fun whenUsernameChangedLiveDataIsUpdated() {
}
@DisplayName("GIVEN_no_password_or_username_WHEN_login_is_Called_THEN_empty_credentials_are_used_in_usecase")
@Test
fun noPasswordUsesEmptyStringInLoginUseCase() {
}
}
}

View file

@ -1,39 +1,29 @@
package org.fnives.test.showcase.ui.splash
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.testutils.InstantExecutorExtension
import org.fnives.test.showcase.testutils.TestMainDispatcher
import org.fnives.test.showcase.ui.shared.Event
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@Disabled("CodeKata")
internal class CodeKataSplashViewModelTest {
@BeforeEach
fun setUp() {
}
@DisplayName("GIVEN not logged in user WHEN splash started THEN after half a second navigated to authentication")
@Test
fun loggedOutUserGoesToAuthentication() {
}
@DisplayName("GIVEN logged in user WHEN splash started THEN after half a second navigated to home")
@Test
fun loggedInUserGoestoHome() {
}
@DisplayName("GIVEN not logged in user WHEN splash started THEN before half a second no event is sent")
@Test
fun withoutEnoughTimeNoNavigationHappens() {
}
}
}

View file

@ -1,2 +1,3 @@
sdk=28
shadows=org.fnives.test.showcase.testutils.shadow.ShadowSnackbar
instrumentedPackages=androidx.loader.content

View file

@ -1,61 +1,30 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.32"
ext.detekt_version = "1.16.0"
ext.kotlin_version = "1.5.30"
ext.detekt_version = "1.18.1"
repositories {
mavenCentral()
google()
maven { url "https://plugins.gradle.org/m2/" }
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.3"
classpath 'com.android.tools.build:gradle:7.0.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.0.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.0"
}
}
//apply plugin: "io.gitlab.arturbosch.detekt" version "$detekt_version"
plugins {
id "io.gitlab.arturbosch.detekt" version "$detekt_version"
}
detekt {
toolVersion = "$detekt_version"
input = files(
"$projectDir/app/src/main/java",
"$projectDir/core/src/main/java",
"$projectDir/mockserver/src/main/java",
"$projectDir/model/src/main/java",
"$projectDir/network/src/main/java"
)
config = files("$projectDir/detekt/detekt.yml")
baseline = file("$projectDir/detekt/baseline.xml")
reports {
txt {
enabled = true
destination = file("build/reports/detekt.txt")
}
html {
enabled = true
destination = file("build/reports/detekt.html")
}
}
}
allprojects {
repositories {
mavenCentral()
google()
jcenter()
}
}
subprojects {
apply plugin: "org.jlleitschuh.gradle.ktlint"
}
task clean(type: Delete) {
delete rootProject.buildDir
}
@ -70,4 +39,8 @@ task androidTests(dependsOn: "app:connectedAndroidTest"){
description = 'Run all Android tests'
}
apply from: 'gradlescripts/versions.gradle'
apply from: 'gradlescripts/versions.gradle'
apply from: 'gradlescripts/detekt.config.gradle'
apply from: 'gradlescripts/ktlint.gradle'
apply from: 'gradlescripts/lint.gradle'
apply from: 'gradlescripts/testoptions.gradle'

View file

@ -8,23 +8,19 @@ java {
targetCompatibility = JavaVersion.VERSION_1_8
}
test {
useJUnitPlatform()
testLogging {
events 'started', 'passed', 'skipped', 'failed'
exceptionFormat "full"
showStandardStreams true
compileKotlin {
kotlinOptions {
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
}
dependencies {
api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
api project(":model")
implementation project(":network")
testImplementation "org.koin:koin-test:$koin_version"
testImplementation "io.insert-koin:koin-test-junit5:$koin_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
testImplementation "org.mockito.kotlin:mockito-kotlin:$testing_kotlin_mockito_version"
testImplementation "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version"

View file

@ -1,5 +1,6 @@
package org.fnives.test.showcase.core.content
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
@ -24,6 +25,8 @@ internal class ContentRepository(private val contentRemoteSource: ContentRemoteS
}
emit(response)
}
@OptIn(ExperimentalCoroutinesApi::class)
val contents: Flow<Resource<List<Content>>> = mutableContentFlow.flatMapLatest {
if (it.item != null) flowOf(Resource.Success(it.item)) else requestFlow
}

View file

@ -14,7 +14,10 @@ class GetAllContentUseCase internal constructor(
) {
fun get(): Flow<Resource<List<FavouriteContent>>> =
contentRepository.contents.combine(favouriteContentLocalStorage.observeFavourites(), ::combineContentWithFavourites)
contentRepository.contents.combine(
favouriteContentLocalStorage.observeFavourites(),
::combineContentWithFavourites
)
companion object {
private fun combineContentWithFavourites(
@ -24,10 +27,18 @@ class GetAllContentUseCase internal constructor(
when (contentResource) {
is Resource.Error -> Resource.Error(contentResource.error)
is Resource.Loading -> Resource.Loading()
is Resource.Success -> Resource.Success(combineContentWithFavourites(contentResource.data, favouriteContents))
is Resource.Success ->
Resource.Success(
combineContentWithFavourites(contentResource.data, favouriteContents)
)
}
private fun combineContentWithFavourites(content: List<Content>, favourite: List<ContentId>): List<FavouriteContent> =
content.map { FavouriteContent(content = it, isFavourite = favourite.contains(it.id)) }
private fun combineContentWithFavourites(
content: List<Content>,
favourite: List<ContentId>
): List<FavouriteContent> =
content.map {
FavouriteContent(content = it, isFavourite = favourite.contains(it.id))
}
}
}

View file

@ -37,7 +37,7 @@ fun createCoreModule(
.plus(repositoryModule())
fun repositoryModule() = module {
single(override = true) { ContentRepository(get()) }
single { ContentRepository(get()) }
}
fun useCaseModule() = module {

View file

@ -6,6 +6,7 @@ import org.fnives.test.showcase.model.shared.Resource
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
@Suppress("RethrowCaughtException")
internal suspend fun <T> wrapIntoAnswer(callback: suspend () -> T): Answer<T> =
try {
Answer.Success(callback())

View file

@ -1,77 +1,50 @@
package org.fnives.test.showcase.core.content
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest
import org.fnives.test.showcase.core.shared.UnexpectedException
import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.ImageUrl
import org.fnives.test.showcase.model.shared.Resource
import org.fnives.test.showcase.network.content.ContentRemoteSource
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doSuspendableAnswer
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@Disabled("CodeKata")
class CodeKataContentRepositoryTest {
@BeforeEach
fun setUp() {
}
@DisplayName("GIVEN no interaction THEN remote source is not called")
@Test
fun fetchingIsLazy() {
}
@DisplayName("GIVEN content response WHEN content observed THEN loading AND data is returned")
@Test
fun happyFlow() = runBlockingTest {
}
@DisplayName("GIVEN content error WHEN content observed THEN loading AND data is returned")
@Test
fun errorFlow() = runBlockingTest {
}
@DisplayName("GIVEN saved cache WHEN collected THEN cache is returned")
@Test
fun verifyCaching() = runBlockingTest {
}
@DisplayName("GIVEN no response from remote source WHEN content observed THEN loading is returned")
@Test
fun loadingIsShownBeforeTheRequestIsReturned() = runBlockingTest {
}
@DisplayName("GIVEN content response THEN error WHEN fetched THEN returned states are loading data loading error")
@Test
fun whenFetchingRequestIsCalledAgain() = runBlockingTest {
}
@DisplayName("GIVEN content response THEN error WHEN fetched THEN only 4 items are emitted")
@Test
fun noAdditionalItemsEmitted() {
}
}
}

View file

@ -16,7 +16,15 @@ import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.kotlin.*
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doSuspendableAnswer
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
internal class ContentRepositoryTest {
@ -43,10 +51,19 @@ internal class ContentRepositoryTest {
@Test
fun happyFlow() = runBlockingTest {
val expected = listOf(
Resource.Loading(),
Resource.Success(listOf(Content(ContentId("a"), "", "", ImageUrl(""))))
Resource.Loading(),
Resource.Success(listOf(Content(ContentId("a"), "", "", ImageUrl(""))))
)
whenever(mockContentRemoteSource.get()).doReturn(
listOf(
Content(
ContentId("a"),
"",
"",
ImageUrl("")
)
)
)
whenever(mockContentRemoteSource.get()).doReturn(listOf(Content(ContentId("a"), "", "", ImageUrl(""))))
val actual = sut.contents.take(2).toList()
@ -58,8 +75,8 @@ internal class ContentRepositoryTest {
fun errorFlow() = runBlockingTest {
val exception = RuntimeException()
val expected = listOf(
Resource.Loading(),
Resource.Error<List<Content>>(UnexpectedException(exception))
Resource.Loading(),
Resource.Error<List<Content>>(UnexpectedException(exception))
)
whenever(mockContentRemoteSource.get()).doThrow(exception)
@ -101,27 +118,27 @@ internal class ContentRepositoryTest {
@DisplayName("GIVEN content response THEN error WHEN fetched THEN returned states are loading data loading error")
@Test
fun whenFetchingRequestIsCalledAgain() =
runBlockingTest(testDispatcher) {
val exception = RuntimeException()
val expected = listOf(
Resource.Loading(),
Resource.Success(emptyList()),
Resource.Loading(),
Resource.Error<List<Content>>(UnexpectedException(exception))
)
var first = true
whenever(mockContentRemoteSource.get()).doAnswer {
if (first) emptyList<Content>().also { first = false } else throw exception
}
val actual = async(testDispatcher) { sut.contents.take(4).toList() }
testDispatcher.advanceUntilIdle()
sut.fetch()
testDispatcher.advanceUntilIdle()
Assertions.assertEquals(expected, actual.await())
runBlockingTest(testDispatcher) {
val exception = RuntimeException()
val expected = listOf(
Resource.Loading(),
Resource.Success(emptyList()),
Resource.Loading(),
Resource.Error<List<Content>>(UnexpectedException(exception))
)
var first = true
whenever(mockContentRemoteSource.get()).doAnswer {
if (first) emptyList<Content>().also { first = false } else throw exception
}
val actual = async(testDispatcher) { sut.contents.take(4).toList() }
testDispatcher.advanceUntilIdle()
sut.fetch()
testDispatcher.advanceUntilIdle()
Assertions.assertEquals(expected, actual.await())
}
@DisplayName("GIVEN content response THEN error WHEN fetched THEN only 4 items are emitted")
@Test
fun noAdditionalItemsEmitted() {
@ -129,10 +146,10 @@ internal class ContentRepositoryTest {
runBlockingTest(testDispatcher) {
val exception = RuntimeException()
val expected = listOf(
Resource.Loading(),
Resource.Success(emptyList()),
Resource.Loading(),
Resource.Error<List<Content>>(UnexpectedException(exception))
Resource.Loading(),
Resource.Success(emptyList()),
Resource.Loading(),
Resource.Error<List<Content>>(UnexpectedException(exception))
)
var first = true
whenever(mockContentRemoteSource.get()).doAnswer {

View file

@ -1,5 +1,6 @@
package org.fnives.test.showcase.core.content
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.take
@ -20,6 +21,7 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
internal class GetAllContentUseCaseTest {
private lateinit var sut: GetAllContentUseCase
@ -36,71 +38,78 @@ internal class GetAllContentUseCaseTest {
mockContentRepository = mock()
favouriteContentIdFlow = MutableStateFlow(emptyList())
contentFlow = MutableStateFlow(Resource.Loading())
whenever(mockFavouriteContentLocalStorage.observeFavourites()).doReturn(favouriteContentIdFlow)
whenever(mockFavouriteContentLocalStorage.observeFavourites()).doReturn(
favouriteContentIdFlow
)
whenever(mockContentRepository.contents).doReturn(contentFlow)
sut = GetAllContentUseCase(mockContentRepository, mockFavouriteContentLocalStorage)
}
@Test
fun GIVEN_loading_AND_empty_favourite_WHEN_observed_THEN_loading_is_shown() = runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = emptyList()
contentFlow.value = Resource.Loading()
val expected = Resource.Loading<List<FavouriteContent>>()
fun GIVEN_loading_AND_empty_favourite_WHEN_observed_THEN_loading_is_shown() =
runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = emptyList()
contentFlow.value = Resource.Loading()
val expected = Resource.Loading<List<FavouriteContent>>()
val actual = sut.get().take(1).toList()
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
Assertions.assertEquals(listOf(expected), actual)
}
@Test
fun GIVEN_loading_AND_listOfFavourite_WHEN_observed_THEN_loading_is_shown() = runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = listOf(ContentId("a"))
contentFlow.value = Resource.Loading()
val expected = Resource.Loading<List<FavouriteContent>>()
fun GIVEN_loading_AND_listOfFavourite_WHEN_observed_THEN_loading_is_shown() =
runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = listOf(ContentId("a"))
contentFlow.value = Resource.Loading()
val expected = Resource.Loading<List<FavouriteContent>>()
val actual = sut.get().take(1).toList()
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
Assertions.assertEquals(listOf(expected), actual)
}
@Test
fun GIVEN_error_AND_empty_favourite_WHEN_observed_THEN_error_is_shown() = runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = emptyList()
val exception = Throwable()
contentFlow.value = Resource.Error(exception)
val expected = Resource.Error<List<FavouriteContent>>(exception)
fun GIVEN_error_AND_empty_favourite_WHEN_observed_THEN_error_is_shown() =
runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = emptyList()
val exception = Throwable()
contentFlow.value = Resource.Error(exception)
val expected = Resource.Error<List<FavouriteContent>>(exception)
val actual = sut.get().take(1).toList()
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
Assertions.assertEquals(listOf(expected), actual)
}
@Test
fun GIVEN_error_AND_listOfFavourite_WHEN_observed_THEN_error_is_shown() = runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = listOf(ContentId("b"))
val exception = Throwable()
contentFlow.value = Resource.Error(exception)
val expected = Resource.Error<List<FavouriteContent>>(exception)
fun GIVEN_error_AND_listOfFavourite_WHEN_observed_THEN_error_is_shown() =
runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = listOf(ContentId("b"))
val exception = Throwable()
contentFlow.value = Resource.Error(exception)
val expected = Resource.Error<List<FavouriteContent>>(exception)
val actual = sut.get().take(1).toList()
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
Assertions.assertEquals(listOf(expected), actual)
}
@Test
fun GIVEN_listOfContent_AND_empty_favourite_WHEN_observed_THEN_favourites_are_returned() = runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = emptyList()
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Success(listOf(content))
val items = listOf(
FavouriteContent(content, false)
)
val expected = Resource.Success(items)
fun GIVEN_listOfContent_AND_empty_favourite_WHEN_observed_THEN_favourites_are_returned() =
runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = emptyList()
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Success(listOf(content))
val items = listOf(
FavouriteContent(content, false)
)
val expected = Resource.Success(items)
val actual = sut.get().take(1).toList()
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
Assertions.assertEquals(listOf(expected), actual)
}
@Test
fun GIVEN_listOfContent_AND_other_favourite_id_WHEN_observed_THEN_favourites_are_returned() =

View file

@ -1,20 +1,11 @@
package org.fnives.test.showcase.core.login
import kotlinx.coroutines.test.runBlockingTest
import org.fnives.test.showcase.core.shared.UnexpectedException
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.session.Session
import org.fnives.test.showcase.model.shared.Answer
import org.fnives.test.showcase.network.auth.LoginRemoteSource
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.kotlin.*
@Disabled("CodeKata")
class CodeKataSecondLoginUseCaseTest {
@BeforeEach
@ -51,4 +42,4 @@ class CodeKataSecondLoginUseCaseTest {
fun invalidResponseResultsInErrorReturned() {
TODO()
}
}
}

View file

@ -13,8 +13,14 @@ import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.kotlin.*
import java.util.stream.Stream
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.verifyZeroInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
internal class LoginUseCaseTest {

View file

@ -1,10 +1,6 @@
package org.fnives.test.showcase.core.session
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.kotlin.*
import org.junit.jupiter.api.Disabled
class CodeKataFirstSessionExpirationAdapterTest {
}
@Disabled("CodeKata")
class CodeKataFirstSessionExpirationAdapterTest

View file

@ -1 +1 @@
#mock-maker-inline
mock-maker-inline

View file

@ -1,56 +1,79 @@
build:
maxIssues: 15
maxIssues: 0
excludeCorrectable: false
weights:
# complexity: 2
# LongParameterList: 1
# style: 1
# comments: 1
# complexity: 2
# LongParameterList: 1
# style: 1
# comments: 1
config:
validation: true
# when writing own rules with new properties, exclude the property path e.g.: "my_rule_set,.*>.*>[my_property]"
excludes: ""
warningsAsErrors: false
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
excludes: ''
processors:
active: true
exclude:
# - 'DetektProgressListener'
- 'DetektProgressListener'
# - 'KtFileCountProcessor'
# - 'PackageCountProcessor'
# - 'ClassCountProcessor'
# - 'FunctionCountProcessor'
# - 'PropertyCountProcessor'
# - 'ClassCountProcessor'
# - 'PackageCountProcessor'
# - 'KtFileCountProcessor'
# - 'ProjectComplexityProcessor'
# - 'ProjectCognitiveComplexityProcessor'
# - 'ProjectLLOCProcessor'
# - 'ProjectCLOCProcessor'
# - 'ProjectLOCProcessor'
# - 'ProjectSLOCProcessor'
# - 'LicenseHeaderLoaderExtension'
console-reports:
active: true
exclude:
# - 'ProjectStatisticsReport'
# - 'ComplexityReport'
# - 'NotificationReport'
# - 'FindingsReport'
- 'FileBasedFindingsReport'
# - 'BuildFailureReport'
- 'ProjectStatisticsReport'
- 'ComplexityReport'
- 'NotificationReport'
# - 'FindingsReport'
- 'FileBasedFindingsReport'
output-reports:
active: true
exclude:
# - 'TxtOutputReport'
# - 'XmlOutputReport'
# - 'HtmlOutputReport'
comments:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
AbsentOrWrongFileLicense:
active: false
licenseTemplateFile: 'license.template'
licenseTemplateIsRegex: false
CommentOverPrivateFunction:
active: false
CommentOverPrivateProperty:
active: false
DeprecatedBlockTag:
active: false
EndOfSentenceFormat:
active: false
endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$)
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
UndocumentedPublicClass:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
searchInNestedClass: true
searchInInnerClass: true
searchInInnerObject: true
searchInInnerInterface: true
UndocumentedPublicFunction:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
UndocumentedPublicProperty:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
complexity:
active: true
@ -61,57 +84,86 @@ complexity:
active: false
threshold: 10
includeStaticDeclarations: false
includePrivateDeclarations: false
ComplexMethod:
active: true
threshold: 25
threshold: 15
ignoreSingleWhenExpression: false
ignoreSimpleWhenEntries: false
ignoreNestingFunctions: false
nestingFunctions: run,let,apply,with,also,use,forEach,isNotNull,ifNull
nestingFunctions:
- 'also'
- 'apply'
- 'forEach'
- 'isNotNull'
- 'ifNull'
- 'let'
- 'run'
- 'use'
- 'with'
LabeledExpression:
active: false
ignoredLabels: ""
ignoredLabels: []
LargeClass:
active: true
threshold: 600
LongMethod:
active: true
threshold: 200
threshold: 60
ignoreAnnotated: []
LongParameterList:
active: true
functionThreshold: 6
constructorThreshold: 6
constructorThreshold: 7
ignoreDefaultParameters: false
ignoreDataClasses: true
ignoreAnnotated: []
MethodOverloading:
active: false
threshold: 6
NamedArguments:
active: false
threshold: 3
NestedBlockDepth:
active: true
threshold: 100
threshold: 4
ReplaceSafeCallChainWithRun:
active: false
StringLiteralDuplication:
active: false
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
threshold: 3
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^'
TooManyFunctions:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
thresholdInFiles: 25
thresholdInClasses: 25
thresholdInInterfaces: 25
thresholdInObjects: 25
thresholdInEnums: 25
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
thresholdInFiles: 11
thresholdInClasses: 11
thresholdInInterfaces: 11
thresholdInObjects: 11
thresholdInEnums: 11
ignoreDeprecated: false
ignorePrivate: false
ignoreOverridden: false
coroutines:
active: true
GlobalCoroutineUsage:
active: false
RedundantSuspendModifier:
active: false
SleepInsteadOfDelay:
active: false
SuspendFunWithFlowReturnType:
active: false
empty-blocks:
active: true
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: "^(_|(ignore|expected).*)"
allowedExceptionNameRegex: '_|(ignore|expected).*'
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
@ -135,6 +187,8 @@ empty-blocks:
active: true
EmptySecondaryConstructor:
active: true
EmptyTryBlock:
active: true
EmptyWhenBlock:
active: true
EmptyWhileBlock:
@ -143,53 +197,73 @@ empty-blocks:
exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: false
methodNames: 'toString,hashCode,equals,finalize'
active: true
methodNames:
- 'equals'
- 'finalize'
- 'hashCode'
- 'toString'
InstanceOfCheckForException:
active: false
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
NotImplementedDeclaration:
active: false
ObjectExtendsThrowable:
active: false
PrintStackTrace:
active: false
active: true
RethrowCaughtException:
active: false
active: true
ReturnFromFinally:
active: false
active: true
ignoreLabeled: false
SwallowedException:
active: false
ignoredExceptionTypes: 'InterruptedException,NumberFormatException,ParseException,MalformedURLException'
allowedExceptionNameRegex: "^(_|(ignore|expected).*)"
active: true
ignoredExceptionTypes:
- 'InterruptedException'
- 'MalformedURLException'
- 'NumberFormatException'
- 'ParseException'
allowedExceptionNameRegex: '_|(ignore|expected).*'
ThrowingExceptionFromFinally:
active: false
active: true
ThrowingExceptionInMain:
active: false
ThrowingExceptionsWithoutMessageOrCause:
active: true
exceptions: 'IllegalArgumentException,IllegalStateException,IOException'
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
exceptions:
- 'ArrayIndexOutOfBoundsException'
- 'Exception'
- 'IllegalArgumentException'
- 'IllegalMonitorStateException'
- 'IllegalStateException'
- 'IndexOutOfBoundsException'
- 'NullPointerException'
- 'RuntimeException'
- 'Throwable'
ThrowingNewInstanceOfSameException:
active: true
TooGenericExceptionCaught:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
exceptionNames:
- ArrayIndexOutOfBoundsException
- Error
- Exception
- IllegalMonitorStateException
- NullPointerException
- IndexOutOfBoundsException
- RuntimeException
- Throwable
allowedExceptionNameRegex: "^(_|(ignore|expected).*)"
- 'ArrayIndexOutOfBoundsException'
- 'Error'
- 'Exception'
- 'IllegalMonitorStateException'
- 'IndexOutOfBoundsException'
- 'NullPointerException'
- 'RuntimeException'
- 'Throwable'
allowedExceptionNameRegex: '_|(ignore|expected).*'
TooGenericExceptionThrown:
active: true
exceptionNames:
- Error
- Exception
- Throwable
- RuntimeException
- 'Error'
- 'Exception'
- 'RuntimeException'
- 'Throwable'
formatting:
active: true
@ -198,28 +272,42 @@ formatting:
AnnotationOnSeparateLine:
active: false
autoCorrect: true
AnnotationSpacing:
active: false
autoCorrect: true
ArgumentListWrapping:
active: false
autoCorrect: true
indentSize: 4
maxLineLength: 120
ChainWrapping:
active: true
autoCorrect: true
CommentSpacing:
active: true
autoCorrect: true
EnumEntryNameCase:
active: false
autoCorrect: true
Filename:
active: true
FinalNewline:
active: true
autoCorrect: true
insertFinalNewLine: true
ImportOrdering:
active: true
active: false
autoCorrect: true
layout: '*,java.**,javax.**,kotlin.**,^'
Indentation:
active: true
active: false
autoCorrect: true
indentSize: 4
continuationIndentSize: 4
MaximumLineLength:
active: true
maxLineLength: 250
maxLineLength: 120
ignoreBackTickedIdentifier: false
ModifierOrdering:
active: true
autoCorrect: true
@ -235,6 +323,9 @@ formatting:
NoEmptyClassBody:
active: true
autoCorrect: true
NoEmptyFirstLineInMethodBlock:
active: false
autoCorrect: true
NoLineBreakAfterElse:
active: true
autoCorrect: true
@ -265,6 +356,10 @@ formatting:
active: true
autoCorrect: true
indentSize: 4
maxLineLength: 120
SpacingAroundAngleBrackets:
active: false
autoCorrect: true
SpacingAroundColon:
active: true
autoCorrect: true
@ -277,6 +372,9 @@ formatting:
SpacingAroundDot:
active: true
autoCorrect: true
SpacingAroundDoubleColon:
active: false
autoCorrect: true
SpacingAroundKeyword:
active: true
autoCorrect: true
@ -289,85 +387,108 @@ formatting:
SpacingAroundRangeOperator:
active: true
autoCorrect: true
SpacingAroundUnaryOperator:
active: false
autoCorrect: true
SpacingBetweenDeclarationsWithAnnotations:
active: false
autoCorrect: true
SpacingBetweenDeclarationsWithComments:
active: false
autoCorrect: true
StringTemplate:
active: true
autoCorrect: true
naming:
active: true
BooleanPropertyNaming:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
allowedPattern: '^(is|has|are)'
ClassNaming:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
classPattern: '[A-Z$][a-zA-Z0-9$]*'
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
classPattern: '[A-Z][a-zA-Z0-9]*'
ConstructorParameterNaming:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
parameterPattern: '[a-z][A-Za-z0-9]*'
privateParameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
EnumNaming:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*'
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
ForbiddenClassName:
active: false
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
forbiddenName: ''
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
forbiddenName: []
FunctionMaxLength:
active: false
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
maximumFunctionNameLength: 30
FunctionMinLength:
active: false
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
minimumFunctionNameLength: 3
FunctionNaming:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
excludeClassPattern: '$^'
ignoreOverridden: true
ignoreAnnotated:
- 'Composable'
FunctionParameterNaming:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
parameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
InvalidPackageDeclaration:
active: false
excludes: ['**/*.kts']
rootPackage: ''
MatchingDeclarationName:
active: true
mustBeFirst: true
MemberNameEqualsClassName:
active: true
ignoreOverridden: true
NoNameShadowing:
active: false
NonBooleanPropertyPrefixedWithIs:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
ObjectPropertyNaming:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
PackageNaming:
active: false
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$'
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
VariableMaxLength:
active: false
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
maximumVariableNameLength: 64
VariableMinLength:
active: false
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
minimumVariableNameLength: 1
VariableNaming:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
@ -379,29 +500,52 @@ performance:
active: true
ForEachOnRange:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
SpreadOperator:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
UnnecessaryTemporaryInstantiation:
active: true
potential-bugs:
active: true
AvoidReferentialEquality:
active: false
forbiddenTypePatterns:
- 'kotlin.String'
CastToNullableType:
active: false
Deprecation:
active: false
DontDowncastCollectionTypes:
active: false
DoubleMutabilityForCollection:
active: false
DuplicateCaseInWhenExpression:
active: true
EqualsAlwaysReturnsTrueOrFalse:
active: true
EqualsWithHashCodeExist:
active: true
ExitOutsideMain:
active: false
ExplicitGarbageCollectionCall:
active: true
HasPlatformType:
active: false
ImplicitDefaultLocale:
IgnoredReturnValue:
active: false
restrictToAnnotatedMethods: true
returnValueAnnotations:
- '*.CheckResult'
- '*.CheckReturnValue'
ignoreReturnValueAnnotations:
- '*.CanIgnoreReturnValue'
ImplicitDefaultLocale:
active: true
ImplicitUnitReturnType:
active: false
allowExplicitReturnType: true
InvalidRange:
active: true
IteratorHasNextCallsNextMethod:
@ -410,20 +554,33 @@ potential-bugs:
active: true
LateinitUsage:
active: false
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludeAnnotatedProperties: ""
ignoreOnClassesPattern: ""
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
excludeAnnotatedProperties: []
ignoreOnClassesPattern: ''
MapGetWithNotNullAssertionOperator:
active: false
MissingWhenCase:
active: true
allowElseExpression: true
NullableToStringCall:
active: false
RedundantElseInWhen:
active: true
UnconditionalJumpStatementInLoop:
active: false
UnnecessaryNotNullOperator:
active: true
UnnecessarySafeCall:
active: true
UnreachableCatchBlock:
active: false
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: true
UnsafeCast:
active: true
UnusedUnaryOperator:
active: false
UselessPostfixExpression:
active: false
@ -432,30 +589,51 @@ potential-bugs:
style:
active: true
ClassOrdering:
active: false
CollapsibleIfStatements:
active: true
active: false
DataClassContainsFunctions:
active: true
active: false
conversionFunctionPrefix: 'to'
DataClassShouldBeImmutable:
active: false
DestructuringDeclarationWithTooManyEntries:
active: false
maxDestructuringEntries: 3
EqualsNullCall:
active: true
EqualsOnSignatureLine:
active: false
ExplicitCollectionElementAccessMethod:
active: false
ExplicitItLambdaParameter:
active: false
ExpressionBodySyntax:
active: true
active: false
includeLineWrapping: false
ForbiddenComment:
active: true
values: 'TODO:,FIXME:,STOPSHIP:'
allowedPatterns: ""
values:
- 'FIXME:'
- 'STOPSHIP:'
- 'TODO:'
allowedPatterns: ''
ForbiddenImport:
active: false
imports: ''
forbiddenPatterns: ""
imports: []
forbiddenPatterns: ''
ForbiddenMethodCall:
active: false
methods:
- 'kotlin.io.print'
- 'kotlin.io.println'
ForbiddenPublicDataClass:
active: true
excludes: ['**']
ignorePackages:
- '*.internal'
- '*.internal.*'
ForbiddenVoid:
active: false
ignoreOverridden: false
@ -463,30 +641,44 @@ style:
FunctionOnlyReturningConstant:
active: true
ignoreOverridableFunction: true
excludedFunctions: 'describeContents'
excludeAnnotatedFunction: "dagger.Provides"
ignoreActualFunction: true
excludedFunctions: ''
excludeAnnotatedFunction:
- 'dagger.Provides'
LibraryCodeMustSpecifyReturnType:
active: true
excludes: ['**']
LibraryEntitiesShouldNotBePublic:
active: true
excludes: ['**']
LoopWithTooManyJumpStatements:
active: true
maxJumpCount: 1
MagicNumber:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
ignoreNumbers: '-1,0,1,2'
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
ignoreNumbers:
- '-1'
- '0'
- '1'
- '2'
ignoreHashCodeFunction: true
ignorePropertyDeclaration: false
ignoreLocalVariableDeclaration: false
ignoreConstantDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotation: false
ignoreNamedArgument: true
ignoreEnums: false
ignoreRanges: false
ignoreExtensionFunctions: true
MandatoryBracesIfStatements:
active: true
active: false
MandatoryBracesLoops:
active: false
MaxLineLength:
active: true
maxLineLength: 160 #default: 120
maxLineLength: 120
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
@ -494,12 +686,16 @@ style:
active: true
ModifierOrder:
active: true
NestedClassesVisibility:
MultilineLambdaItParameter:
active: false
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: true
NoTabs:
active: false
ObjectLiteralToLambda:
active: false
OptionalAbstractKeyword:
active: true
OptionalUnit:
@ -507,29 +703,32 @@ style:
OptionalWhenBraces:
active: false
PreferToOverPairSyntax:
active: true
active: false
ProtectedMemberInFinalClass:
active: true
RedundantExplicitType:
active: true
active: false
RedundantHigherOrderMapUsage:
active: false
RedundantVisibilityModifierRule:
active: false
ReturnCount:
active: true
max: 5
excludedFunctions: "equals"
excludedFunctions: 'equals'
excludeLabeled: false
excludeReturnFromLambda: true
excludeGuardClauses: false
SafeCast:
active: true
SerialVersionUIDInSerializableClass:
active: false
active: true
SpacingBetweenPackageAndImports:
active: false
ThrowsCount:
active: true
max: 10
max: 2
excludeGuardClauses: false
TrailingWhitespace:
active: false
UnderscoresInNumericLiterals:
@ -537,43 +736,62 @@ style:
acceptableDecimalLength: 5
UnnecessaryAbstractClass:
active: true
excludeAnnotatedClasses: "dagger.Module"
excludeAnnotatedClasses:
- 'dagger.Module'
UnnecessaryAnnotationUseSiteTarget:
active: false
UnnecessaryApply:
active: true
UnnecessaryFilter:
active: false
UnnecessaryInheritance:
active: true
UnnecessaryLet:
active: true
active: false
UnnecessaryParentheses:
active: true
active: false
UntilInsteadOfRangeTo:
active: false
UnusedImports:
active: true
active: false
UnusedPrivateClass:
active: true
UnusedPrivateMember:
active: true
allowedNames: "(_|ignored|expected|serialVersionUID)"
allowedNames: '(_|ignored|expected|serialVersionUID)'
UseArrayLiteralsInAnnotations:
active: false
UseCheckNotNull:
active: false
UseCheckOrError:
active: false
UseDataClass:
active: false
excludeAnnotatedClasses: ""
excludeAnnotatedClasses: []
allowVars: false
UseEmptyCounterpart:
active: false
UseIfEmptyOrIfBlank:
active: false
UseIfInsteadOfWhen:
active: false
UseIsNullOrEmpty:
active: false
UseOrEmpty:
active: false
UseRequire:
active: false
UseRequireNotNull:
active: false
UselessCallOnNotNull:
active: true
UtilityClassWithPublicConstructor:
active: true
VarCouldBeVal:
active: false
active: true
WildcardImport:
active: true
excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
excludeImports: 'java.util.*,kotlinx.android.synthetic.*'
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
excludeImports:
- 'java.util.*'
- 'kotlinx.android.synthetic.*'

View file

@ -17,5 +17,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
android.jetifier.ignorelist=bcprov-jdk15on
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip

View file

@ -0,0 +1,24 @@
detekt {
toolVersion = "$detekt_version"
source = files(
"$projectDir/app/src/main/java",
"$projectDir/core/src/main/java",
"$projectDir/mockserver/src/main/java",
"$projectDir/model/src/main/java",
"$projectDir/network/src/main/java"
)
config = files("$projectDir/detekt/detekt.yml")
baseline = file("$projectDir/detekt/baseline.xml")
reports {
txt {
enabled = true
destination = file("build/reports/detekt.txt")
}
html {
enabled = true
destination = file("build/reports/detekt.html")
}
}
}

View file

@ -0,0 +1,3 @@
subprojects {
apply plugin: "org.jlleitschuh.gradle.ktlint"
}

34
gradlescripts/lint.gradle Normal file
View file

@ -0,0 +1,34 @@
subprojects { module ->
plugins.withType(JavaPlugin).whenPluginAdded {
configure(module) {
apply plugin: "com.android.lint"
lintOptions {
warningsAsErrors true
abortOnError true
textReport true
ignore 'Overdraw'
textOutput "stdout"
}
}
}
plugins.withId("com.android.application") {
module.android.lintOptions {
warningsAsErrors true
abortOnError true
textReport true
ignore 'Overdraw'
textOutput "stdout"
}
}
plugins.withId("com.android.library") {
module.android.lintOptions {
warningsAsErrors true
abortOnError true
textReport true
ignore 'Overdraw'
textOutput "stdout"
}
}
}

View file

@ -0,0 +1,44 @@
subprojects { module ->
plugins.withType(JavaPlugin).whenPluginAdded {
module.test {
useJUnitPlatform()
testLogging {
events 'started', 'passed', 'skipped', 'failed'
exceptionFormat "full"
showStandardStreams true
}
}
}
plugins.withId("com.android.application") {
module.android.testOptions.unitTests.all {
useJUnitPlatform()
testLogging {
events 'started', 'passed', 'skipped', 'failed'
exceptionFormat "full"
showStandardStreams true
}
}
module.android.testOptions {
unitTests {
includeAndroidResources = true
}
}
}
plugins.withId("com.android.library") {
module.android.testOptions.unitTests.all {
useJUnitPlatform()
testLogging {
events 'started', 'passed', 'skipped', 'failed'
exceptionFormat "full"
showStandardStreams true
}
}
module.android.testOptions {
unitTests {
includeAndroidResources = true
}
}
}
}

View file

@ -1,28 +1,28 @@
project.ext {
androidx_core_version = "1.3.2"
androidx_appcompat_version = "1.2.0"
androidx_material_version = "1.3.0"
androidx_constraintlayout_version = "2.0.4"
androidx_core_version = "1.6.0"
androidx_appcompat_version = "1.3.1"
androidx_material_version = "1.4.0"
androidx_constraintlayout_version = "2.1.0"
androidx_livedata_version = "2.3.1"
androidx_swiperefreshlayout_version = "1.1.0"
androidx_room_version = "2.2.6"
androidx_room_version = "2.3.0"
coroutines_version = "1.4.3"
koin_version = "2.2.2"
koin_version = "3.1.2"
coil_version = "1.1.1"
retrofit_version = "2.9.0"
okhttp_version = "4.9.1"
moshi_version = "1.11.0"
moshi_version = "1.12.0"
testing_androidx_code_version = "1.3.0"
testing_androidx_junit_version = "1.1.2"
testing_androidx_code_version = "1.4.0"
testing_androidx_junit_version = "1.1.3"
testing_androidx_arch_core_version = "2.1.0"
testing_livedata_version = "1.1.2"
testing_livedata_version = "1.2.0"
testing_kotlin_mockito_version = "3.1.0"
testing_junit5_version = "5.7.0"
testing_json_assert_version = "1.5.0"
testing_junit4_version = "4.13.2"
testing_robolectric_version = "4.5.1"
testing_espresso_version = "3.3.0"
testing_robolectric_version = "4.6.1"
testing_espresso_version = "3.4.0"
testing_okhttp3_idling_resource_version = "1.0.0"
}

View file

@ -9,8 +9,6 @@ java {
}
dependencies {
api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
api project(":model")
api "com.squareup.okhttp3:mockwebserver:$okhttp_version"

View file

@ -70,7 +70,7 @@ class MockServerScenarioSetup internal constructor(
companion object {
const val PORT: Int = 7335
val HTTP_BASE_URL get() = "http://${InetAddress.getLocalHost().hostName}"
val HTTP_BASE_URL get() = "http://${InetAddress.getLocalHost().canonicalHostName}"
val HTTPS_BASE_URL get() = "https://localhost"
private fun MockWebServer.useHttps(): HandshakeCertificates {

View file

@ -6,14 +6,4 @@ plugins {
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
compileKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xinline-classes"]
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

View file

@ -1,3 +1,4 @@
package org.fnives.test.showcase.model.content
inline class ContentId(val id: String)
@JvmInline
value class ContentId(val id: String)

View file

@ -1,3 +1,4 @@
package org.fnives.test.showcase.model.content
inline class ImageUrl(val url: String)
@JvmInline
value class ImageUrl(val url: String)

View file

@ -1,3 +1,4 @@
package org.fnives.test.showcase.model.network
inline class BaseUrl(val baseUrl: String)
@JvmInline
value class BaseUrl(val baseUrl: String)

View file

@ -9,17 +9,7 @@ java {
targetCompatibility = JavaVersion.VERSION_1_8
}
test {
useJUnitPlatform()
testLogging {
events 'started', 'passed', 'skipped', 'failed'
exceptionFormat "full"
showStandardStreams true
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
@ -27,15 +17,15 @@ dependencies {
implementation "com.squareup.moshi:moshi:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
api "org.koin:koin-core:$koin_version"
api "io.insert-koin:koin-core:$koin_version"
api project(":model")
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
testImplementation "org.mockito.kotlin:mockito-kotlin:$testing_kotlin_mockito_version"
testImplementation "org.junit.jupiter:junit-jupiter-engine:5.7.0"
testImplementation "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version"
testImplementation project(':mockserver')
testImplementation "org.koin:koin-test:$koin_version"
testImplementation "io.insert-koin:koin-test-junit5:$koin_version"
testImplementation "org.skyscreamer:jsonassert:$testing_json_assert_version"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version"
}

View file

@ -50,7 +50,7 @@ private fun contentModule() = module {
private fun sessionlessNetworkingModule(baseUrl: BaseUrl, enableLogging: Boolean) = module {
factory { MoshiConverterFactory.create() }
single(qualifier = sessionless, override = true) {
single(qualifier = sessionless) {
OkHttpClient.Builder()
.addInterceptor(PlatformInterceptor())
.setupLogging(enableLogging)

View file

@ -14,11 +14,15 @@ internal class SessionAuthenticator(
private val networkSessionExpirationListener: NetworkSessionExpirationListener
) : Authenticator {
@Suppress("SwallowedException")
override fun authenticate(route: Route?, response: Response): Request? {
if (authenticationHeaderUtils.hasToken(response.request)) {
return runBlocking {
try {
val newSession = loginRemoteSource.refresh(networkSessionLocalStorage.session?.refreshToken.orEmpty())
val refreshToken = networkSessionLocalStorage.session
?.refreshToken
.orEmpty()
val newSession = loginRemoteSource.refresh(refreshToken)
networkSessionLocalStorage.session = newSession
return@runBlocking authenticationHeaderUtils.attachToken(response.request)
} catch (throwable: Throwable) {

View file

@ -8,6 +8,7 @@ import java.io.EOFException
internal object ExceptionWrapper {
@Suppress("RethrowCaughtException")
@Throws(NetworkException::class, ParsingException::class)
suspend fun <T> wrap(request: suspend () -> T) = try {
request()

View file

@ -1,79 +1,59 @@
package org.fnives.test.showcase.network.auth
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.model.session.Session
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
import org.fnives.test.showcase.network.di.createNetworkModules
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
import org.junit.jupiter.api.*
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.test.KoinTest
import org.koin.test.inject
import org.mockito.kotlin.mock
import org.skyscreamer.jsonassert.JSONAssert
import org.skyscreamer.jsonassert.JSONCompareMode
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import java.io.BufferedReader
import java.io.InputStreamReader
@Disabled("CodeKata")
class CodeKataLoginRemoteSourceTest {
@BeforeEach
fun setUp() {
}
@AfterEach
fun tearDown() {
}
@DisplayName("GIVEN successful response WHEN request is fired THEN login status success is returned")
@Test
fun successResponseIsParsedProperly() = runBlocking {
}
@DisplayName("GIVEN successful response WHEN request is fired THEN the request is setup properly")
@Test
fun requestProperlySetup() = runBlocking {
}
@DisplayName("GIVEN bad request response WHEN request is fired THEN login status invalid credentials is returned")
@Test
fun badRequestMeansInvalidCredentials() = runBlocking {
}
@DisplayName("GIVEN_internal_error_response_WHEN_request_is_fired_THEN_network_exception_is_thrown")
@Test
fun genericErrorMeansNetworkError() {
}
@DisplayName("GIVEN invalid json response WHEN request is fired THEN network exception is thrown")
@Test
fun invalidJsonMeansParsingException() {
}
@DisplayName("GIVEN malformed json response WHEN request is fired THEN network exception is thrown")
@Test
fun malformedJsonMeansParsingException() {
}
companion object {
internal fun Any.readResourceFile(filePath: String): String = try {
BufferedReader(InputStreamReader(this.javaClass.classLoader.getResourceAsStream(filePath)!!))
.readLines().joinToString("\n")
.readLines().joinToString("\n")
} catch (nullPointerException: NullPointerException) {
throw IllegalArgumentException("$filePath file not found!", nullPointerException)
}
@ -86,6 +66,5 @@ class CodeKataLoginRemoteSourceTest {
} while (true)
}
}
}
}
}

View file

@ -12,7 +12,11 @@ import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
import org.junit.jupiter.api.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
@ -26,6 +30,7 @@ import org.skyscreamer.jsonassert.JSONCompareMode
class LoginRemoteSourceTest : KoinTest {
private val sut by inject<LoginRemoteSource>()
@RegisterExtension
@JvmField
val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
@ -76,7 +81,11 @@ class LoginRemoteSourceTest : KoinTest {
Assertions.assertEquals(null, request.getHeader("Authorization"))
Assertions.assertEquals("/login", request.path)
val loginRequest = createExpectedLoginRequestJson("a", "b")
JSONAssert.assertEquals(loginRequest, request.body.readUtf8(), JSONCompareMode.NON_EXTENSIBLE)
JSONAssert.assertEquals(
loginRequest,
request.body.readUtf8(),
JSONCompareMode.NON_EXTENSIBLE
)
}
@DisplayName("GIVEN bad request response WHEN request is fired THEN login status invalid credentials is returned")

View file

@ -1,22 +1,23 @@
package org.fnives.test.showcase.network.content
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.model.session.Session
import org.fnives.test.showcase.network.auth.CodeKataLoginRemoteSourceTest.Companion.readResourceFile
import org.fnives.test.showcase.network.di.createNetworkModules
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
import org.junit.jupiter.api.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.test.KoinTest
import org.koin.test.inject
import org.mockito.kotlin.*
import org.mockito.kotlin.mock
@Disabled("CodeKata")
class CodeKataSessionExpirationTest : KoinTest {
private val sut by inject<ContentRemoteSource>()
@ -32,12 +33,12 @@ class CodeKataSessionExpirationTest : KoinTest {
mockNetworkSessionExpirationListener = mock()
startKoin {
modules(
createNetworkModules(
baseUrl = BaseUrl(mockWebServer.url("mockserver/").toString()),
enableLogging = true,
networkSessionExpirationListenerProvider = { mockNetworkSessionExpirationListener },
networkSessionLocalStorageProvider = { mockNetworkSessionLocalStorage }
).toList()
createNetworkModules(
baseUrl = BaseUrl(mockWebServer.url("mockserver/").toString()),
enableLogging = true,
networkSessionExpirationListenerProvider = { mockNetworkSessionExpirationListener },
networkSessionLocalStorageProvider = { mockNetworkSessionLocalStorage }
).toList()
)
}
}
@ -56,6 +57,5 @@ class CodeKataSessionExpirationTest : KoinTest {
@DisplayName("GIVEN 401 THEN failing refresh WHEN content requested THE error is returned and callback is Called")
@Test
fun failingRefreshResultsInSessionExpiration() = runBlocking {
}
}
}

View file

@ -11,7 +11,11 @@ import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
import org.junit.jupiter.api.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
@ -64,35 +68,39 @@ class SessionExpirationTest : KoinTest {
@DisplayName("GIVEN_401_THEN_refresh_token_ok_response_WHEN_content_requested_THE_tokens_are_refreshed_and_request_retried_with_new_tokens")
@Test
fun successRefreshResultsInRequestRetry() = runBlocking {
var sessionToReturnByMock: Session? = ContentData.loginSuccessResponse
mockServerScenarioSetup.setScenario(
ContentScenario.Unauthorized(false)
.then(ContentScenario.Success(true)),
false
)
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success, false)
whenever(mockNetworkSessionLocalStorage.session).doAnswer { sessionToReturnByMock }
doAnswer { sessionToReturnByMock = it.arguments[0] as Session? }
.whenever(mockNetworkSessionLocalStorage).session = anyOrNull()
var sessionToReturnByMock: Session? = ContentData.loginSuccessResponse
mockServerScenarioSetup.setScenario(
ContentScenario.Unauthorized(false)
.then(ContentScenario.Success(true)),
false
)
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success, false)
whenever(mockNetworkSessionLocalStorage.session).doAnswer { sessionToReturnByMock }
doAnswer { sessionToReturnByMock = it.arguments[0] as Session? }
.whenever(mockNetworkSessionLocalStorage).session = anyOrNull()
sut.get()
sut.get()
mockServerScenarioSetup.takeRequest()
val refreshRequest = mockServerScenarioSetup.takeRequest()
val retryAfterTokenRefreshRequest = mockServerScenarioSetup.takeRequest()
mockServerScenarioSetup.takeRequest()
val refreshRequest = mockServerScenarioSetup.takeRequest()
val retryAfterTokenRefreshRequest = mockServerScenarioSetup.takeRequest()
Assertions.assertEquals("PUT", refreshRequest.method)
Assertions.assertEquals("/login/${ContentData.loginSuccessResponse.refreshToken}", refreshRequest.path)
Assertions.assertEquals(null, refreshRequest.getHeader("Authorization"))
Assertions.assertEquals("Android", refreshRequest.getHeader("Platform"))
Assertions.assertEquals("", refreshRequest.body.readUtf8())
Assertions.assertEquals(
ContentData.refreshSuccessResponse.accessToken,
retryAfterTokenRefreshRequest.getHeader("Authorization")
)
verify(mockNetworkSessionLocalStorage, times(1)).session = ContentData.refreshSuccessResponse
verifyZeroInteractions(mockNetworkSessionExpirationListener)
}
Assertions.assertEquals("PUT", refreshRequest.method)
Assertions.assertEquals(
"/login/${ContentData.loginSuccessResponse.refreshToken}",
refreshRequest.path
)
Assertions.assertEquals(null, refreshRequest.getHeader("Authorization"))
Assertions.assertEquals("Android", refreshRequest.getHeader("Platform"))
Assertions.assertEquals("", refreshRequest.body.readUtf8())
Assertions.assertEquals(
ContentData.refreshSuccessResponse.accessToken,
retryAfterTokenRefreshRequest.getHeader("Authorization")
)
verify(mockNetworkSessionLocalStorage, times(1)).session =
ContentData.refreshSuccessResponse
verifyZeroInteractions(mockNetworkSessionExpirationListener)
}
@DisplayName("GIVEN 401 THEN failing refresh WHEN content requested THE error is returned and callback is Called")
@Test