diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/spaces/responses/SpacesResponse.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/spaces/responses/SpacesResponse.kt index a108b3bd95..aefb9f7c80 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/spaces/responses/SpacesResponse.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/spaces/responses/SpacesResponse.kt @@ -54,7 +54,7 @@ data class OwnerResponse( data class QuotaResponse( val remaining: Long?, val state: String?, - val total: Long, + val total: Long?, val used: Long?, ) diff --git a/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/spaces/responses/SpacesResponseTest.kt b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/spaces/responses/SpacesResponseTest.kt new file mode 100644 index 0000000000..e5da2e8a34 --- /dev/null +++ b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/spaces/responses/SpacesResponseTest.kt @@ -0,0 +1,66 @@ +/* openCloud Android Library is available under MIT license + * Copyright (C) 2026 OpenCloud GmbH. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package eu.opencloud.android.lib.resources.spaces.responses + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test + +class SpacesResponseTest { + + private lateinit var adapter: JsonAdapter + + @Before + fun setUp() { + adapter = Moshi.Builder().build().adapter(SpacesResponseWrapper::class.java) + } + + @Test + fun `empty quota object is parsed`() { + val response = adapter.fromJson( + """ + { + "value": [ + { + "driveType": "personal", + "id": "personal-space-id", + "name": "Personal", + "quota": {}, + "root": { + "id": "personal-space-id", + "webDavUrl": "https://server.url/dav/spaces/personal-space-id" + } + } + ] + } + """.trimIndent() + ) + + assertEquals(1, response?.value?.size) + assertNull(response?.value?.first()?.quota?.total) + } +} diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/spaces/db/SpacesEntity.kt b/opencloudData/src/main/java/eu/opencloud/android/data/spaces/db/SpacesEntity.kt index abcf7f667c..db28052af3 100644 --- a/opencloudData/src/main/java/eu/opencloud/android/data/spaces/db/SpacesEntity.kt +++ b/opencloudData/src/main/java/eu/opencloud/android/data/spaces/db/SpacesEntity.kt @@ -89,7 +89,7 @@ data class SpaceQuotaEntity( @ColumnInfo(name = SPACES_QUOTA_STATE) val state: String?, @ColumnInfo(name = SPACES_QUOTA_TOTAL) - val total: Long, + val total: Long?, @ColumnInfo(name = SPACES_QUOTA_USED) val used: Long?, ) diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/spaces/repository/OCSpacesRepository.kt b/opencloudData/src/main/java/eu/opencloud/android/data/spaces/repository/OCSpacesRepository.kt index 665434fea6..cfccb51e19 100644 --- a/opencloudData/src/main/java/eu/opencloud/android/data/spaces/repository/OCSpacesRepository.kt +++ b/opencloudData/src/main/java/eu/opencloud/android/data/spaces/repository/OCSpacesRepository.kt @@ -28,8 +28,8 @@ import eu.opencloud.android.data.spaces.datasources.RemoteSpacesDataSource import eu.opencloud.android.data.user.datasources.LocalUserDataSource import eu.opencloud.android.domain.spaces.SpacesRepository import eu.opencloud.android.domain.spaces.model.OCSpace -import eu.opencloud.android.domain.user.model.UserQuotaState import eu.opencloud.android.domain.user.model.UserQuota +import eu.opencloud.android.domain.user.model.UserQuotaState class OCSpacesRepository( private val localSpacesDataSource: LocalSpacesDataSource, @@ -43,19 +43,36 @@ class OCSpacesRepository( val personalSpace = listOfSpaces.find { it.isPersonal } val capabilities = localCapabilitiesDataSource.getCapabilitiesForAccount(accountName) val isMultiPersonal = capabilities?.spaces?.hasMultiplePersonalSpaces - val userQuota = personalSpace?.let { - if (isMultiPersonal == true) { - UserQuota(accountName, -4, 0, 0, UserQuotaState.NORMAL) - } else if (it.quota?.total!!.toInt() == 0) { - UserQuota(accountName, -3, it.quota?.used!!, it.quota?.total!!, UserQuotaState.fromValue(it.quota?.state!!)) - } else { - UserQuota(accountName, it.quota?.remaining!!, it.quota?.used!!, it.quota?.total!!, UserQuotaState.fromValue(it.quota?.state!!)) - } - } ?: UserQuota(accountName, -4, 0, 0, UserQuotaState.NORMAL) + val userQuota = getUserQuotaForPersonalSpace(accountName, personalSpace, isMultiPersonal == true) localUserDataSource.saveQuotaForAccount(accountName, userQuota) } } + private fun getUserQuotaForPersonalSpace( + accountName: String, + personalSpace: OCSpace?, + isMultiPersonal: Boolean, + ): UserQuota { + if (isMultiPersonal || personalSpace == null) { + return unavailableQuota(accountName) + } + + val quota = personalSpace.quota ?: return unavailableQuota(accountName) + val total = quota.total ?: return unavailableQuota(accountName) + val used = quota.used ?: 0 + val state = quota.state?.let { UserQuotaState.fromValue(it) } ?: UserQuotaState.NORMAL + + return if (total == 0L) { + UserQuota(accountName, -3, used, total, state) + } else { + val remaining = quota.remaining ?: (total - used).coerceAtLeast(0) + UserQuota(accountName, remaining, used, total, state) + } + } + + private fun unavailableQuota(accountName: String) = + UserQuota(accountName, -4, 0, 0, UserQuotaState.NORMAL) + override fun getSpacesFromEveryAccountAsStream() = localSpacesDataSource.getSpacesFromEveryAccountAsStream() diff --git a/opencloudData/src/test/java/eu/opencloud/android/data/spaces/repository/OCSpacesRepositoryTest.kt b/opencloudData/src/test/java/eu/opencloud/android/data/spaces/repository/OCSpacesRepositoryTest.kt index d3a221ca96..e5681a6466 100644 --- a/opencloudData/src/test/java/eu/opencloud/android/data/spaces/repository/OCSpacesRepositoryTest.kt +++ b/opencloudData/src/test/java/eu/opencloud/android/data/spaces/repository/OCSpacesRepositoryTest.kt @@ -28,8 +28,10 @@ import eu.opencloud.android.testutil.OC_ACCOUNT_NAME import eu.opencloud.android.testutil.OC_CAPABILITY import eu.opencloud.android.testutil.OC_CAPABILITY_WITH_MULTIPERSONAL_ENABLED import eu.opencloud.android.testutil.OC_SPACE_PERSONAL +import eu.opencloud.android.testutil.OC_SPACE_PERSONAL_WITH_EMPTY_QUOTA import eu.opencloud.android.testutil.OC_SPACE_PERSONAL_WITH_LIMITED_QUOTA import eu.opencloud.android.testutil.OC_SPACE_PERSONAL_WITH_UNLIMITED_QUOTA +import eu.opencloud.android.testutil.OC_SPACE_PERSONAL_WITHOUT_QUOTA import eu.opencloud.android.testutil.OC_SPACE_PROJECT_WITH_IMAGE import eu.opencloud.android.testutil.OC_USER_QUOTA_LIMITED import eu.opencloud.android.testutil.OC_USER_QUOTA_UNLIMITED @@ -133,6 +135,46 @@ class OCSpacesRepositoryTest { } } + @Test + fun `refreshSpacesForAccount refreshes spaces for account correctly when quota is missing`() { + every { + remoteSpacesDataSource.refreshSpacesForAccount(OC_ACCOUNT_NAME) + } returns listOf(OC_SPACE_PERSONAL_WITHOUT_QUOTA) + + every { + localCapabilitiesDataSource.getCapabilitiesForAccount(OC_ACCOUNT_NAME) + } returns OC_CAPABILITY + + ocSpacesRepository.refreshSpacesForAccount(OC_ACCOUNT_NAME) + + verify(exactly = 1) { + remoteSpacesDataSource.refreshSpacesForAccount(OC_ACCOUNT_NAME) + localSpacesDataSource.saveSpacesForAccount(listOf(OC_SPACE_PERSONAL_WITHOUT_QUOTA)) + localCapabilitiesDataSource.getCapabilitiesForAccount(OC_ACCOUNT_NAME) + localUserDataSource.saveQuotaForAccount(OC_ACCOUNT_NAME, OC_USER_QUOTA_WITHOUT_PERSONAL) + } + } + + @Test + fun `refreshSpacesForAccount refreshes spaces for account correctly when quota is empty`() { + every { + remoteSpacesDataSource.refreshSpacesForAccount(OC_ACCOUNT_NAME) + } returns listOf(OC_SPACE_PERSONAL_WITH_EMPTY_QUOTA) + + every { + localCapabilitiesDataSource.getCapabilitiesForAccount(OC_ACCOUNT_NAME) + } returns OC_CAPABILITY + + ocSpacesRepository.refreshSpacesForAccount(OC_ACCOUNT_NAME) + + verify(exactly = 1) { + remoteSpacesDataSource.refreshSpacesForAccount(OC_ACCOUNT_NAME) + localSpacesDataSource.saveSpacesForAccount(listOf(OC_SPACE_PERSONAL_WITH_EMPTY_QUOTA)) + localCapabilitiesDataSource.getCapabilitiesForAccount(OC_ACCOUNT_NAME) + localUserDataSource.saveQuotaForAccount(OC_ACCOUNT_NAME, OC_USER_QUOTA_WITHOUT_PERSONAL) + } + } + @Test fun `getSpacesFromEveryAccountAsStream returns a Flow with a list of OCSpace`() = runTest { every { diff --git a/opencloudDomain/src/main/java/eu/opencloud/android/domain/spaces/model/OCSpace.kt b/opencloudDomain/src/main/java/eu/opencloud/android/domain/spaces/model/OCSpace.kt index 421efbd0d7..766c27d200 100644 --- a/opencloudDomain/src/main/java/eu/opencloud/android/domain/spaces/model/OCSpace.kt +++ b/opencloudDomain/src/main/java/eu/opencloud/android/domain/spaces/model/OCSpace.kt @@ -64,7 +64,7 @@ data class SpaceOwner( data class SpaceQuota( val remaining: Long?, val state: String?, - val total: Long, + val total: Long?, val used: Long?, ) diff --git a/opencloudTestUtil/src/main/java/eu/opencloud/android/testutil/OCSpace.kt b/opencloudTestUtil/src/main/java/eu/opencloud/android/testutil/OCSpace.kt index 5282b4a4e4..fb9cf46a39 100644 --- a/opencloudTestUtil/src/main/java/eu/opencloud/android/testutil/OCSpace.kt +++ b/opencloudTestUtil/src/main/java/eu/opencloud/android/testutil/OCSpace.kt @@ -146,6 +146,19 @@ val OC_SPACE_PERSONAL_WITH_LIMITED_QUOTA = OC_SPACE_PERSONAL.copy( ) ) +val OC_SPACE_PERSONAL_WITHOUT_QUOTA = OC_SPACE_PERSONAL.copy( + quota = null +) + +val OC_SPACE_PERSONAL_WITH_EMPTY_QUOTA = OC_SPACE_PERSONAL.copy( + quota = SpaceQuota( + remaining = null, + state = null, + total = null, + used = null + ) +) + val OC_SPACE_SHARES = OCSpace( accountName = OC_ACCOUNT_NAME, driveAlias = "virtual/shares",