diff --git a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/common/KatanaKoverPlugin.kt b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/common/KatanaKoverPlugin.kt index 83ce97149..d5bbaf29e 100644 --- a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/common/KatanaKoverPlugin.kt +++ b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/common/KatanaKoverPlugin.kt @@ -53,7 +53,7 @@ internal class KatanaKoverPlugin : Plugin { "*.data.type", // UI - "*.generated.resources", + "*.resources", "*.shared.navigation", "*.shared.resources", "*.shared.strings", diff --git a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/mobile/data/KatanaMultiplatformDataRemotePlugin.kt b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/mobile/data/KatanaMultiplatformDataRemotePlugin.kt index 24ba61b55..fca6a4c69 100644 --- a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/mobile/data/KatanaMultiplatformDataRemotePlugin.kt +++ b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/mobile/data/KatanaMultiplatformDataRemotePlugin.kt @@ -60,24 +60,32 @@ internal class KatanaMultiplatformDataRemotePlugin : Plugin { context(Project) private fun ApolloExtension.configureApollo() { service("anilist") { + decapitalizeFields = true generateAsInternal = true - generateDataBuilders = true + generateMethods = listOf("equalsHashCode") packageName = fullPackageName + warnOnDeprecatedUsages = true - if (fullPackageName.contains(BASE_PACKAGE)) { - alwaysGenerateTypesMatching = listOf("Query", "User") + if (path == CORE_PROJECT) { generateApolloMetadata = true generateAsInternal = false + generateDataBuilders = true + schemaFiles.from( + file("src/commonMain/graphql/schema.graphqls"), + file("src/commonMain/graphql/extra.graphqls"), + ) introspection { endpointUrl = "https://graphql.anilist.co" - schemaFile = project.file("src/commonMain/graphql/schema.graphqls") + schemaFile = file("src/commonMain/graphql/schema.graphqls") } + } else { + dependsOn(project(CORE_PROJECT)) } } } private companion object { - const val BASE_PACKAGE = ".remote" + const val CORE_PROJECT = ":core:remote" } } diff --git a/common/session/data/src/commonMain/kotlin/dev/alvr/katana/common/session/data/di/module.kt b/common/session/data/src/commonMain/kotlin/dev/alvr/katana/common/session/data/di/module.kt index 2ea72cfc7..1392f8dbc 100644 --- a/common/session/data/src/commonMain/kotlin/dev/alvr/katana/common/session/data/di/module.kt +++ b/common/session/data/src/commonMain/kotlin/dev/alvr/katana/common/session/data/di/module.kt @@ -12,11 +12,11 @@ import org.koin.dsl.module internal expect fun dataStoreModule(): Module private val repositoriesModule = module { - singleOf(::SessionRepositoryImpl).bind() + singleOf(::SessionRepositoryImpl) bind SessionRepository::class } private val sourcesModule = module { - singleOf(::SessionLocalSourceImpl).bind() + singleOf(::SessionLocalSourceImpl) bind SessionLocalSource::class } val commonSessionDataModule = module { diff --git a/common/session/data/src/commonTest/kotlin/dev/alvr/katana/common/session/data/datastore/SessionDataStoreTest.kt b/common/session/data/src/commonTest/kotlin/dev/alvr/katana/common/session/data/datastore/SessionDataStoreTest.kt index e5cbf62c3..bbd5e511e 100644 --- a/common/session/data/src/commonTest/kotlin/dev/alvr/katana/common/session/data/datastore/SessionDataStoreTest.kt +++ b/common/session/data/src/commonTest/kotlin/dev/alvr/katana/common/session/data/datastore/SessionDataStoreTest.kt @@ -12,7 +12,6 @@ import dev.alvr.katana.core.tests.koinExtension import io.kotest.core.spec.style.FreeSpec import io.kotest.core.test.TestCase import io.kotest.matchers.equals.shouldBeEqual -import kotlin.time.Duration.Companion.milliseconds import org.koin.test.KoinTest import org.koin.test.inject @@ -22,7 +21,7 @@ internal class SessionDataStoreTest : FreeSpec(), KoinTest { init { "initial session should equal to the Session class" { - dataStore.data.test(100.milliseconds) { + dataStore.data.test { awaitItem() shouldBeEqual Session() cancelAndConsumeRemainingEvents() } @@ -37,7 +36,7 @@ internal class SessionDataStoreTest : FreeSpec(), KoinTest { ) } - data.test(100.milliseconds) { + data.test { awaitItem() shouldBeEqual Session( anilistToken = AnilistToken("token"), isSessionActive = true, @@ -48,7 +47,7 @@ internal class SessionDataStoreTest : FreeSpec(), KoinTest { } "corrupted dataStore should recreate again the file with initial values" { - corruptedDataStore.data.test(100.milliseconds) { + corruptedDataStore.data.test { awaitItem() shouldBeEqual Session(anilistToken = AnilistToken("recreated")) cancelAndConsumeRemainingEvents() } diff --git a/common/session/data/src/commonTest/kotlin/dev/alvr/katana/common/session/data/repositories/SessionRepositoryTest.kt b/common/session/data/src/commonTest/kotlin/dev/alvr/katana/common/session/data/repositories/SessionRepositoryTest.kt index d56bf4b78..4a7d4fea6 100644 --- a/common/session/data/src/commonTest/kotlin/dev/alvr/katana/common/session/data/repositories/SessionRepositoryTest.kt +++ b/common/session/data/src/commonTest/kotlin/dev/alvr/katana/common/session/data/repositories/SessionRepositoryTest.kt @@ -20,7 +20,6 @@ import dev.mokkery.mock import dev.mokkery.verify import dev.mokkery.verifySuspend import io.kotest.core.spec.style.FreeSpec -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.flowOf internal class SessionRepositoryTest : FreeSpec() { @@ -37,7 +36,7 @@ internal class SessionRepositoryTest : FreeSpec() { false.right(), ) - repo.sessionActive.test(100.milliseconds) { + repo.sessionActive.test { awaitItem().shouldBeRight(true) awaitItem().shouldBeRight(true) awaitItem().shouldBeRight(false) diff --git a/common/session/data/src/commonTest/kotlin/dev/alvr/katana/common/session/data/sources/SessionLocalSourceTest.kt b/common/session/data/src/commonTest/kotlin/dev/alvr/katana/common/session/data/sources/SessionLocalSourceTest.kt index 58d0440b3..8190c0602 100644 --- a/common/session/data/src/commonTest/kotlin/dev/alvr/katana/common/session/data/sources/SessionLocalSourceTest.kt +++ b/common/session/data/src/commonTest/kotlin/dev/alvr/katana/common/session/data/sources/SessionLocalSourceTest.kt @@ -20,7 +20,6 @@ import dev.mokkery.mock import dev.mokkery.verify import dev.mokkery.verifySuspend import io.kotest.core.spec.style.FreeSpec -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.flowOf internal class SessionLocalSourceTest : FreeSpec() { @@ -80,7 +79,7 @@ internal class SessionLocalSourceTest : FreeSpec() { "checking session active for ${session.anilistToken} and ${session.isSessionActive}" { every { store.data } returns flowOf(session) - source.sessionActive.test(100.milliseconds) { + source.sessionActive.test { awaitItem().shouldBeRight((session.anilistToken == null && session.isSessionActive).not()) cancelAndIgnoreRemainingEvents() } diff --git a/common/session/domain/src/commonTest/kotlin/dev/alvr/katana/common/session/domain/usecases/ObserveActiveSessionUseCaseTest.kt b/common/session/domain/src/commonTest/kotlin/dev/alvr/katana/common/session/domain/usecases/ObserveActiveSessionUseCaseTest.kt index 5f513a275..9308177f6 100644 --- a/common/session/domain/src/commonTest/kotlin/dev/alvr/katana/common/session/domain/usecases/ObserveActiveSessionUseCaseTest.kt +++ b/common/session/domain/src/commonTest/kotlin/dev/alvr/katana/common/session/domain/usecases/ObserveActiveSessionUseCaseTest.kt @@ -17,7 +17,6 @@ import dev.mokkery.mock import dev.mokkery.verify import io.kotest.core.spec.style.FreeSpec import io.kotest.core.test.TestCase -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.flowOf import org.koin.test.KoinTest import org.koin.test.inject @@ -41,7 +40,7 @@ internal class ObserveActiveSessionUseCaseTest : FreeSpec(), KoinTest { useCase() - useCase.flow.test(100.milliseconds) { + useCase.flow.test { awaitItem().shouldBeRight(false) awaitItem().shouldBeRight(true) awaitItem().shouldBeRight(false) @@ -58,7 +57,7 @@ internal class ObserveActiveSessionUseCaseTest : FreeSpec(), KoinTest { useCase() - useCase.flow.test(100.milliseconds) { + useCase.flow.test { awaitItem().shouldBeLeft(SessionFailure.CheckingActiveSession) cancelAndConsumeRemainingEvents() } diff --git a/common/user/data/build.gradle.kts b/common/user/data/build.gradle.kts index 65993d49a..2d8eef1ae 100644 --- a/common/user/data/build.gradle.kts +++ b/common/user/data/build.gradle.kts @@ -2,10 +2,6 @@ plugins { id("katana.multiplatform.data.remote") } -dependencies { - apolloMetadata(projects.core.remote) -} - kotlin { sourceSets { commonMain.dependencies { diff --git a/common/user/data/src/commonMain/graphql/QueryUserId.graphql b/common/user/data/src/commonMain/graphql/QueryUserId.graphql index 98ed9b76a..68edc8b22 100644 --- a/common/user/data/src/commonMain/graphql/QueryUserId.graphql +++ b/common/user/data/src/commonMain/graphql/QueryUserId.graphql @@ -1,5 +1,5 @@ query UserIdQuery { - viewer: Viewer { - id + Viewer @nonnull { + id @nonnull } } diff --git a/common/user/data/src/commonMain/graphql/QueryUserInfo.graphql b/common/user/data/src/commonMain/graphql/QueryUserInfo.graphql index a4b4f9b39..a66905f8f 100644 --- a/common/user/data/src/commonMain/graphql/QueryUserInfo.graphql +++ b/common/user/data/src/commonMain/graphql/QueryUserInfo.graphql @@ -1,8 +1,8 @@ query UserInfoQuery { - user: Viewer { - name - avatar { - medium + Viewer @nonnull { + name @nonnull + avatar @nonnull { + large @nonnull } bannerImage } diff --git a/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/di/module.kt b/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/di/module.kt index 6b5a9269b..771a15a20 100644 --- a/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/di/module.kt +++ b/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/di/module.kt @@ -13,16 +13,16 @@ import org.koin.dsl.bind import org.koin.dsl.module private val managersModule = module { - singleOf(::UserIdManagerImpl).bind() + singleOf(::UserIdManagerImpl) bind UserIdManager::class } private val repositoriesModule = module { - singleOf(::UserRepositoryImpl).bind() + singleOf(::UserRepositoryImpl) bind UserRepository::class } private val sourcesModule = module { - singleOf(::UserIdRemoteSourceImpl).bind() - singleOf(::UserInfoRemoteSourceImpl).bind() + singleOf(::UserIdRemoteSourceImpl) bind UserIdRemoteSource::class + singleOf(::UserInfoRemoteSourceImpl) bind UserInfoRemoteSource::class } val commonUserDataModule = module { diff --git a/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/mappers/responses/userId.kt b/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/mappers/responses/userId.kt index ece7c64db..9ffae85e0 100644 --- a/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/mappers/responses/userId.kt +++ b/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/mappers/responses/userId.kt @@ -3,6 +3,6 @@ package dev.alvr.katana.common.user.data.mappers.responses import dev.alvr.katana.common.user.data.UserIdQuery import dev.alvr.katana.common.user.domain.models.UserId -internal operator fun UserIdQuery.Data?.invoke(): UserId = UserId( - id = checkNotNull(this?.viewer?.id) { "ViewerId is required." }, +internal operator fun UserIdQuery.Data.invoke(): UserId = UserId( + id = viewer.id, ) diff --git a/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/mappers/responses/userInfo.kt b/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/mappers/responses/userInfo.kt index dff0a97a0..8630bb0d7 100644 --- a/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/mappers/responses/userInfo.kt +++ b/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/mappers/responses/userInfo.kt @@ -3,8 +3,8 @@ package dev.alvr.katana.common.user.data.mappers.responses import dev.alvr.katana.common.user.data.UserInfoQuery import dev.alvr.katana.common.user.domain.models.UserInfo -internal operator fun UserInfoQuery.Data?.invoke() = UserInfo( - username = this?.user?.name.orEmpty(), - avatar = this?.user?.avatar?.medium.orEmpty(), - banner = this?.user?.bannerImage.orEmpty(), +internal operator fun UserInfoQuery.Data.invoke() = UserInfo( + username = viewer.name, + avatar = viewer.avatar.large, + banner = viewer.bannerImage, ) diff --git a/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/sources/id/UserIdRemoteSourceImpl.kt b/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/sources/id/UserIdRemoteSourceImpl.kt index d32f18818..1f667ebeb 100644 --- a/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/sources/id/UserIdRemoteSourceImpl.kt +++ b/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/sources/id/UserIdRemoteSourceImpl.kt @@ -9,6 +9,7 @@ import dev.alvr.katana.common.user.data.UserIdQuery import dev.alvr.katana.common.user.data.mappers.responses.invoke import dev.alvr.katana.common.user.domain.failures.UserFailure import dev.alvr.katana.core.common.catchUnit +import dev.alvr.katana.core.remote.executeOrThrow import dev.alvr.katana.core.remote.toFailure internal class UserIdRemoteSourceImpl( @@ -36,6 +37,6 @@ internal class UserIdRemoteSourceImpl( private suspend fun userIdHandler(policy: FetchPolicy) = client .query(UserIdQuery()) .fetchPolicy(policy) - .execute() - .data() + .executeOrThrow() + .dataAssertNoErrors() } diff --git a/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/sources/info/UserInfoRemoteSourceImpl.kt b/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/sources/info/UserInfoRemoteSourceImpl.kt index 9c3424008..212d2e97e 100644 --- a/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/sources/info/UserInfoRemoteSourceImpl.kt +++ b/common/user/data/src/commonMain/kotlin/dev/alvr/katana/common/user/data/sources/info/UserInfoRemoteSourceImpl.kt @@ -26,7 +26,7 @@ internal class UserInfoRemoteSourceImpl( client.query(UserInfoQuery()) .fetchPolicy(FetchPolicy.CacheAndNetwork) .watch() - .map { res -> res.data().right() as Either } + .map { res -> res.dataAssertNoErrors().right() as Either } .catch { error -> Logger.e(error) { "Was not possible to get the user info" } diff --git a/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/managers/ApolloUserIdManagerTest.kt b/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/managers/ApolloUserIdManagerTest.kt index c2640a27e..bf5ba081a 100644 --- a/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/managers/ApolloUserIdManagerTest.kt +++ b/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/managers/ApolloUserIdManagerTest.kt @@ -9,6 +9,7 @@ import com.apollographql.apollo3.cache.normalized.store import com.apollographql.apollo3.testing.QueueTestNetworkTransport import com.apollographql.apollo3.testing.enqueueTestResponse import dev.alvr.katana.common.user.data.UserIdQuery +import dev.alvr.katana.core.remote.executeOrThrow import dev.alvr.katana.core.remote.type.buildUser import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.booleans.shouldBeFalse @@ -25,13 +26,13 @@ internal class ApolloUserIdManagerTest : FreeSpec() { "retrieving the authenticated user" - { "the first time should make a HTTP request" { val query = UserIdQuery.Data { - this["viewer"] = buildUser { - this["id"] = 12345 + viewer = buildUser { + id = 12345 } } client.enqueueTestResponse(UserIdQuery(), query) - client.query(UserIdQuery()).execute() + client.query(UserIdQuery()).executeOrThrow() .also { res -> res.isFromCache.shouldBeFalse() } .data.shouldNotBeNull() .viewer.shouldNotBeNull() shouldBeEqual query.viewer.shouldNotBeNull() @@ -39,14 +40,14 @@ internal class ApolloUserIdManagerTest : FreeSpec() { "the second onwards it should be read from cache" { val query = UserIdQuery.Data { - this["viewer"] = buildUser { - this["id"] = 12345 + viewer = buildUser { + id = 12345 } } client.enqueueTestResponse(UserIdQuery(), query) - client.query(UserIdQuery()).execute() // Simulate HTTP request - client.query(UserIdQuery()).execute() // Next request is from cache + client.query(UserIdQuery()).executeOrThrow() // Simulate HTTP request + client.query(UserIdQuery()).executeOrThrow() // Next request is from cache .also { res -> res.isFromCache.shouldBeTrue() } .data.shouldNotBeNull() .viewer.shouldNotBeNull() shouldBeEqual query.viewer.shouldNotBeNull() @@ -55,21 +56,21 @@ internal class ApolloUserIdManagerTest : FreeSpec() { "clearing the database" { val query = UserIdQuery.Data { - this["viewer"] = buildUser { - this["id"] = 12345 + viewer = buildUser { + id = 12345 } } client.enqueueTestResponse(UserIdQuery(), query) - client.query(UserIdQuery()).execute() // Simulate HTTP request - client.query(UserIdQuery()).execute() // Next request is from cache + client.query(UserIdQuery()).executeOrThrow() // Simulate HTTP request + client.query(UserIdQuery()).executeOrThrow() // Next request is from cache .also { res -> res.isFromCache.shouldBeTrue() } .data.shouldNotBeNull() .viewer.shouldNotBeNull() shouldBeEqual query.viewer.shouldNotBeNull() store.clearAll() - client.query(UserIdQuery()).execute() // No cache, HTTP request + client.query(UserIdQuery()).executeOrThrow() // No cache, HTTP request .also { res -> res.isFromCache.shouldBeFalse() } .data.shouldNotBeNull() .viewer.shouldNotBeNull() shouldBeEqual query.viewer.shouldNotBeNull() diff --git a/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/mappers/responses/UserIdMapperTest.kt b/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/mappers/responses/UserIdMapperTest.kt index c7e3e84f8..3912a117e 100644 --- a/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/mappers/responses/UserIdMapperTest.kt +++ b/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/mappers/responses/UserIdMapperTest.kt @@ -2,26 +2,10 @@ package dev.alvr.katana.common.user.data.mappers.responses import dev.alvr.katana.common.user.data.UserIdQuery import io.kotest.assertions.throwables.shouldNotThrowExactlyUnit -import io.kotest.assertions.throwables.shouldThrowExactlyUnit import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.ints.shouldBeExactly -import io.kotest.matchers.throwable.shouldHaveMessage internal class UserIdMapperTest : FreeSpec({ - "null UserIdQuery Data" { - shouldThrowExactlyUnit { - val userId: UserIdQuery.Data? = null - userId() - } shouldHaveMessage "ViewerId is required." - } - - "UserIdQuery with null viewer" { - shouldThrowExactlyUnit { - val userId = UserIdQuery.Data(viewer = null) - userId() - } shouldHaveMessage "ViewerId is required." - } - "an UserIdQuery with a viewer should have same id" { shouldNotThrowExactlyUnit { UserIdQuery.Data(viewer = UserIdQuery.Viewer(37_384)) diff --git a/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/repositories/UserRepositoryTest.kt b/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/repositories/UserRepositoryTest.kt index 1cf801db0..46d2af12a 100644 --- a/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/repositories/UserRepositoryTest.kt +++ b/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/repositories/UserRepositoryTest.kt @@ -18,7 +18,6 @@ import dev.mokkery.mock import dev.mokkery.verify import dev.mokkery.verifySuspend import io.kotest.core.spec.style.FreeSpec -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.emptyFlow internal class UserRepositoryTest : FreeSpec() { @@ -68,7 +67,7 @@ internal class UserRepositoryTest : FreeSpec() { "observing userInfo" - { "the server returns no data" { every { userInfoSource.userInfo } returns emptyFlow() - repo.userInfo.test(100.milliseconds) { awaitComplete() } + repo.userInfo.test { awaitComplete() } verify { userInfoSource.userInfo } } } diff --git a/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/sources/id/UserIdRemoteSourceTest.kt b/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/sources/id/UserIdRemoteSourceTest.kt index 56d96368f..c0bfee06a 100644 --- a/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/sources/id/UserIdRemoteSourceTest.kt +++ b/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/sources/id/UserIdRemoteSourceTest.kt @@ -2,7 +2,9 @@ package dev.alvr.katana.common.user.data.sources.id import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.annotations.ApolloExperimental +import com.apollographql.apollo3.exception.JsonDataException import com.apollographql.apollo3.testing.QueueTestNetworkTransport +import com.apollographql.apollo3.testing.enqueueTestNetworkError import com.apollographql.apollo3.testing.enqueueTestResponse import dev.alvr.katana.common.user.data.UserIdQuery import dev.alvr.katana.common.user.domain.models.UserId @@ -10,6 +12,7 @@ import dev.alvr.katana.core.domain.failures.Failure import dev.alvr.katana.core.remote.type.buildUser import dev.alvr.katana.core.tests.shouldBeLeft import dev.alvr.katana.core.tests.shouldBeRight +import io.kotest.assertions.throwables.shouldThrowExactlyUnit import io.kotest.core.spec.style.FreeSpec @OptIn(ApolloExperimental::class) @@ -25,13 +28,15 @@ internal class UserIdRemoteSourceTest : FreeSpec() { } "the server returns an empty userId" { - val query = UserIdQuery.Data { this["viewer"] = null } - client.enqueueTestResponse(UserIdQuery(), query) - source.getUserId().shouldBeLeft(Failure.Unknown) + shouldThrowExactlyUnit { + val query = UserIdQuery.Data { this["Viewer"] = null } + client.enqueueTestResponse(UserIdQuery(), query) + source.getUserId() + } } "the server returns a valid id" { - val query = UserIdQuery.Data { this["viewer"] = buildUser { this["id"] = 37_384 } } + val query = UserIdQuery.Data { this["Viewer"] = buildUser { id = 37_384 } } client.enqueueTestResponse(UserIdQuery(), query) source.getUserId().shouldBeRight(UserId(37_384)) } @@ -39,10 +44,15 @@ internal class UserIdRemoteSourceTest : FreeSpec() { "saving" - { "is successful" { - val query = UserIdQuery.Data { this["viewer"] = buildUser { this["id"] = 37_384 } } + val query = UserIdQuery.Data { this["viewer"] = buildUser { id = 37_384 } } client.enqueueTestResponse(UserIdQuery(), query) source.saveUserId().shouldBeRight() } + + "is error" { + client.enqueueTestNetworkError() + source.saveUserId().shouldBeLeft() + } } } } diff --git a/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/sources/info/UserInfoRemoteSourceTest.kt b/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/sources/info/UserInfoRemoteSourceTest.kt index c21bfc8fe..d23437a99 100644 --- a/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/sources/info/UserInfoRemoteSourceTest.kt +++ b/common/user/data/src/commonTest/kotlin/dev/alvr/katana/common/user/data/sources/info/UserInfoRemoteSourceTest.kt @@ -9,13 +9,11 @@ import com.apollographql.apollo3.testing.registerTestResponse import dev.alvr.katana.common.user.data.UserInfoQuery import dev.alvr.katana.common.user.domain.failures.UserFailure import dev.alvr.katana.common.user.domain.models.UserInfo -import dev.alvr.katana.core.common.empty import dev.alvr.katana.core.remote.type.buildUser import dev.alvr.katana.core.remote.type.buildUserAvatar import dev.alvr.katana.core.tests.shouldBeLeft import dev.alvr.katana.core.tests.shouldBeRight import io.kotest.core.spec.style.FreeSpec -import kotlin.time.Duration.Companion.milliseconds @OptIn(ApolloExperimental::class) internal class UserInfoRemoteSourceTest : FreeSpec() { @@ -26,17 +24,8 @@ internal class UserInfoRemoteSourceTest : FreeSpec() { "observing the user info" - { "the server returns no data" { client.registerTestResponse(UserInfoQuery()) - source.userInfo.test(100.milliseconds) { - awaitItem().shouldBeRight(userInfoNoData) - cancelAndIgnoreRemainingEvents() - } - } - - "the server returns an empty user" { - val query = UserInfoQuery.Data { this["user"] = null } - client.registerTestResponse(UserInfoQuery(), query) - source.userInfo.test(100.milliseconds) { - awaitItem().shouldBeRight(userInfoNoData) + source.userInfo.test { + awaitItem().shouldBeLeft(UserFailure.GettingUserInfo) cancelAndIgnoreRemainingEvents() } } @@ -44,7 +33,7 @@ internal class UserInfoRemoteSourceTest : FreeSpec() { validUserInfoData.map { (query, userInfo) -> "the server returns valid user info ($query)" { client.registerTestResponse(UserInfoQuery(), query) - source.userInfo.test(100.milliseconds) { + source.userInfo.test { awaitItem().shouldBeRight(userInfo) cancelAndIgnoreRemainingEvents() } @@ -53,7 +42,7 @@ internal class UserInfoRemoteSourceTest : FreeSpec() { "there is a problem with the server" { client.registerTestNetworkError(UserInfoQuery()) - source.userInfo.test(100.milliseconds) { + source.userInfo.test { awaitItem().shouldBeLeft(UserFailure.GettingUserInfo) cancelAndIgnoreRemainingEvents() } @@ -66,34 +55,21 @@ internal class UserInfoRemoteSourceTest : FreeSpec() { const val AVATAR = "https://s4.anilist.co/file/anilistcdn/user/avatar/large/b37384-xJE9aA4X20Yr.png" const val BANNER = "https://s4.anilist.co/file/anilistcdn/user/banner/37384-jtds8dpQIGVG.jpg" - val userInfoNoData = UserInfo( - username = String.empty, - avatar = String.empty, - banner = String.empty, - ) - val validUserInfoData = listOf( UserInfoQuery.Data { - this["user"] = buildUser { - this["name"] = USER_NAME - this["avatar"] = buildUserAvatar { this["medium"] = AVATAR } - this["bannerImage"] = BANNER + this["Viewer"] = buildUser { + name = USER_NAME + avatar = buildUserAvatar { large = AVATAR } + bannerImage = BANNER } } to UserInfo(username = USER_NAME, avatar = AVATAR, banner = BANNER), UserInfoQuery.Data { - this["user"] = buildUser { - this["name"] = USER_NAME - this["avatar"] = buildUserAvatar { this["medium"] = null } - this["bannerImage"] = BANNER - } - } to UserInfo(username = USER_NAME, avatar = String.empty, banner = BANNER), - UserInfoQuery.Data { - this["user"] = buildUser { - this["name"] = USER_NAME - this["avatar"] = null - this["bannerImage"] = BANNER + this["Viewer"] = buildUser { + name = USER_NAME + avatar = buildUserAvatar { large = AVATAR } + bannerImage = null } - } to UserInfo(username = USER_NAME, avatar = String.empty, banner = BANNER), + } to UserInfo(username = USER_NAME, avatar = AVATAR, banner = null), ) } } diff --git a/common/user/domain/src/commonMain/kotlin/dev/alvr/katana/common/user/domain/models/UserInfo.kt b/common/user/domain/src/commonMain/kotlin/dev/alvr/katana/common/user/domain/models/UserInfo.kt index 946da083e..8112ccace 100644 --- a/common/user/domain/src/commonMain/kotlin/dev/alvr/katana/common/user/domain/models/UserInfo.kt +++ b/common/user/domain/src/commonMain/kotlin/dev/alvr/katana/common/user/domain/models/UserInfo.kt @@ -3,5 +3,5 @@ package dev.alvr.katana.common.user.domain.models data class UserInfo( val username: String, val avatar: String, - val banner: String, + val banner: String?, ) diff --git a/common/user/domain/src/commonTest/kotlin/dev/alvr/katana/common/user/domain/usecases/ObserveUserInfoUseCaseTest.kt b/common/user/domain/src/commonTest/kotlin/dev/alvr/katana/common/user/domain/usecases/ObserveUserInfoUseCaseTest.kt index c67a3c3e6..41b704fc4 100644 --- a/common/user/domain/src/commonTest/kotlin/dev/alvr/katana/common/user/domain/usecases/ObserveUserInfoUseCaseTest.kt +++ b/common/user/domain/src/commonTest/kotlin/dev/alvr/katana/common/user/domain/usecases/ObserveUserInfoUseCaseTest.kt @@ -18,7 +18,6 @@ import dev.mokkery.mock import dev.mokkery.verify import io.kotest.core.spec.style.FreeSpec import io.kotest.core.test.TestCase -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.flowOf import org.koin.test.KoinTest import org.koin.test.inject @@ -35,7 +34,7 @@ internal class ObserveUserInfoUseCaseTest : FreeSpec(), KoinTest { useCase() - useCase.flow.test(100.milliseconds) { + useCase.flow.test { awaitItem().shouldBeRight(userInfoMock) cancelAndConsumeRemainingEvents() } @@ -48,7 +47,7 @@ internal class ObserveUserInfoUseCaseTest : FreeSpec(), KoinTest { useCase() - useCase.flow.test(100.milliseconds) { + useCase.flow.test { awaitItem().shouldBeLeft(UserFailure.GettingUserInfo) cancelAndConsumeRemainingEvents() } diff --git a/core/preferences/src/androidMain/kotlin/dev/alvr/katana/core/preferences/di/module.android.kt b/core/preferences/src/androidMain/kotlin/dev/alvr/katana/core/preferences/di/module.android.kt index f3728644b..9a6f48f62 100644 --- a/core/preferences/src/androidMain/kotlin/dev/alvr/katana/core/preferences/di/module.android.kt +++ b/core/preferences/src/androidMain/kotlin/dev/alvr/katana/core/preferences/di/module.android.kt @@ -26,7 +26,7 @@ private val aeadModule = module { } private val securerModule = module { - factoryOf(::AndroidPreferencesEncrypt).bind() + factoryOf(::AndroidPreferencesEncrypt) bind PreferencesEncrypt::class } internal actual fun encryptionModule() = module { diff --git a/core/preferences/src/iosMain/kotlin/dev/alvr/katana/core/preferences/di/module.ios.kt b/core/preferences/src/iosMain/kotlin/dev/alvr/katana/core/preferences/di/module.ios.kt index 01ea55f37..36fb9bfe7 100644 --- a/core/preferences/src/iosMain/kotlin/dev/alvr/katana/core/preferences/di/module.ios.kt +++ b/core/preferences/src/iosMain/kotlin/dev/alvr/katana/core/preferences/di/module.ios.kt @@ -7,7 +7,7 @@ import org.koin.dsl.bind import org.koin.dsl.module private val securerModule = module { - factoryOf(::IosPreferencesEncrypt).bind() + factoryOf(::IosPreferencesEncrypt) bind PreferencesEncrypt::class } internal actual fun encryptionModule() = module { diff --git a/core/remote/src/commonMain/graphql/extra.graphqls b/core/remote/src/commonMain/graphql/extra.graphqls new file mode 100644 index 000000000..13685db55 --- /dev/null +++ b/core/remote/src/commonMain/graphql/extra.graphqls @@ -0,0 +1,4 @@ +extend schema @link( + url: "https://specs.apollo.dev/kotlin_labs/v0.2", + import: ["@nonnull"] +) diff --git a/core/remote/src/commonMain/kotlin/dev/alvr/katana/core/remote/di/module.kt b/core/remote/src/commonMain/kotlin/dev/alvr/katana/core/remote/di/module.kt index 2c008ee03..509601a74 100644 --- a/core/remote/src/commonMain/kotlin/dev/alvr/katana/core/remote/di/module.kt +++ b/core/remote/src/commonMain/kotlin/dev/alvr/katana/core/remote/di/module.kt @@ -28,9 +28,11 @@ import org.koin.dsl.module internal expect fun ApolloClient.Builder.sentryInterceptor(): ApolloClient.Builder -private val getAnilistTokenInterceptor = named("getAnilistTokenInterceptor") -private val deleteAnilistTokenInterceptor = named("deleteAnilistTokenInterceptor") -private val loggingInterceptor = named("loggingInterceptor") +private enum class Interceptor { + GET_TOKEN, + DELETE_TOKEN, + LOGGING, +} private val apolloClientModule = module { single { @@ -50,9 +52,9 @@ private val apolloClientModule = module { ApolloClient.Builder() .serverUrl(ANILIST_BASE_URL) - .addHttpInterceptor(get(getAnilistTokenInterceptor)) - .addHttpInterceptor(get(deleteAnilistTokenInterceptor)) - .addHttpInterceptor(get(loggingInterceptor)) + .addHttpInterceptor(get(named(Interceptor.GET_TOKEN))) + .addHttpInterceptor(get(named(Interceptor.DELETE_TOKEN))) + .addHttpInterceptor(get(named(Interceptor.LOGGING))) .sentryInterceptor() .fetchPolicy(FetchPolicy.CacheAndNetwork) .normalizedCache( @@ -69,10 +71,10 @@ private val apolloDatabaseModule: Module = module { } private val apolloInterceptorsModule = module { - single(getAnilistTokenInterceptor) { - object : HttpInterceptor { - val useCase = get() + single(named(Interceptor.GET_TOKEN)) { + val useCase = get() + object : HttpInterceptor { override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain) = request.newBuilder() .addHeader("Authorization", "Bearer ${useCase().getOrNull()?.token}") @@ -82,10 +84,10 @@ private val apolloInterceptorsModule = module { } } - single(deleteAnilistTokenInterceptor) { - object : HttpInterceptor { - val useCase = get() + single(named(Interceptor.DELETE_TOKEN)) { + val useCase = get() + object : HttpInterceptor { override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain) = chain.proceed(request).also { response -> if ( @@ -98,7 +100,7 @@ private val apolloInterceptorsModule = module { } } - single(loggingInterceptor) { + single(named(Interceptor.LOGGING)) { LoggingInterceptor( log = { Logger.i("ApolloLoggingInterceptor") { it } }, level = if (KatanaBuildConfig.DEBUG) { @@ -109,7 +111,7 @@ private val apolloInterceptorsModule = module { ) } - factoryOf(::ReloadInterceptor).bind() + factoryOf(::ReloadInterceptor) bind ApolloInterceptor::class } val coreRemoteModule = module { diff --git a/core/remote/src/commonMain/kotlin/dev/alvr/katana/core/remote/extensions.kt b/core/remote/src/commonMain/kotlin/dev/alvr/katana/core/remote/extensions.kt index 338f51fa2..cfa628296 100644 --- a/core/remote/src/commonMain/kotlin/dev/alvr/katana/core/remote/extensions.kt +++ b/core/remote/src/commonMain/kotlin/dev/alvr/katana/core/remote/extensions.kt @@ -1,18 +1,20 @@ package dev.alvr.katana.core.remote import arrow.core.Either +import arrow.core.Option +import com.apollographql.apollo3.ApolloCall +import com.apollographql.apollo3.api.Operation import com.apollographql.apollo3.api.Optional import com.apollographql.apollo3.exception.ApolloHttpException import com.apollographql.apollo3.exception.ApolloNetworkException -import com.apollographql.apollo3.exception.ApolloParseException import com.apollographql.apollo3.exception.CacheMissException +import com.apollographql.apollo3.exception.DefaultApolloException import com.apollographql.apollo3.exception.HttpCacheMissException import com.apollographql.apollo3.exception.JsonDataException import com.apollographql.apollo3.exception.JsonEncodingException +import com.apollographql.apollo3.exception.NoDataException import dev.alvr.katana.core.domain.failures.Failure -fun Either.optional() = Optional.presentIfNotNull(getOrNull()) - fun Throwable.toFailure( network: Failure = Failure.Unknown, response: Failure = Failure.Unknown, @@ -20,11 +22,27 @@ fun Throwable.toFailure( unknown: Failure = Failure.Unknown, ): Failure = when (this) { is ApolloHttpException, - is ApolloNetworkException -> network + is ApolloNetworkException, + is DefaultApolloException -> network + is CacheMissException, is HttpCacheMissException -> cache - is ApolloParseException, + is JsonDataException, - is JsonEncodingException -> response + is JsonEncodingException, + is NoDataException -> response + else -> unknown } + +suspend fun ApolloCall.executeOrThrow() = execute().also { response -> + response.exception?.let { throw it } +} + +val V?.optional get(): Optional = Optional.presentIfNotNull(this) + +val V.present get(): Optional = Optional.present(this) + +val Either<*, V>.optional get(): Optional = Optional.presentIfNotNull(getOrNull()) + +val Option.optional get(): Optional = Optional.presentIfNotNull(getOrNull()) diff --git a/features/account/data/build.gradle.kts b/features/account/data/build.gradle.kts index d387ef177..1d8ff7b73 100644 --- a/features/account/data/build.gradle.kts +++ b/features/account/data/build.gradle.kts @@ -2,10 +2,6 @@ plugins { id("katana.multiplatform.data.remote") } -dependencies { - apolloMetadata(projects.core.remote) -} - kotlin { sourceSets { commonMain.dependencies { diff --git a/features/account/ui/src/commonMain/kotlin/dev/alvr/katana/features/account/ui/entities/UserInfoUi.kt b/features/account/ui/src/commonMain/kotlin/dev/alvr/katana/features/account/ui/entities/UserInfoUi.kt index 8d087017c..4eb136000 100644 --- a/features/account/ui/src/commonMain/kotlin/dev/alvr/katana/features/account/ui/entities/UserInfoUi.kt +++ b/features/account/ui/src/commonMain/kotlin/dev/alvr/katana/features/account/ui/entities/UserInfoUi.kt @@ -5,5 +5,5 @@ import dev.alvr.katana.core.common.empty internal data class UserInfoUi( val username: String = String.empty, val avatar: String = String.empty, - val banner: String = String.empty, + val banner: String? = null, ) diff --git a/features/explore/data/build.gradle.kts b/features/explore/data/build.gradle.kts index 8ae0e182c..70609af06 100644 --- a/features/explore/data/build.gradle.kts +++ b/features/explore/data/build.gradle.kts @@ -2,10 +2,6 @@ plugins { id("katana.multiplatform.data.remote") } -dependencies { - apolloMetadata(projects.core.remote) -} - kotlin { sourceSets { commonMain.dependencies { diff --git a/features/lists/data/build.gradle.kts b/features/lists/data/build.gradle.kts index 6fa78589e..faa5c9692 100644 --- a/features/lists/data/build.gradle.kts +++ b/features/lists/data/build.gradle.kts @@ -2,10 +2,6 @@ plugins { id("katana.multiplatform.data.remote") } -dependencies { - apolloMetadata(projects.core.remote) -} - kotlin { sourceSets { commonMain.dependencies { diff --git a/features/lists/data/src/commonMain/graphql/FragmentMediaEntry.graphql b/features/lists/data/src/commonMain/graphql/FragmentMediaEntry.graphql index 336513d3b..1982b3d55 100644 --- a/features/lists/data/src/commonMain/graphql/FragmentMediaEntry.graphql +++ b/features/lists/data/src/commonMain/graphql/FragmentMediaEntry.graphql @@ -1,14 +1,14 @@ -fragment MediaEntry on Media { - id - title { - userPreferred +fragment mediaEntry on Media { + id @nonnull + title @nonnull { + userPreferred @nonnull } episodes chapters volumes format - coverImage { - large + coverImage @nonnull { + large @nonnull } nextAiringEpisode { airingAt diff --git a/features/lists/data/src/commonMain/graphql/FragmentMediaListEntry.graphql b/features/lists/data/src/commonMain/graphql/FragmentMediaListEntry.graphql index 16a9b1d20..471fa67f3 100644 --- a/features/lists/data/src/commonMain/graphql/FragmentMediaListEntry.graphql +++ b/features/lists/data/src/commonMain/graphql/FragmentMediaListEntry.graphql @@ -1,12 +1,13 @@ -fragment MediaListEntry on MediaList { - id - score +fragment mediaListEntry on MediaList { + id @nonnull + score @nonnull progress progressVolumes repeat private notes hiddenFromStatusLists + updatedAt startedAt { year month @@ -17,5 +18,7 @@ fragment MediaListEntry on MediaList { month day } - updatedAt + media @nonnull { + ...mediaEntry + } } diff --git a/features/lists/data/src/commonMain/graphql/MutationMediaListEntries.graphql b/features/lists/data/src/commonMain/graphql/MutationMediaListEntries.graphql index 92ec3fb53..f03ff0101 100644 --- a/features/lists/data/src/commonMain/graphql/MutationMediaListEntries.graphql +++ b/features/lists/data/src/commonMain/graphql/MutationMediaListEntries.graphql @@ -21,7 +21,7 @@ mutation MediaListEntries( hiddenFromStatusLists: $hiddenFromStatusLists, startedAt: $startedAt, completedAt: $completedAt, - ) { - ...MediaListEntry + ) @nonnull { + ...mediaListEntry } } diff --git a/features/lists/data/src/commonMain/graphql/QueryMediaListCollection.graphql b/features/lists/data/src/commonMain/graphql/QueryMediaListCollection.graphql index 3cc2a4d35..b21a824db 100644 --- a/features/lists/data/src/commonMain/graphql/QueryMediaListCollection.graphql +++ b/features/lists/data/src/commonMain/graphql/QueryMediaListCollection.graphql @@ -1,21 +1,18 @@ query MediaListCollection($user: Int, $type: MediaType!) { - collection: MediaListCollection(userId: $user, type: $type) @nonnull { + MediaListCollection(userId: $user, type: $type) @nonnull { lists @nonnull { + name @nonnull entries @nonnull { - ...MediaListEntry - media @nonnull { - ...MediaEntry - } + ...mediaListEntry } - name } - user { - mediaListOptions { - animeList { - sectionOrder + user @nonnull { + mediaListOptions @nonnull { + animeList @nonnull { + sectionOrder @nonnull } - mangaList { - sectionOrder + mangaList @nonnull { + sectionOrder @nonnull } } } diff --git a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/di/module.kt b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/di/module.kt index d278696e0..641edd357 100644 --- a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/di/module.kt +++ b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/di/module.kt @@ -13,13 +13,13 @@ import org.koin.dsl.bind import org.koin.dsl.module private val repositoriesModule = module { - singleOf(::ListsRepositoryImpl).bind() + singleOf(::ListsRepositoryImpl) bind ListsRepository::class } private val sourcesModule = module { - singleOf(::CommonListsRemoteSourceImpl).bind() - singleOf(::AnimeListsRemoteSourceImpl).bind() - singleOf(::MangaListsRemoteSourceImpl).bind() + singleOf(::CommonListsRemoteSourceImpl) bind CommonListsRemoteSource::class + singleOf(::AnimeListsRemoteSourceImpl) bind AnimeListsRemoteSource::class + singleOf(::MangaListsRemoteSourceImpl) bind MangaListsRemoteSource::class } val featuresListsDataModule = module { diff --git a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/fuzzyDateInput.kt b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/fuzzyDateInput.kt index 60626b1b0..94711817e 100644 --- a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/fuzzyDateInput.kt +++ b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/fuzzyDateInput.kt @@ -1,15 +1,11 @@ package dev.alvr.katana.features.lists.data.mappers.requests -import com.apollographql.apollo3.api.Optional +import dev.alvr.katana.core.remote.present import dev.alvr.katana.core.remote.type.FuzzyDateInput import korlibs.time.Date -internal fun Date?.toFuzzyDate() = FuzzyDateInput( - year = Optional.presentIfNotNull(this?.year), - month = Optional.presentIfNotNull(this?.month1), - day = Optional.presentIfNotNull(this?.day), +internal fun Date.toFuzzyDate() = FuzzyDateInput( + year = year.present, + month = month1.present, + day = day.present, ) - -internal fun FuzzyDateInput.takeIfValid() = takeIf { - year !is Optional.Absent && month !is Optional.Absent && day !is Optional.Absent -} diff --git a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/mediaList.kt b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/mediaList.kt index 6ca2e7d05..d2f7c9af8 100644 --- a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/mediaList.kt +++ b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/mediaList.kt @@ -1,18 +1,18 @@ package dev.alvr.katana.features.lists.data.mappers.requests -import com.apollographql.apollo3.api.Optional +import dev.alvr.katana.core.remote.optional import dev.alvr.katana.features.lists.data.MediaListEntriesMutation import dev.alvr.katana.features.lists.domain.models.lists.MediaList internal fun MediaList.toMutation() = MediaListEntriesMutation( id = id, - score = Optional.presentIfNotNull(score), - progress = Optional.presentIfNotNull(progress), - progressVolumes = Optional.presentIfNotNull(progressVolumes), - repeat = Optional.presentIfNotNull(repeat), - private = Optional.presentIfNotNull(private), - notes = Optional.presentIfNotNull(notes), - hiddenFromStatusLists = Optional.presentIfNotNull(hiddenFromStatusLists), - startedAt = Optional.presentIfNotNull(startedAt.toFuzzyDate().takeIfValid()), - completedAt = Optional.presentIfNotNull(completedAt.toFuzzyDate().takeIfValid()), + score = score.optional, + progress = progress.optional, + progressVolumes = progressVolumes.optional, + repeat = repeat.optional, + private = private.optional, + notes = notes.optional, + hiddenFromStatusLists = hiddenFromStatusLists.optional, + startedAt = startedAt?.toFuzzyDate().optional, + completedAt = completedAt?.toFuzzyDate().optional, ) diff --git a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/animeEntry.kt b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/animeEntry.kt index e28252c2a..ee6f8cb65 100644 --- a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/animeEntry.kt +++ b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/animeEntry.kt @@ -3,15 +3,13 @@ package dev.alvr.katana.features.lists.data.mappers.responses import dev.alvr.katana.features.lists.domain.models.entries.MediaEntry import dev.alvr.katana.features.lists.data.fragment.MediaEntry as MediaEntryFragment -internal fun MediaEntryFragment?.animeEntry() = let { entry -> - MediaEntry.Anime( - entry = mediaEntry(), - episodes = entry?.episodes, - nextEpisode = entry?.nextAiringEpisode?.let { next -> - MediaEntry.Anime.NextEpisode( - number = next.episode, - at = next.airingAt.toLocalDateTime(), - ) - }, - ) -} +internal fun MediaEntryFragment.animeEntry() = MediaEntry.Anime( + entry = mediaEntry(), + episodes = episodes, + nextEpisode = nextAiringEpisode?.let { next -> + MediaEntry.Anime.NextEpisode( + number = next.episode, + at = next.airingAt.toLocalDateTime(), + ) + }, +) diff --git a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/mangaEntry.kt b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/mangaEntry.kt index d126c77b7..1ea6474c1 100644 --- a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/mangaEntry.kt +++ b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/mangaEntry.kt @@ -3,10 +3,8 @@ package dev.alvr.katana.features.lists.data.mappers.responses import dev.alvr.katana.features.lists.domain.models.entries.MediaEntry import dev.alvr.katana.features.lists.data.fragment.MediaEntry as MediaEntryFragment -internal fun MediaEntryFragment?.mangaEntry() = let { entry -> - MediaEntry.Manga( - entry = mediaEntry(), - chapters = entry?.chapters, - volumes = entry?.volumes, - ) -} +internal fun MediaEntryFragment.mangaEntry() = MediaEntry.Manga( + entry = mediaEntry(), + chapters = chapters, + volumes = volumes, +) diff --git a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/mediaEntry.kt b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/mediaEntry.kt index 01f4bbd2e..7cc92e03d 100644 --- a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/mediaEntry.kt +++ b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/mediaEntry.kt @@ -1,16 +1,15 @@ package dev.alvr.katana.features.lists.data.mappers.responses -import dev.alvr.katana.core.common.orZero import dev.alvr.katana.core.remote.type.MediaFormat import dev.alvr.katana.features.lists.domain.models.entries.CommonMediaEntry import dev.alvr.katana.features.lists.data.fragment.MediaEntry as MediaEntryFragment -internal fun MediaEntryFragment?.mediaEntry() = let { entry -> +internal fun MediaEntryFragment.mediaEntry() = let { entry -> CommonMediaEntry( - id = entry?.id.orZero(), - title = entry?.title?.userPreferred.orEmpty(), - coverImage = entry?.coverImage?.large.orEmpty(), - format = entry?.format.toFormat(), + id = entry.id, + title = entry.title.userPreferred, + coverImage = entry.coverImage.large, + format = entry.format.toFormat(), ) } diff --git a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/mediaList.kt b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/mediaList.kt index eaec81eed..f06a90946 100644 --- a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/mediaList.kt +++ b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/mediaList.kt @@ -1,6 +1,7 @@ package dev.alvr.katana.features.lists.data.mappers.responses import dev.alvr.katana.core.common.orZero +import dev.alvr.katana.core.common.zero import dev.alvr.katana.core.remote.type.MediaType import dev.alvr.katana.features.lists.data.MediaListCollectionQuery import dev.alvr.katana.features.lists.domain.models.entries.MediaEntry @@ -9,40 +10,35 @@ import dev.alvr.katana.features.lists.domain.models.lists.MediaListEntry import dev.alvr.katana.features.lists.domain.models.lists.MediaListGroup import dev.alvr.katana.features.lists.data.fragment.MediaEntry as MediaEntryFragment -internal fun MediaListCollectionQuery.Data.mediaList(type: MediaType): List> = - collection.lists.asSequence().map { list -> - val entries = list?.entries.orEmpty().asSequence().mapNotNull { entry -> - entry?.toModel(type) - }.toList() - +internal operator fun MediaListCollectionQuery.Data.invoke(type: MediaType): List> = + mediaListCollection.listsFilterNotNull().map { list -> MediaListGroup( - name = list?.name.orEmpty(), - entries = entries, + name = list.name, + entries = list.entriesFilterNotNull().map { entry -> entry.toModel(type) }.toList(), ) - }.sortedBy { sortLists(type, it.name) }.toList() + }.sortedBy { sortLists(type, it.name) } @Suppress("UNCHECKED_CAST") private fun MediaListCollectionQuery.Entry.toModel(type: MediaType) = - mediaListEntry.let { entry -> - val list = MediaList( - id = entry.id, - score = entry.score.orZero(), - progress = entry.progress.orZero(), - progressVolumes = entry.progressVolumes, - repeat = entry.repeat.orZero(), - private = entry.private ?: false, - notes = entry.notes.orEmpty(), - hiddenFromStatusLists = entry.hiddenFromStatusLists ?: false, - startedAt = entry.startedAt?.let { date -> - dateMapper(date.day, date.month, date.year) - }, - completedAt = entry.completedAt?.let { date -> - dateMapper(date.day, date.month, date.year) - }, - updatedAt = entry.updatedAt?.toLocalDateTime(), - ) + with(mediaListEntry) { MediaListEntry( - list = list, + list = MediaList( + id = id, + score = score, + progress = progress.orZero(), + progressVolumes = progressVolumes, + repeat = repeat.orZero(), + private = private ?: false, + notes = notes.orEmpty(), + hiddenFromStatusLists = hiddenFromStatusLists ?: false, + startedAt = startedAt?.let { date -> + dateMapper(date.day, date.month, date.year) + }, + completedAt = completedAt?.let { date -> + dateMapper(date.day, date.month, date.year) + }, + updatedAt = updatedAt?.toLocalDateTime(), + ), entry = media.mediaEntry.toMedia(type) as T, ) } @@ -53,15 +49,12 @@ private fun MediaEntryFragment.toMedia(type: MediaType) = type.onMediaEntry( ) private fun MediaListCollectionQuery.Data.sortLists(type: MediaType, listName: String) = - with(collection.user?.mediaListOptions) { + with(mediaListCollection.user.mediaListOptions) { type.onMediaEntry( - anime = { this?.animeList?.sectionOrder.orEmpty().listPosition(listName) }, - manga = { this?.mangaList?.sectionOrder.orEmpty().listPosition(listName) }, + anime = { animeList.sectionOrder.listPosition(listName) }, + manga = { mangaList.sectionOrder.listPosition(listName) }, ) } -private fun List.listPosition(listName: String) = if (listName.isEmpty()) { - size -} else { - indexOf(listName) -} +private fun List.listPosition(listName: String) = + indexOf(listName).takeIf { it >= Int.zero } ?: size diff --git a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/sources/CommonListsRemoteSourceImpl.kt b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/sources/CommonListsRemoteSourceImpl.kt index 62c2bda61..d12bb418a 100644 --- a/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/sources/CommonListsRemoteSourceImpl.kt +++ b/features/lists/data/src/commonMain/kotlin/dev/alvr/katana/features/lists/data/sources/CommonListsRemoteSourceImpl.kt @@ -7,15 +7,17 @@ import co.touchlab.kermit.Logger import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.cache.normalized.fetchPolicyInterceptor import com.apollographql.apollo3.cache.normalized.watch +import com.apollographql.apollo3.exception.CacheMissException import com.apollographql.apollo3.interceptor.ApolloInterceptor import dev.alvr.katana.common.user.domain.managers.UserIdManager import dev.alvr.katana.core.common.catchUnit +import dev.alvr.katana.core.remote.executeOrThrow import dev.alvr.katana.core.remote.optional import dev.alvr.katana.core.remote.toFailure import dev.alvr.katana.core.remote.type.MediaType import dev.alvr.katana.features.lists.data.MediaListCollectionQuery import dev.alvr.katana.features.lists.data.mappers.requests.toMutation -import dev.alvr.katana.features.lists.data.mappers.responses.mediaList +import dev.alvr.katana.features.lists.data.mappers.responses.invoke import dev.alvr.katana.features.lists.domain.failures.ListsFailure import dev.alvr.katana.features.lists.domain.models.MediaCollection import dev.alvr.katana.features.lists.domain.models.entries.MediaEntry @@ -23,6 +25,7 @@ import dev.alvr.katana.features.lists.domain.models.lists.MediaList import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map @@ -32,7 +35,7 @@ internal class CommonListsRemoteSourceImpl( private val reloadInterceptor: ApolloInterceptor, ) : CommonListsRemoteSource { override suspend fun updateList(entry: MediaList) = Either.catchUnit { - client.mutation(entry.toMutation()).execute() + client.mutation(entry.toMutation()).executeOrThrow() }.mapLeft { error -> Logger.e(error) { "There was an error updating the entry" } @@ -44,12 +47,12 @@ internal class CommonListsRemoteSourceImpl( override fun getMediaCollection(type: MediaType) = flow { val response = client - .query(MediaListCollectionQuery(userId.getId().optional(), type)) + .query(MediaListCollectionQuery(userId.getId().optional, type)) .fetchPolicyInterceptor(reloadInterceptor) .watch() - .distinctUntilChanged() - .map { res -> MediaCollection(res.data?.mediaList(type).orEmpty()).right() } - .distinctUntilChanged() + .filterNot { it.exception is CacheMissException } + .distinctUntilChanged { old, new -> old.data == new.data } + .map { res -> MediaCollection(res.dataAssertNoErrors(type)).right() } .catch { error -> Logger.e(error) { "There was an error collecting the lists" } diff --git a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/FuzzyDateMapperTest.kt b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/FuzzyDateMapperTest.kt index 2e345f2db..f40284139 100644 --- a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/FuzzyDateMapperTest.kt +++ b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/FuzzyDateMapperTest.kt @@ -6,64 +6,31 @@ import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.equals.shouldBeEqual import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.property.Arb -import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.next import korlibs.time.Date -internal class FuzzyDateMapperTest : FreeSpec() { - private val possibleValues = listOf( - Optional.Present(Arb.int(min = 1, max = 12).next()), - Optional.Absent, - ) +internal class FuzzyDateMapperTest : FreeSpec({ + "a null Date" { + val date: Date? = null - init { - "a null Date" { - val date: Date? = null - - date.toFuzzyDate() shouldBeEqual FuzzyDateInput( - year = Optional.Absent, - month = Optional.Absent, - day = Optional.Absent, - ) - } - - @Suppress("RedundantNullableReturnType") - "a nullable Date with value" { - val date: Date? = Date(2022, 7, 20) - - date.toFuzzyDate() shouldBeEqual FuzzyDateInput( - year = Optional.Present(2022), - month = Optional.Present(7), - day = Optional.Present(20), - ) - } - - "a valid Date" { - Date(2022, 7, 20).toFuzzyDate() shouldBeEqual FuzzyDateInput( - year = Optional.Present(2022), - month = Optional.Present(7), - day = Optional.Present(20), - ) - } - - (possibleValues * possibleValues * possibleValues).forEach { (dayMonth, year) -> - val (day, month) = dayMonth - val (dayValue, monthValue, yearValue) = Triple(day.getOrNull(), month.getOrNull(), year.getOrNull()) + date?.toFuzzyDate().shouldBeNull() + } - "date $dayValue/$monthValue/$yearValue with optional values" { - val date = FuzzyDateInput(year, month, day) + @Suppress("RedundantNullableReturnType") + "a nullable Date with value" { + val date: Date? = Date(2022, 7, 20) - if (date.day is Optional.Present && date.month is Optional.Present && date.year is Optional.Present) { - date.takeIfValid().shouldNotBeNull() - } else { - date.takeIfValid().shouldBeNull() - } - } - } + date?.toFuzzyDate().shouldNotBeNull() shouldBeEqual FuzzyDateInput( + year = Optional.Present(2022), + month = Optional.Present(7), + day = Optional.Present(20), + ) } - private operator fun List.times(other: List) = flatMap { list -> - List(other.size) { list }.zip(other) + "a valid Date" { + Date(2022, 7, 20).toFuzzyDate().shouldNotBeNull() shouldBeEqual FuzzyDateInput( + year = Optional.Present(2022), + month = Optional.Present(7), + day = Optional.Present(20), + ) } -} +}) diff --git a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/MediaListMapperTest.kt b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/MediaListMapperTest.kt index 3743085c7..aba04589a 100644 --- a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/MediaListMapperTest.kt +++ b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/requests/MediaListMapperTest.kt @@ -3,6 +3,8 @@ package dev.alvr.katana.features.lists.data.mappers.requests import com.apollographql.apollo3.api.Optional import dev.alvr.katana.core.common.empty import dev.alvr.katana.core.common.zero +import dev.alvr.katana.core.remote.optional +import dev.alvr.katana.core.remote.present import dev.alvr.katana.core.remote.type.FuzzyDateInput import dev.alvr.katana.features.lists.data.MediaListEntriesMutation import dev.alvr.katana.features.lists.domain.models.lists.MediaList @@ -27,27 +29,23 @@ internal class MediaListMapperTest : FreeSpec({ updatedAt = DateTimeTz.nowLocal(), ).toMutation() shouldBeEqual MediaListEntriesMutation( id = Int.zero, - score = Optional.Present(Double.zero), - progress = Optional.Present(Int.zero), - progressVolumes = Optional.Present(Int.zero), - repeat = Optional.Present(Int.zero), - private = Optional.Present(false), - notes = Optional.Present(String.empty), - hiddenFromStatusLists = Optional.Present(false), - startedAt = Optional.Present( - FuzzyDateInput( - year = Optional.Present(2022), - month = Optional.Present(7), - day = Optional.Present(20), - ), - ), - completedAt = Optional.Present( - FuzzyDateInput( - year = Optional.Present(2022), - month = Optional.Present(7), - day = Optional.Present(20), - ), - ), + score = Double.zero.present, + progress = Int.zero.present, + progressVolumes = Int.zero.present, + repeat = Int.zero.present, + private = false.present, + notes = String.empty.present, + hiddenFromStatusLists = false.optional, + startedAt = FuzzyDateInput( + year = Optional.Present(2022), + month = Optional.Present(7), + day = Optional.Present(20), + ).present, + completedAt = FuzzyDateInput( + year = 2022.present, + month = 7.present, + day = 20.present, + ).present, ) } @@ -66,15 +64,15 @@ internal class MediaListMapperTest : FreeSpec({ updatedAt = null, ).toMutation() shouldBeEqual MediaListEntriesMutation( id = Int.zero, - score = Optional.Present(Double.zero), - progress = Optional.Present(Int.zero), - progressVolumes = Optional.Absent, - repeat = Optional.Present(Int.zero), - private = Optional.Present(false), - notes = Optional.Present(String.empty), - hiddenFromStatusLists = Optional.Present(false), - startedAt = Optional.Absent, - completedAt = Optional.Absent, + score = Double.zero.present, + progress = Int.zero.present, + progressVolumes = null.optional, + repeat = Int.zero.present, + private = false.present, + notes = String.empty.present, + hiddenFromStatusLists = false.present, + startedAt = null.optional, + completedAt = null.optional, ) } }) diff --git a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/AnimeEntryMapperTest.kt b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/AnimeEntryMapperTest.kt index b3dee0d3e..d05118c68 100644 --- a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/AnimeEntryMapperTest.kt +++ b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/AnimeEntryMapperTest.kt @@ -13,36 +13,15 @@ import korlibs.time.TimezoneOffset import dev.alvr.katana.features.lists.data.fragment.MediaEntry as MediaEntryFragment internal class AnimeEntryMapperTest : FreeSpec({ - "a null entry" { - val entry: MediaEntryFragment? = null - entry.animeEntry().also { result -> - result shouldBeEqual MediaEntry.Anime( - entry = CommonMediaEntry( - id = Int.zero, - title = String.empty, - coverImage = String.empty, - format = CommonMediaEntry.Format.UNKNOWN, - ), - episodes = null, - nextEpisode = null, - ) - result shouldBeEqual MediaEntry.Anime( - entry = entry.mediaEntry(), - episodes = null, - nextEpisode = null, - ) - } - } - "an entry with null values" { val entry = MediaEntryFragment( id = Int.zero, - title = null, + title = MediaEntryFragment.Title(String.empty), episodes = null, chapters = null, volumes = null, format = null, - coverImage = null, + coverImage = MediaEntryFragment.CoverImage(String.empty), nextAiringEpisode = null, ) entry.animeEntry().also { result -> @@ -67,12 +46,12 @@ internal class AnimeEntryMapperTest : FreeSpec({ "an entry with null values but data classes with null" { val entry = MediaEntryFragment( id = Int.zero, - title = MediaEntryFragment.Title(null), + title = MediaEntryFragment.Title(String.empty), episodes = null, chapters = null, volumes = null, format = null, - coverImage = MediaEntryFragment.CoverImage(null), + coverImage = MediaEntryFragment.CoverImage(String.empty), nextAiringEpisode = null, ) diff --git a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/MangaEntryMapperTest.kt b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/MangaEntryMapperTest.kt index 55913345e..446e628d9 100644 --- a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/MangaEntryMapperTest.kt +++ b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/MangaEntryMapperTest.kt @@ -10,36 +10,15 @@ import io.kotest.matchers.equals.shouldBeEqual import dev.alvr.katana.features.lists.data.fragment.MediaEntry as MediaEntryFragment internal class MangaEntryMapperTest : FreeSpec({ - "a null entry" { - val entry: MediaEntryFragment? = null - entry.mangaEntry().also { result -> - result shouldBeEqual MediaEntry.Manga( - entry = CommonMediaEntry( - id = Int.zero, - title = String.empty, - coverImage = String.empty, - format = CommonMediaEntry.Format.UNKNOWN, - ), - chapters = null, - volumes = null, - ) - result shouldBeEqual MediaEntry.Manga( - entry = entry.mediaEntry(), - chapters = null, - volumes = null, - ) - } - } - "an entry with null values" { val entry = MediaEntryFragment( id = Int.zero, - title = null, + title = MediaEntryFragment.Title(String.empty), episodes = null, chapters = null, volumes = null, format = null, - coverImage = null, + coverImage = MediaEntryFragment.CoverImage(String.empty), nextAiringEpisode = null, ) entry.mangaEntry().also { result -> @@ -64,12 +43,12 @@ internal class MangaEntryMapperTest : FreeSpec({ "an entry with null values but data classes with null" { val entry = MediaEntryFragment( id = Int.zero, - title = MediaEntryFragment.Title(null), + title = MediaEntryFragment.Title(String.empty), episodes = null, chapters = null, volumes = null, format = null, - coverImage = MediaEntryFragment.CoverImage(null), + coverImage = MediaEntryFragment.CoverImage(String.empty), nextAiringEpisode = null, ) diff --git a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/MediaEntryMapperTest.kt b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/MediaEntryMapperTest.kt index 4b602939c..017bcfddf 100644 --- a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/MediaEntryMapperTest.kt +++ b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/MediaEntryMapperTest.kt @@ -1,7 +1,9 @@ package dev.alvr.katana.features.lists.data.mappers.responses +import dev.alvr.katana.core.common.empty import dev.alvr.katana.core.common.zero import dev.alvr.katana.core.remote.type.MediaFormat +import dev.alvr.katana.features.lists.data.fragment.MediaEntry import dev.alvr.katana.features.lists.domain.models.entries.CommonMediaEntry import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.shouldBe @@ -9,17 +11,17 @@ import io.kotest.matchers.shouldNotBe import dev.alvr.katana.features.lists.data.fragment.MediaEntry as MediaEntryFragment internal class MediaEntryMapperTest : FreeSpec({ - MediaFormat.knownValues() + MediaFormat.knownEntries .forEach { format -> "MediaFormat $format should not be ${CommonMediaEntry.Format.UNKNOWN}" { MediaEntryFragment( id = Int.zero, - title = null, + title = MediaEntry.Title(String.empty), episodes = null, chapters = null, volumes = null, format = format, - coverImage = null, + coverImage = MediaEntry.CoverImage(String.empty), nextAiringEpisode = null, ).mediaEntry().format shouldNotBe CommonMediaEntry.Format.UNKNOWN } @@ -30,12 +32,12 @@ internal class MediaEntryMapperTest : FreeSpec({ "MediaFormat $format should be ${CommonMediaEntry.Format.UNKNOWN}" { MediaEntryFragment( id = Int.zero, - title = null, + title = MediaEntry.Title(String.empty), episodes = null, chapters = null, volumes = null, format = format, - coverImage = null, + coverImage = MediaEntry.CoverImage(String.empty), nextAiringEpisode = null, ).mediaEntry().format shouldBe CommonMediaEntry.Format.UNKNOWN } diff --git a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/MediaListMapperTest.kt b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/MediaListMapperTest.kt index 33d23a85a..a35423639 100644 --- a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/MediaListMapperTest.kt +++ b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mappers/responses/MediaListMapperTest.kt @@ -40,38 +40,33 @@ import korlibs.time.TimezoneOffset internal class MediaListMapperTest : FreeSpec({ "a null response from server" { val data: MediaListCollectionQuery.Data? = null - data?.mediaList(MediaType.ANIME).shouldBeNull() - data?.mediaList(MediaType.MANGA).shouldBeNull() + data?.invoke(MediaType.ANIME).shouldBeNull() + data?.invoke(MediaType.MANGA).shouldBeNull() } "a collection without lists" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = emptyList() - user = null + user = buildUser { } } }.run { - mediaList(MediaType.ANIME).shouldBeEmpty() - mediaList(MediaType.MANGA).shouldBeEmpty() + this(MediaType.ANIME).shouldBeEmpty() + this(MediaType.MANGA).shouldBeEmpty() } } "a list without name" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( - buildMediaListGroup { - name = null - entries = emptyList() - }, buildMediaListGroup { name = String.empty entries = emptyList() }, - null, ) user = buildUser { - this["mediaListOptions"] = buildMediaListOptions { + mediaListOptions = buildMediaListOptions { animeList = buildMediaListTypeOptions { sectionOrder = emptyList() } @@ -82,15 +77,15 @@ internal class MediaListMapperTest : FreeSpec({ } } }.run { - mediaList(MediaType.ANIME).forAll { list -> list.name.shouldBeEmpty() } - mediaList(MediaType.MANGA).forAll { list -> list.name.shouldBeEmpty() } + this(MediaType.ANIME).forAll { list -> list.name.shouldBeEmpty() } + this(MediaType.MANGA).forAll { list -> list.name.shouldBeEmpty() } } } "anime" - { "a collection of animes without entries" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Rewatching" @@ -110,7 +105,7 @@ internal class MediaListMapperTest : FreeSpec({ }, ) user = buildUser { - this["mediaListOptions"] = buildMediaListOptions { + mediaListOptions = buildMediaListOptions { animeList = buildMediaListTypeOptions { sectionOrder = listOf("Watching", "Rewatching", "Completed TV", "Paused") @@ -118,7 +113,7 @@ internal class MediaListMapperTest : FreeSpec({ } } } - }.mediaList(MediaType.ANIME).also { list -> + }.invoke(MediaType.ANIME).also { list -> list[0].name shouldBe "Watching" list[1].name shouldBe "Rewatching" list[2].name shouldBe "Completed TV" @@ -128,7 +123,7 @@ internal class MediaListMapperTest : FreeSpec({ "a collection of animes without entries and sectionOrder" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Rewatching" @@ -148,14 +143,14 @@ internal class MediaListMapperTest : FreeSpec({ }, ) user = buildUser { - this["mediaListOptions"] = buildMediaListOptions { + mediaListOptions = buildMediaListOptions { animeList = buildMediaListTypeOptions { - sectionOrder = null + sectionOrder = emptyList() } } } } - }.mediaList(MediaType.ANIME).also { list -> + }.invoke(MediaType.ANIME).also { list -> list[0].name shouldBe "Rewatching" list[1].name shouldBe "Watching" list[2].name shouldBe "Paused" @@ -165,7 +160,7 @@ internal class MediaListMapperTest : FreeSpec({ "a collection of animes without entries and mediaListOptions" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Rewatching" @@ -184,11 +179,9 @@ internal class MediaListMapperTest : FreeSpec({ entries = emptyList() }, ) - user = buildUser { - this["mediaListOptions"] = null - } + user = buildUser { mediaListOptions = buildMediaListOptions { } } } - }.mediaList(MediaType.ANIME).also { list -> + }.invoke(MediaType.ANIME).also { list -> list[0].name shouldBe "Rewatching" list[1].name shouldBe "Watching" list[2].name shouldBe "Paused" @@ -198,7 +191,7 @@ internal class MediaListMapperTest : FreeSpec({ "a collection of animes without entries and animeListSorting" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Rewatching" @@ -218,12 +211,12 @@ internal class MediaListMapperTest : FreeSpec({ }, ) user = buildUser { - this["mediaListOptions"] = buildMediaListOptions { - animeList = null + mediaListOptions = buildMediaListOptions { + animeList = buildMediaListTypeOptions { } } } } - }.mediaList(MediaType.ANIME).also { list -> + }.invoke(MediaType.ANIME).also { list -> list[0].name shouldBe "Rewatching" list[1].name shouldBe "Watching" list[2].name shouldBe "Paused" @@ -233,7 +226,7 @@ internal class MediaListMapperTest : FreeSpec({ "a collection of animes with valid entries" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Watching" @@ -278,7 +271,7 @@ internal class MediaListMapperTest : FreeSpec({ }, ) } - }.mediaList(MediaType.ANIME).forAll { list -> + }.invoke(MediaType.ANIME).forAll { list -> list.entries.forAll { entry -> with(entry.list) { id shouldBe 100 @@ -321,14 +314,14 @@ internal class MediaListMapperTest : FreeSpec({ "a collection of animes with invalid entries" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Watching" entries = listOf( buildMediaList { id = Int.zero - score = null + score = Double.zero progress = null progressVolumes = null repeat = null @@ -340,10 +333,10 @@ internal class MediaListMapperTest : FreeSpec({ updatedAt = null media = buildMedia { id = Int.zero - title = null + title = buildMediaTitle { userPreferred = String.empty } episodes = null format = null - coverImage = null + coverImage = buildMediaCoverImage { large = String.empty } nextAiringEpisode = null } }, @@ -351,7 +344,7 @@ internal class MediaListMapperTest : FreeSpec({ }, ) } - }.mediaList(MediaType.ANIME).forAll { list -> + }.invoke(MediaType.ANIME).forAll { list -> list.entries.forAll { entry -> with(entry.list) { id shouldBe Int.zero @@ -387,7 +380,7 @@ internal class MediaListMapperTest : FreeSpec({ "manga" - { "a collection of mangas without entries" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Rereading" @@ -407,15 +400,14 @@ internal class MediaListMapperTest : FreeSpec({ }, ) user = buildUser { - this["mediaListOptions"] = buildMediaListOptions { + mediaListOptions = buildMediaListOptions { mangaList = buildMediaListTypeOptions { - sectionOrder = - listOf("Reading", "Rereading", "Completed Novel", "Paused") + sectionOrder = listOf("Reading", "Rereading", "Completed Novel", "Paused") } } } } - }.mediaList(MediaType.MANGA).also { list -> + }.invoke(MediaType.MANGA).also { list -> list[0].name shouldBe "Reading" list[1].name shouldBe "Rereading" list[2].name shouldBe "Completed Novel" @@ -425,7 +417,7 @@ internal class MediaListMapperTest : FreeSpec({ "a collection of mangas without entries and sectionOrder" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Rereading" @@ -445,14 +437,14 @@ internal class MediaListMapperTest : FreeSpec({ }, ) user = buildUser { - this["mediaListOptions"] = buildMediaListOptions { + mediaListOptions = buildMediaListOptions { mangaList = buildMediaListTypeOptions { - sectionOrder = null + sectionOrder = emptyList() } } } } - }.mediaList(MediaType.MANGA).also { list -> + }.invoke(MediaType.MANGA).also { list -> list[0].name shouldBe "Rereading" list[1].name shouldBe "Reading" list[2].name shouldBe "Paused" @@ -462,7 +454,7 @@ internal class MediaListMapperTest : FreeSpec({ "a collection of mangas without entries and mediaListOptions" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Rereading" @@ -481,11 +473,9 @@ internal class MediaListMapperTest : FreeSpec({ entries = emptyList() }, ) - user = buildUser { - this["mediaListOptions"] = null - } + user = buildUser { mediaListOptions = buildMediaListOptions { } } } - }.mediaList(MediaType.MANGA).also { list -> + }.invoke(MediaType.MANGA).also { list -> list[0].name shouldBe "Rereading" list[1].name shouldBe "Reading" list[2].name shouldBe "Paused" @@ -495,7 +485,7 @@ internal class MediaListMapperTest : FreeSpec({ "a collection of mangas without entries and mangaListSorting" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Rereading" @@ -515,12 +505,12 @@ internal class MediaListMapperTest : FreeSpec({ }, ) user = buildUser { - this["mediaListOptions"] = buildMediaListOptions { - mangaList = null + mediaListOptions = buildMediaListOptions { + mangaList = buildMediaListTypeOptions { } } } } - }.mediaList(MediaType.MANGA).also { list -> + }.invoke(MediaType.MANGA).also { list -> list[0].name shouldBe "Rereading" list[1].name shouldBe "Reading" list[2].name shouldBe "Paused" @@ -530,7 +520,7 @@ internal class MediaListMapperTest : FreeSpec({ "a collection of mangas with valid entries" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Reading" @@ -573,7 +563,7 @@ internal class MediaListMapperTest : FreeSpec({ }, ) } - }.mediaList(MediaType.MANGA).forAll { list -> + }.invoke(MediaType.MANGA).forAll { list -> list.entries.forAll { entry -> with(entry.list) { id shouldBe 100 @@ -610,14 +600,14 @@ internal class MediaListMapperTest : FreeSpec({ "a collection of mangas with invalid entries" { MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Reading" entries = listOf( buildMediaList { id = Int.zero - score = null + score = Double.zero progress = null progressVolumes = null repeat = null @@ -629,11 +619,11 @@ internal class MediaListMapperTest : FreeSpec({ updatedAt = null media = buildMedia { id = Int.zero - title = null + title = buildMediaTitle { userPreferred = String.empty } chapters = null volumes = null format = null - coverImage = null + coverImage = buildMediaCoverImage { large = String.empty } nextAiringEpisode = null } }, @@ -641,7 +631,7 @@ internal class MediaListMapperTest : FreeSpec({ }, ) } - }.mediaList(MediaType.MANGA).forAll { list -> + }.invoke(MediaType.MANGA).forAll { list -> list.entries.forAll { entry -> with(entry.list) { id shouldBe Int.zero diff --git a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mocks.kt b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mocks.kt index 3a6c764d7..3d8cb8059 100644 --- a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mocks.kt +++ b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/mocks.kt @@ -1,8 +1,7 @@ package dev.alvr.katana.features.lists.data import com.apollographql.apollo3.api.Error -import com.apollographql.apollo3.api.Optional -import dev.alvr.katana.core.remote.type.FuzzyDateInput +import dev.alvr.katana.core.remote.optional import dev.alvr.katana.core.remote.type.MediaType import dev.alvr.katana.features.lists.domain.models.lists.MediaList import io.kotest.property.Arb @@ -19,22 +18,10 @@ import korlibs.time.Date import korlibs.time.DateTimeTz internal val mediaListCollectionQueryMock = MediaListCollectionQuery( + user = Arb.positiveInt().orNull().next().optional, type = Arb.enum().next(), ) -internal val mediaListEntriesMutationMock = MediaListEntriesMutation( - id = Arb.int().next(), - score = Optional.presentIfNotNull(Arb.positiveDouble().orNull().next()), - progress = Optional.presentIfNotNull(Arb.positiveInt().orNull().next()), - progressVolumes = Optional.presentIfNotNull(Arb.positiveInt().orNull().next()), - repeat = Optional.presentIfNotNull(Arb.positiveInt().orNull().next()), - private = Optional.presentIfNotNull(Arb.boolean().orNull().next()), - notes = Optional.presentIfNotNull(Arb.string().orNull().next()), - hiddenFromStatusLists = Optional.presentIfNotNull(Arb.boolean().orNull().next()), - startedAt = Optional.presentIfNotNull(Arb.fuzzyDate().orNull().next()), - completedAt = Optional.presentIfNotNull(Arb.fuzzyDate().orNull().next()), -) - internal val mediaListMock = MediaList( id = Arb.int().next(), score = Arb.positiveDouble().next(), @@ -49,13 +36,7 @@ internal val mediaListMock = MediaList( updatedAt = Arb.dateTimeTz().orNull().next(), ) -internal val apolloErrorMock = Error( - message = Arb.string().next(), - locations = emptyList(), - path = emptyList(), - extensions = emptyMap(), - nonStandardFields = emptyMap(), -) +internal val apolloErrorMock = Error.Builder(Arb.string().next()).build() private fun Arb.Companion.date() = arbitrary { Date( @@ -68,11 +49,3 @@ private fun Arb.Companion.date() = arbitrary { private fun Arb.Companion.dateTimeTz() = arbitrary { DateTimeTz.nowLocal() } - -private fun Arb.Companion.fuzzyDate() = arbitrary { - FuzzyDateInput( - year = Optional.presentIfNotNull(Arb.int().orNull().next()), - month = Optional.presentIfNotNull(Arb.int().orNull().next()), - day = Optional.presentIfNotNull(Arb.int().orNull().next()), - ) -} diff --git a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/repositories/ListsRepositoryTest.kt b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/repositories/ListsRepositoryTest.kt index 06d7028f9..c664aa1bd 100644 --- a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/repositories/ListsRepositoryTest.kt +++ b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/repositories/ListsRepositoryTest.kt @@ -22,7 +22,6 @@ import dev.mokkery.mock import dev.mokkery.verify import dev.mokkery.verifySuspend import io.kotest.core.spec.style.FreeSpec -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.flowOf internal class ListsRepositoryTest : FreeSpec() { @@ -37,7 +36,7 @@ internal class ListsRepositoryTest : FreeSpec() { val collection = MediaCollection(emptyList()) every { animeSource.animeCollection } returns flowOf(collection.right()) - repo.animeCollection.test(100.milliseconds) { + repo.animeCollection.test { awaitItem().shouldBeRight(collection) awaitComplete() } @@ -49,7 +48,7 @@ internal class ListsRepositoryTest : FreeSpec() { val collection = MediaCollection(emptyList()) every { mangaSource.mangaCollection } returns flowOf(collection.right()) - repo.mangaCollection.test(100.milliseconds) { + repo.mangaCollection.test { awaitItem().shouldBeRight(collection) awaitComplete() } @@ -73,17 +72,5 @@ internal class ListsRepositoryTest : FreeSpec() { verifySuspend { commonSource.updateList(mediaListMock) } } } - -// "failure updating the list" { -// everySuspend { commonSource.updateList(any()) } sequentially { -// returns(ListsFailure.UpdatingList.left()) -// returns(Failure.Unknown.left()) -// } -// -// repo.updateList(mediaListMock).shouldBeLeft(ListsFailure.UpdatingList) -// repo.updateList(mediaListMock).shouldBeLeft(Failure.Unknown) -// -// verifySuspend { repo.updateList(mediaListMock) } -// } } } diff --git a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/sources/ApolloListsRemoteSourceTest.kt b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/sources/ApolloListsRemoteSourceTest.kt index 367a3dcd6..15610ae06 100644 --- a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/sources/ApolloListsRemoteSourceTest.kt +++ b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/sources/ApolloListsRemoteSourceTest.kt @@ -9,6 +9,7 @@ import com.apollographql.apollo3.testing.MapTestNetworkTransport import com.apollographql.apollo3.testing.registerTestNetworkError import com.apollographql.apollo3.testing.registerTestResponse import dev.alvr.katana.common.user.domain.managers.UserIdManager +import dev.alvr.katana.core.common.empty import dev.alvr.katana.core.common.zero import dev.alvr.katana.core.remote.optional import dev.alvr.katana.core.remote.type.MediaFormat @@ -54,7 +55,6 @@ import korlibs.time.Date import korlibs.time.DateTime import korlibs.time.DateTimeTz import korlibs.time.TimezoneOffset -import kotlin.time.Duration.Companion.milliseconds @ApolloExperimental internal class ApolloListsRemoteSourceTest : FreeSpec() { @@ -63,7 +63,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { private val client = ApolloClient.Builder().networkTransport(MapTestNetworkTransport()).build() private val userId = 37_384.right() - private val userIdOpt = userId.optional() + private val userIdOpt = userId.getOrNull().optional private val source: CommonListsRemoteSource = CommonListsRemoteSourceImpl(client, userIdManager, reloadInterceptor) private val animeSource: AnimeListsRemoteSource = AnimeListsRemoteSourceImpl(source) @@ -77,7 +77,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { "anime" - { "the collection has no lists" { val query = MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = emptyList() } } @@ -87,7 +87,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { query, ) - animeSource.animeCollection.test(100.milliseconds) { + animeSource.animeCollection.test { awaitItem().shouldBeRight().lists.shouldBeEmpty() awaitComplete() } @@ -96,7 +96,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { "the entries are empty" { val query = MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Watching" @@ -110,13 +110,9 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { name = "Custom List" entries = emptyList() }, - buildMediaListGroup { - name = null - entries = emptyList() - }, ) user = buildUser { - this["mediaListOptions"] = buildMediaListOptions { + mediaListOptions = buildMediaListOptions { animeList = buildMediaListTypeOptions { sectionOrder = listOf("Watching", "Completed TV", "Custom List") } @@ -130,9 +126,9 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { query, ) - animeSource.animeCollection.test(100.milliseconds) { + animeSource.animeCollection.test { awaitItem().shouldBeRight().lists - .shouldHaveSize(4) + .shouldHaveSize(3) .also { lists -> with(lists.first()) { entries.shouldBeEmpty() @@ -146,14 +142,14 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { "the entry has null values" { val query = MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Watching" entries = listOf( buildMediaList { id = Int.zero - score = null + score = Double.zero progress = null progressVolumes = null repeat = null @@ -164,10 +160,10 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { completedAt = null media = buildMedia { id = Int.zero - title = null + title = buildMediaTitle { userPreferred = String.empty } episodes = null format = null - coverImage = null + coverImage = buildMediaCoverImage { large = String.empty } nextAiringEpisode = null } }, @@ -182,7 +178,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { query, ) - animeSource.animeCollection.test(100.milliseconds) { + animeSource.animeCollection.test { awaitItem().shouldBeRight().lists.also { lists -> val entry = lists.first().entries.shouldHaveSize(1).first() @@ -219,7 +215,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { "the entry has values" { val query = MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Watching" @@ -270,7 +266,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { query, ) - animeSource.animeCollection.test(100.milliseconds) { + animeSource.animeCollection.test { awaitItem().shouldBeRight().lists.also { lists -> val entry = lists.first().entries.shouldHaveSize(1).first() @@ -317,8 +313,8 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { null, ) - animeSource.animeCollection.test(100.milliseconds) { - awaitItem().shouldBeRight().lists.shouldBeEmpty() + animeSource.animeCollection.test { + awaitItem().shouldBeLeft(ListsFailure.GetMediaCollection) awaitComplete() } @@ -333,7 +329,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { ), ) - animeSource.animeCollection.test(100.milliseconds) { + animeSource.animeCollection.test { awaitItem().shouldBeLeft(ListsFailure.GetMediaCollection) awaitComplete() } @@ -345,7 +341,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { "manga" - { "the collection has no lists" { val query = MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = emptyList() } } @@ -355,7 +351,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { query, ) - mangaSource.mangaCollection.test(100.milliseconds) { + mangaSource.mangaCollection.test { awaitItem().shouldBeRight().lists.shouldBeEmpty() awaitComplete() } @@ -365,7 +361,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { "the entries are empty" { val query = MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Rereading" @@ -379,13 +375,9 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { name = "Custom List" entries = emptyList() }, - buildMediaListGroup { - name = null - entries = emptyList() - }, ) user = buildUser { - this["mediaListOptions"] = buildMediaListOptions { + mediaListOptions = buildMediaListOptions { mangaList = buildMediaListTypeOptions { sectionOrder = listOf("Custom List", "Reading", "Rereading") } @@ -399,9 +391,9 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { query, ) - mangaSource.mangaCollection.test(100.milliseconds) { + mangaSource.mangaCollection.test { awaitItem().shouldBeRight().lists - .shouldHaveSize(4) + .shouldHaveSize(3) .also { lists -> with(lists.first()) { entries.shouldBeEmpty() @@ -415,14 +407,14 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { "the entry has null values" { val query = MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Reading" entries = listOf( buildMediaList { id = Int.zero - score = null + score = Double.zero progress = null progressVolumes = null repeat = null @@ -433,11 +425,11 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { completedAt = null media = buildMedia { id = Int.zero - title = null + title = buildMediaTitle { userPreferred = String.empty } chapters = null volumes = null format = null - coverImage = null + coverImage = buildMediaCoverImage { large = String.empty } nextAiringEpisode = null } }, @@ -452,7 +444,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { query, ) - mangaSource.mangaCollection.test(100.milliseconds) { + mangaSource.mangaCollection.test { awaitItem().shouldBeRight().lists.also { lists -> val entry = lists.first().entries.shouldHaveSize(1).first() @@ -489,7 +481,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { "the entry has values" { val query = MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = listOf( buildMediaListGroup { name = "Reading" @@ -538,7 +530,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { query, ) - mangaSource.mangaCollection.test(100.milliseconds) { + mangaSource.mangaCollection.test { awaitItem().shouldBeRight().lists.also { lists -> val entry = lists.first().entries.shouldHaveSize(1).first() @@ -579,8 +571,8 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { null, ) - mangaSource.mangaCollection.test(100.milliseconds) { - awaitItem().shouldBeRight().lists.shouldBeEmpty() + mangaSource.mangaCollection.test { + awaitItem().shouldBeLeft(ListsFailure.GetMediaCollection) awaitComplete() } @@ -595,7 +587,7 @@ internal class ApolloListsRemoteSourceTest : FreeSpec() { ), ) - mangaSource.mangaCollection.test(100.milliseconds) { + mangaSource.mangaCollection.test { awaitItem().shouldBeLeft(ListsFailure.GetMediaCollection) awaitComplete() } diff --git a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/sources/CommonListsRemoteSourceTest.kt b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/sources/CommonListsRemoteSourceTest.kt index 4d8d0e344..7787872cd 100644 --- a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/sources/CommonListsRemoteSourceTest.kt +++ b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/sources/CommonListsRemoteSourceTest.kt @@ -6,21 +6,25 @@ import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.annotations.ApolloExperimental import com.apollographql.apollo3.api.ApolloResponse import com.apollographql.apollo3.interceptor.ApolloInterceptor -import com.apollographql.apollo3.mockserver.MockResponse import com.apollographql.apollo3.mockserver.MockServer -import com.apollographql.apollo3.testing.QueueTestNetworkTransport -import com.apollographql.apollo3.testing.enqueueTestResponse +import com.apollographql.apollo3.mockserver.enqueueError +import com.apollographql.apollo3.mockserver.enqueueString +import com.apollographql.apollo3.testing.MapTestNetworkTransport +import com.apollographql.apollo3.testing.registerTestNetworkError +import com.apollographql.apollo3.testing.registerTestResponse import com.benasher44.uuid.uuid4 import dev.alvr.katana.common.user.domain.managers.UserIdManager +import dev.alvr.katana.core.domain.failures.Failure +import dev.alvr.katana.core.remote.optional import dev.alvr.katana.core.remote.type.MediaType import dev.alvr.katana.core.remote.type.buildMediaListCollection +import dev.alvr.katana.core.remote.type.buildUser import dev.alvr.katana.core.tests.shouldBeLeft import dev.alvr.katana.core.tests.shouldBeRight import dev.alvr.katana.features.lists.data.MediaListCollectionQuery import dev.alvr.katana.features.lists.data.apolloErrorMock -import dev.alvr.katana.features.lists.data.enqueueResponse +import dev.alvr.katana.features.lists.data.mappers.requests.toMutation import dev.alvr.katana.features.lists.data.mediaListCollectionQueryMock -import dev.alvr.katana.features.lists.data.mediaListEntriesMutationMock import dev.alvr.katana.features.lists.data.mediaListMock import dev.alvr.katana.features.lists.domain.failures.ListsFailure import dev.alvr.katana.features.lists.domain.models.MediaCollection @@ -30,93 +34,106 @@ import dev.mokkery.everySuspend import dev.mokkery.mock import dev.mokkery.verifySuspend import io.kotest.core.spec.style.FreeSpec -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds @OptIn(ApolloExperimental::class) internal class CommonListsRemoteSourceTest : FreeSpec() { private val userIdManager = mock() private val reloadInterceptor = mock() - private val client = - ApolloClient.Builder().networkTransport(QueueTestNetworkTransport()).build() - private val source = CommonListsRemoteSourceImpl(client, userIdManager, reloadInterceptor) + private val client = ApolloClient.Builder().networkTransport(MapTestNetworkTransport()).build() + private val source: CommonListsRemoteSource = + CommonListsRemoteSourceImpl(client, userIdManager, reloadInterceptor) init { "querying" - { queryList().forEach { (data, type) -> "the server responded with data or not ($data, $type)" { - everySuspend { userIdManager.getId() } returns 37_384.right() + everySuspend { userIdManager.getId() } returns USER_ID.right() val response = ApolloResponse.Builder( operation = mediaListCollectionQueryMock, requestUuid = uuid4(), - data = data, - ).build() - client.enqueueTestResponse(response) + ).data(data).build() + client.registerTestResponse( + MediaListCollectionQuery(USER_ID.optional, type), + response, + ) + + source.getMediaCollection(type).test { + if (data == null) { + awaitItem().shouldBeLeft(ListsFailure.GetMediaCollection) + } else { + awaitItem().shouldBeRight(MediaCollection(emptyList())) + } - source.getMediaCollection(type).test(100.milliseconds) { - awaitItem().shouldBeRight(MediaCollection(emptyList())) cancelAndIgnoreRemainingEvents() } + verifySuspend { userIdManager.getId() } } "a HTTP error occurs ($data, $type)" { - everySuspend { userIdManager.getId() } returns 37_384.right() + everySuspend { userIdManager.getId() } returns USER_ID.right() val response = ApolloResponse.Builder( operation = mediaListCollectionQueryMock, requestUuid = uuid4(), - data = data, - ).errors(listOf(apolloErrorMock)).build() - client.enqueueTestResponse(response) - - source.getMediaCollection(type).test(100.milliseconds) { - awaitItem().shouldBeRight(MediaCollection(emptyList())) + ).data(data).errors(listOf(apolloErrorMock)).build() + client.registerTestResponse( + MediaListCollectionQuery(USER_ID.optional, type), + response, + ) + + source.getMediaCollection(type).test { + awaitItem().shouldBeLeft(Failure.Unknown) cancelAndIgnoreRemainingEvents() } verifySuspend { userIdManager.getId() } } } + + "with errors" - { + val mockServer = MockServer() + val badClient = ApolloClient.Builder().serverUrl(mockServer.url()).build() + val source: CommonListsRemoteSource = CommonListsRemoteSourceImpl( + badClient, + userIdManager, + reloadInterceptor, + ) + + afterSpec { mockServer.close() } + + mockServer.badClient().forEach { (type, enqueueAction) -> + "a HTTP error occurs" { + everySuspend { userIdManager.getId() } returns USER_ID.right() + enqueueAction() + + source.getMediaCollection(type).test { + awaitItem().shouldBeLeft(ListsFailure.GetMediaCollection) + cancelAndIgnoreRemainingEvents() + } + verifySuspend { userIdManager.getId() } + } + } + } } "updating" - { "the server returns some data" { - client.enqueueTestResponse(mediaListEntriesMutationMock) + client.registerTestResponse(mediaListMock.toMutation()) source.updateList(mediaListMock).shouldBeRight() } - } - - "with errors" - { - val mockServer = MockServer() - val badClient = ApolloClient.Builder().serverUrl(mockServer.url()).build() - val source: CommonListsRemoteSource = CommonListsRemoteSourceImpl( - badClient, - userIdManager, - reloadInterceptor, - ) - - afterSpec { mockServer.stop() } - badClient().forEach { (type, action) -> - "a HTTP error occurs" { - everySuspend { userIdManager.getId() } returns 37_384.right() - mockServer.enqueueResponse(action) - - source.getMediaCollection(type).test(5.seconds) { - awaitItem().shouldBeLeft(ListsFailure.GetMediaCollection) - cancelAndIgnoreRemainingEvents() - } - verifySuspend { userIdManager.getId() } - } + "with errors" - { + client.registerTestNetworkError(mediaListMock.toMutation()) + source.updateList(mediaListMock).shouldBeLeft() } } } private fun queryList(): List> { val empty = MediaListCollectionQuery.Data { - this["collection"] = buildMediaListCollection { + this["MediaListCollection"] = buildMediaListCollection { lists = emptyList() - user = null + user = buildUser { } } } @@ -124,27 +141,27 @@ internal class CommonListsRemoteSourceTest : FreeSpec() { add(null) add(empty) } - val types = MediaType.knownValues() + val types = MediaType.knownEntries return buildList { values.forEach { v -> types.forEach { t -> add(v to t) } } } } - private fun badClient(): List Unit)>> { - fun apolloCommand( - block: MockResponse.Builder.() -> Unit, - ): MockResponse.Builder.() -> Unit = { MockResponse.Builder().apply(block) } - + private fun MockServer.badClient(): List Unit)>> { val commands = buildList { - add(apolloCommand { statusCode(500) }) - add(apolloCommand { body("Malformed body") }) - add(apolloCommand { body("""{"data": {"random": 42}}""") }) + add { enqueueError(500) } + add { enqueueString("Malformed body") } + add { enqueueString("""{"data": {"random": 42}}""") } } - val types = MediaType.knownValues() + val types = MediaType.knownEntries return buildList { commands.forEach { c -> types.forEach { t -> add(t to c) } } } } + + private companion object { + const val USER_ID = 37_384 + } } diff --git a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/sources/ListsRemoteSourceTest.kt b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/sources/ListsRemoteSourceTest.kt index 0c34f5eda..eb5e67ad6 100644 --- a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/sources/ListsRemoteSourceTest.kt +++ b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/sources/ListsRemoteSourceTest.kt @@ -15,7 +15,6 @@ import dev.mokkery.every import dev.mokkery.mock import dev.mokkery.verify import io.kotest.core.spec.style.FreeSpec -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.flowOf internal class ListsRemoteSourceTest : FreeSpec() { @@ -33,12 +32,12 @@ internal class ListsRemoteSourceTest : FreeSpec() { MediaCollection(emptyList()).right(), ) - animeSource.animeCollection.test(100.milliseconds) { + animeSource.animeCollection.test { awaitItem().shouldBeRight(MediaCollection(emptyList())) cancelAndIgnoreRemainingEvents() } - mangaSource.mangaCollection.test(100.milliseconds) { + mangaSource.mangaCollection.test { awaitItem().shouldBeRight(MediaCollection(emptyList())) cancelAndIgnoreRemainingEvents() } diff --git a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/utils.kt b/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/utils.kt deleted file mode 100644 index 4952d415d..000000000 --- a/features/lists/data/src/commonTest/kotlin/dev/alvr/katana/features/lists/data/utils.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.alvr.katana.features.lists.data - -import com.apollographql.apollo3.annotations.ApolloExperimental -import com.apollographql.apollo3.mockserver.MockResponse -import com.apollographql.apollo3.mockserver.MockServer - -@OptIn(ApolloExperimental::class) -internal fun MockServer.enqueueResponse(builder: MockResponse.Builder.() -> Unit) { - repeat(4) { - enqueue(MockResponse.Builder().apply(builder).build()) - } -} diff --git a/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveAnimeListUseCaseTest.kt b/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveAnimeListUseCaseTest.kt index 1ddc8c156..e32e16314 100644 --- a/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveAnimeListUseCaseTest.kt +++ b/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveAnimeListUseCaseTest.kt @@ -19,7 +19,6 @@ import dev.mokkery.mock import dev.mokkery.verify import io.kotest.core.spec.style.FreeSpec import io.kotest.core.test.TestCase -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.flowOf import org.koin.test.KoinTest import org.koin.test.inject @@ -38,7 +37,7 @@ internal class ObserveAnimeListUseCaseTest : FreeSpec(), KoinTest { useCase() - useCase.flow.test(100.milliseconds) { + useCase.flow.test { awaitItem().shouldBeRight(MediaCollection(emptyList())) cancelAndConsumeRemainingEvents() } @@ -53,7 +52,7 @@ internal class ObserveAnimeListUseCaseTest : FreeSpec(), KoinTest { useCase() - useCase.flow.test(100.milliseconds) { + useCase.flow.test { awaitItem().shouldBeLeft(ListsFailure.GetMediaCollection) cancelAndConsumeRemainingEvents() } diff --git a/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveMangaListUseCaseTest.kt b/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveMangaListUseCaseTest.kt index f3bf55424..d44b1bb9c 100644 --- a/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveMangaListUseCaseTest.kt +++ b/features/lists/domain/src/commonTest/kotlin/dev/alvr/katana/features/lists/domain/usecases/ObserveMangaListUseCaseTest.kt @@ -19,7 +19,6 @@ import dev.mokkery.mock import dev.mokkery.verify import io.kotest.core.spec.style.FreeSpec import io.kotest.core.test.TestCase -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.flowOf import org.koin.test.KoinTest import org.koin.test.inject @@ -38,7 +37,7 @@ internal class ObserveMangaListUseCaseTest : FreeSpec(), KoinTest { useCase() - useCase.flow.test(100.milliseconds) { + useCase.flow.test { awaitItem().shouldBeRight(MediaCollection(emptyList())) cancelAndConsumeRemainingEvents() } @@ -53,7 +52,7 @@ internal class ObserveMangaListUseCaseTest : FreeSpec(), KoinTest { useCase() - useCase.flow.test(100.milliseconds) { + useCase.flow.test { awaitItem().shouldBeLeft(ListsFailure.GetMediaCollection) cancelAndConsumeRemainingEvents() } diff --git a/features/social/data/build.gradle.kts b/features/social/data/build.gradle.kts index 8c35680de..658dedb0b 100644 --- a/features/social/data/build.gradle.kts +++ b/features/social/data/build.gradle.kts @@ -2,10 +2,6 @@ plugins { id("katana.multiplatform.data.remote") } -dependencies { - apolloMetadata(projects.core.remote) -} - kotlin { sourceSets { commonMain.dependencies { diff --git a/gradle.properties b/gradle.properties index 55662c778..45b3d2979 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,6 +12,11 @@ kotlin.code.style=official kotlin.incremental=true kotlin.incremental.useClasspathSnapshot=true +# Kotest +kotest.framework.classpath.scanning.autoscan.disable=true +kotest.framework.classpath.scanning.config.disable=true +kotest.framework.config.fqn=dev.alvr.katana.core.tests.KotestProjectConfig + # KSP # ksp.useKSP2=true TODO: https://github.com/raamcosta/compose-destinations/issues/545 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7bac1b3b1..7cb0196a4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ android = "8.4.1" androidx-activity = "1.9.0" androidx-splashscreen = "1.0.1" -apollo = "3.8.4" +apollo = "4.0.0-beta.6" arrow = "1.2.4" buildconfig = "5.3.5" compose = "1.6.10"