[PM-28468] Added service methods to migration to MyItems validation (#6248)

This commit is contained in:
aj-rosado 2025-12-09 15:58:23 +00:00 committed by GitHub
parent cd27fe339d
commit 4a874668f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 322 additions and 0 deletions

View File

@ -25,4 +25,10 @@ interface PolicyManager {
userId: String,
type: PolicyTypeJson,
): List<SyncResponseJson.Policy>
/**
* Get the organization id of the personal ownership policy.
* If multiple organizations enforce the policy, return the first to set it.
*/
fun getPersonalOwnershipPolicyOrganizationId(): String?
}

View File

@ -66,6 +66,13 @@ class PolicyManagerImpl(
)
.orEmpty()
override fun getPersonalOwnershipPolicyOrganizationId(): String? =
this
.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP)
.sortedBy { it.revisionDate }
.firstOrNull()
?.organizationId
/**
* A helper method to filter policies.
*/

View File

@ -166,4 +166,11 @@ interface VaultRepository :
* `null` if the item cannot be found.
*/
fun getVaultListItemStateFlow(itemId: String): StateFlow<DataState<CipherListView?>>
/**
* Checks if there are any personal vault items (items without an organization ID) in the vault.
*
* @return `true` if there are personal vault items, `false` otherwise.
*/
fun hasPersonalVaultItems(): Boolean
}

View File

@ -550,4 +550,9 @@ class VaultRepositoryImpl(
organizationKeys = organizationKeys,
)
}
override fun hasPersonalVaultItems(): Boolean {
val vaultData = vaultSyncManager.vaultDataStateFlow.value.data ?: return false
return vaultData.decryptCipherListResult.successes.any { it.organizationId.isNullOrEmpty() }
}
}

View File

@ -13,9 +13,11 @@ import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.ZonedDateTime
class PolicyManagerTest {
private val mutableUserStateFlow = MutableStateFlow<UserStateJson?>(null)
@ -277,6 +279,207 @@ class PolicyManagerTest {
),
)
}
@Test
fun `getPersonalOwnershipPolicyOrganizationId returns null when no active user`() {
every { authDiskSource.userState } returns null
assertNull(policyManager.getPersonalOwnershipPolicyOrganizationId())
}
@Test
fun `getPersonalOwnershipPolicyOrganizationId returns null when no policies exist`() {
val userState: UserStateJson = mockk {
every { activeUserId } returns USER_ID
}
every { authDiskSource.userState } returns userState
every { authDiskSource.getOrganizations(USER_ID) } returns emptyList()
every { authDiskSource.getPolicies(USER_ID) } returns emptyList()
assertNull(policyManager.getPersonalOwnershipPolicyOrganizationId())
}
@Test
fun `getPersonalOwnershipPolicyOrganizationId returns null when policy is disabled`() {
val userState: UserStateJson = mockk {
every { activeUserId } returns USER_ID
}
every { authDiskSource.userState } returns userState
every {
authDiskSource.getOrganizations(USER_ID)
} returns listOf(
createMockOrganization(
number = 1,
isEnabled = true,
shouldUsePolicies = true,
type = OrganizationType.USER,
),
)
every {
authDiskSource.getPolicies(USER_ID)
} returns listOf(
createMockPolicy(
organizationId = "mockId-1",
isEnabled = false,
type = PolicyTypeJson.PERSONAL_OWNERSHIP,
),
)
assertNull(policyManager.getPersonalOwnershipPolicyOrganizationId())
}
@Test
fun `getPersonalOwnershipPolicyOrganizationId returns organization id for single policy`() {
val userState: UserStateJson = mockk {
every { activeUserId } returns USER_ID
}
val expectedOrganizationId = "mockId-1"
every { authDiskSource.userState } returns userState
every {
authDiskSource.getOrganizations(USER_ID)
} returns listOf(
createMockOrganization(
number = 1,
isEnabled = true,
shouldUsePolicies = true,
type = OrganizationType.USER,
),
)
every {
authDiskSource.getPolicies(USER_ID)
} returns listOf(
createMockPolicy(
organizationId = expectedOrganizationId,
isEnabled = true,
type = PolicyTypeJson.PERSONAL_OWNERSHIP,
),
)
assertEquals(
expectedOrganizationId,
policyManager.getPersonalOwnershipPolicyOrganizationId(),
)
}
@Suppress("MaxLineLength")
@Test
fun `getPersonalOwnershipPolicyOrganizationId returns earliest policy when multiple orgs have policy`() {
val userState: UserStateJson = mockk {
every { activeUserId } returns USER_ID
}
val earliestRevisionDate = ZonedDateTime.parse("2024-01-01T00:00:00Z")
val middleRevisionDate = ZonedDateTime.parse("2024-06-01T00:00:00Z")
val latestRevisionDate = ZonedDateTime.parse("2024-12-01T00:00:00Z")
val expectedOrganizationId = "mockId-1"
every { authDiskSource.userState } returns userState
every {
authDiskSource.getOrganizations(USER_ID)
} returns listOf(
createMockOrganization(
number = 1,
isEnabled = true,
shouldUsePolicies = true,
type = OrganizationType.USER,
),
createMockOrganization(
number = 2,
isEnabled = true,
shouldUsePolicies = true,
type = OrganizationType.USER,
),
createMockOrganization(
number = 3,
isEnabled = true,
shouldUsePolicies = true,
type = OrganizationType.USER,
),
)
every {
authDiskSource.getPolicies(USER_ID)
} returns listOf(
createMockPolicy(
number = 3,
organizationId = "mockId-3",
isEnabled = true,
type = PolicyTypeJson.PERSONAL_OWNERSHIP,
revisionDate = latestRevisionDate,
),
createMockPolicy(
number = 1,
organizationId = expectedOrganizationId,
isEnabled = true,
type = PolicyTypeJson.PERSONAL_OWNERSHIP,
revisionDate = earliestRevisionDate,
),
createMockPolicy(
number = 2,
organizationId = "mockId-2",
isEnabled = true,
type = PolicyTypeJson.PERSONAL_OWNERSHIP,
revisionDate = middleRevisionDate,
),
)
assertEquals(
expectedOrganizationId,
policyManager.getPersonalOwnershipPolicyOrganizationId(),
)
}
@Suppress("MaxLineLength")
@Test
fun `getPersonalOwnershipPolicyOrganizationId filters out policies from organizations not using policies`() {
val userState: UserStateJson = mockk {
every { activeUserId } returns USER_ID
}
val earlierRevisionDate = ZonedDateTime.parse("2024-01-01T00:00:00Z")
val laterRevisionDate = ZonedDateTime.parse("2024-06-01T00:00:00Z")
val expectedOrganizationId = "mockId-2"
every { authDiskSource.userState } returns userState
every {
authDiskSource.getOrganizations(USER_ID)
} returns listOf(
createMockOrganization(
number = 1,
isEnabled = true,
shouldUsePolicies = false, // This org does NOT use policies
type = OrganizationType.USER,
),
createMockOrganization(
number = 2,
isEnabled = true,
shouldUsePolicies = true, // This org uses policies
type = OrganizationType.USER,
),
)
every {
authDiskSource.getPolicies(USER_ID)
} returns listOf(
createMockPolicy(
number = 1,
organizationId = "mockId-1",
isEnabled = true,
type = PolicyTypeJson.PERSONAL_OWNERSHIP,
revisionDate = earlierRevisionDate, // Earlier but org doesn't enforce
),
createMockPolicy(
number = 2,
organizationId = expectedOrganizationId,
isEnabled = true,
type = PolicyTypeJson.PERSONAL_OWNERSHIP,
revisionDate = laterRevisionDate,
),
)
// Should return mockId-2 because mockId-1's organization doesn't enforce policies
assertEquals(
expectedOrganizationId,
policyManager.getPersonalOwnershipPolicyOrganizationId(),
)
}
}
private const val USER_ID = "userId"

View File

@ -1390,6 +1390,100 @@ class VaultRepositoryTest {
}
}
@Test
fun `hasPersonalVaultItems returns false when vault data is loading`() {
mutableVaultDataStateFlow.value = DataState.Loading
val result = vaultRepository.hasPersonalVaultItems()
assertEquals(false, result)
}
@Test
fun `hasPersonalVaultItems returns false when all items belong to organizations`() {
mutableVaultDataStateFlow.value = DataState.Loaded(
data = VaultData(
decryptCipherListResult = DecryptCipherListResult(
successes = listOf(
createMockCipherListView(number = 1, organizationId = "org-1"),
createMockCipherListView(number = 2, organizationId = "org-2"),
),
failures = emptyList(),
),
collectionViewList = emptyList(),
folderViewList = emptyList(),
sendViewList = emptyList(),
),
)
val result = vaultRepository.hasPersonalVaultItems()
assertEquals(false, result)
}
@Test
fun `hasPersonalVaultItems returns true when there are items without organization ID`() {
mutableVaultDataStateFlow.value = DataState.Loaded(
data = VaultData(
decryptCipherListResult = DecryptCipherListResult(
successes = listOf(
createMockCipherListView(number = 1, organizationId = null),
createMockCipherListView(number = 2, organizationId = "org-2"),
),
failures = emptyList(),
),
collectionViewList = emptyList(),
folderViewList = emptyList(),
sendViewList = emptyList(),
),
)
val result = vaultRepository.hasPersonalVaultItems()
assertEquals(true, result)
}
@Test
fun `hasPersonalVaultItems returns true when there are items with empty organization ID`() {
mutableVaultDataStateFlow.value = DataState.Loaded(
data = VaultData(
decryptCipherListResult = DecryptCipherListResult(
successes = listOf(
createMockCipherListView(number = 1, organizationId = ""),
createMockCipherListView(number = 2, organizationId = "org-2"),
),
failures = emptyList(),
),
collectionViewList = emptyList(),
folderViewList = emptyList(),
sendViewList = emptyList(),
),
)
val result = vaultRepository.hasPersonalVaultItems()
assertEquals(true, result)
}
@Test
fun `hasPersonalVaultItems returns false when successes list is empty`() {
mutableVaultDataStateFlow.value = DataState.Loaded(
data = VaultData(
decryptCipherListResult = DecryptCipherListResult(
successes = emptyList(),
failures = emptyList(),
),
collectionViewList = emptyList(),
folderViewList = emptyList(),
sendViewList = emptyList(),
),
)
val result = vaultRepository.hasPersonalVaultItems()
assertEquals(false, result)
}
//region Helper functions
/**