Added all the User's fields

#comment Some of them are still subject to changes, but all of them has been added from User API, with relative doc and tests
Affected: none

MAILAND-719
This commit is contained in:
Davide Farella 2020-06-24 21:42:00 +02:00 committed by Zorica Stojchevska
parent b59d2f2997
commit 6b656b190b
10 changed files with 516 additions and 78 deletions

View File

@ -16,8 +16,7 @@
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
import me.proton.core.util.gradle.setupDetekt
import me.proton.core.util.gradle.setupKotlin
import me.proton.core.util.gradle.*
buildscript {
initVersions()
@ -36,7 +35,8 @@ allprojects {
setupKotlin(
"-XXLanguage:+NewInference",
"-Xuse-experimental=kotlin.Experimental",
"-XXLanguage:+InlineClasses"
"-XXLanguage:+InlineClasses",
"-Xopt-in=kotlin.ExperimentalUnsignedTypes"
)
setupDetekt { "tokenAutoComplete" !in it.name }

View File

@ -37,11 +37,11 @@ package ch.protonmail.android.domain.entity
/**
* Represent a given number of bytes
*/
inline class Bytes(val l: Long)
inline class Bytes(val l: ULong)
/**
* Entity representing an email address
* Implements [Validable] by [RegexValidator]
* [Validable] by [RegexValidator]
*/
@Validated
data class EmailAddress(val s: String) : Validable by RegexValidator(s, VALIDATION_REGEX) {
@ -55,7 +55,7 @@ data class EmailAddress(val s: String) : Validable by RegexValidator(s, VALIDATI
/**
* Entity representing an id
* Implements [Validable] by [NotBlankStringValidator]
* [Validable] by [NotBlankStringValidator]
*/
@Validated
data class Id(val s: String) : Validable by NotBlankStringValidator(s) {
@ -64,7 +64,7 @@ data class Id(val s: String) : Validable by NotBlankStringValidator(s) {
/**
* Entity representing a generic name
* Implements [Validable] by [NotBlankStringValidator]
* [Validable] by [NotBlankStringValidator]
*/
data class Name(val s: String) : Validable by NotBlankStringValidator(s) {
init { requireValid() }
@ -72,7 +72,7 @@ data class Name(val s: String) : Validable by NotBlankStringValidator(s) {
/**
* Entity representing a generic String that cannot be blank
* Implements [Validable] by [NotBlankStringValidator]
* [Validable] by [NotBlankStringValidator]
*/
data class NotBlankString(val s: String) : Validable by NotBlankStringValidator(s) {
init { requireValid() }

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.domain.entity.user
import ch.protonmail.android.domain.entity.EmailAddress
import ch.protonmail.android.domain.entity.Id
import ch.protonmail.android.domain.entity.Name
import ch.protonmail.android.domain.entity.Validable
import ch.protonmail.android.domain.entity.Validated
import ch.protonmail.android.domain.entity.Validator
import ch.protonmail.android.domain.entity.requireValid
/**
* Representation of an user's address
* @author Davide Farella
*/
@Validated
data class Address(
val id: Id,
val domainId: Id,
val email: EmailAddress,
val displayName: Name?,
val keys: AddressKeys
)
/**
* A set of [Address]s with a primary one
*
* @param primaryAddress can be `null`, as is possible that the [user] didn't set up its email address ( VPN user )
* @param addresses can be empty only if [primaryAddress] is `null`
*/
@Validated
data class Addresses(
val primaryAddress: Address?,
val addresses: Collection<Address>
) : Validable by Validator<Addresses>({
primaryAddress == null && addresses.isEmpty() ||
primaryAddress in addresses
}) {
init { requireValid() }
val hasAddresses get() = addresses.isNotEmpty()
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.domain.entity.user
import ch.protonmail.android.domain.entity.Id
import ch.protonmail.android.domain.entity.Validable
import ch.protonmail.android.domain.entity.Validated
import ch.protonmail.android.domain.entity.Validator
import ch.protonmail.android.domain.entity.requireValid
// It is possible for an address to not have any key
/**
* Representation of an user's address' Key
* @author Davide Farella
*/
@Validated
data class AddressKey(
val id: Id
)
/**
* A set of [AddressKey]s with a primary one
* [Validable]: [keys] must contains [primaryKey], if not `null`
*
* @param primaryKey can be `null`, as an [Address] is not required to have keys
* @param keys can be empty only if [primaryKey] is `null`
*/
@Validated
data class AddressKeys(
val primaryKey: AddressKey?,
val keys: Collection<AddressKey>
) : Validable by Validator<AddressKeys>({
primaryKey == null && keys.isEmpty() ||
primaryKey in keys
}) {
init { requireValid() }
val hasKeys get() = keys.isNotEmpty()
}

View File

@ -1,31 +0,0 @@
package ch.protonmail.android.domain.entity.user
import ch.protonmail.android.domain.entity.Id
import ch.protonmail.android.domain.entity.NotBlankString
import ch.protonmail.android.domain.entity.Validable
import ch.protonmail.android.domain.entity.Validated
import ch.protonmail.android.domain.entity.Validator
import ch.protonmail.android.domain.entity.requireValid
/**
* Representation of an user's Key
* @author Davide Farella
*/
@Validated
data class Key(
val id: Id,
val version: Int,
val privateKey: NotBlankString
)
/**
* A set of [Key]s with a primary one
* [Validable]: [keys] must contains [primaryKey]
*/
@Validated
data class Keys(
val primaryKey: Key,
val keys: Collection<Key> = listOf(primaryKey) // Verify whether is a possible scenario to have a single key
) : Validable by Validator<Keys>({ primaryKey in keys }) {
init { requireValid() }
}

View File

@ -0,0 +1,23 @@
package ch.protonmail.android.domain.entity.user
/**
* A plan for [User]
* Plan types are exclusive; for example if [Mail.Paid] is present, [Mail.Free] cannot
*
* Free plan is represented on BE as `Services`, paid as `Subscribed`
* Combination of Mail + Vpn flag is 5.
*
* @author Davide Farella
*/
sealed class Plan {
sealed class Mail : Plan() { // Flag is 1 on BE
object Free : Mail()
object Paid : Mail()
}
sealed class Vpn : Plan() { // Flag is 4 on BE
object Free : Vpn()
object Paid : Vpn()
}
}

View File

@ -19,63 +19,88 @@
package ch.protonmail.android.domain.entity.user
import ch.protonmail.android.domain.entity.Bytes
import ch.protonmail.android.domain.entity.EmailAddress
import ch.protonmail.android.domain.entity.Id
import ch.protonmail.android.domain.entity.Name
import ch.protonmail.android.domain.entity.NotBlankString
import ch.protonmail.android.domain.entity.Validable
import ch.protonmail.android.domain.entity.Validated
import ch.protonmail.android.domain.entity.Validator
import ch.protonmail.android.domain.entity.requireValid
/**
* Representation of a server user.
*
* [Validable]
*
* * [addresses] can be empty only if no [Plan.Mail] is available in [plans]
*
* * [keys] can be empty if no [addresses] is set
*
* * [organizationPrivateKey] can and must be present only if [role] is [Role.ORGANIZATION_ADMIN]
*
* * [plans] contains at most 1 [Plan.Mail] and at most 1 [Plan.Vpn]
*
* @author Davide Farella
*
* TODO remove 👇 before merge!
* https://docs.protontech.ch/#user-get-user-info-get
*
"User": {
"ID": "MJLke8kWh1BBvG95JBIrZvzpgsZ94hNNgjNHVyhXMiv4g9cn6SgvqiIFR5cigpml2LD_iUk_3DkV29oojTt3eA==",
"Name": "jason",
"UsedSpace": 96691332,
"Currency": "USD",
"Credit": 0, TODO
"MaxSpace": 10737418240,
"MaxUpload": 26214400,
"Role": 2, TODO
"Private": 1, TODO
"Subscribed": 1, TODO
"Services": 1, TODO
"Delinquent": 0,
"OrganizationPrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----*", TODO
"Email": "jason@protonmail.ch",
"DisplayName": "Jason",
"Keys": [
{
"ID": "IlnTbqicN-2HfUGIn-ki8bqZfLqNj5ErUB0z24Qx5g-4NvrrIc6GLvEpj2EPfwGDv28aKYVRRrSgEFhR_zhlkA==",
"Version": 3,
"PrivateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----*-----END PGP PRIVATE KEY BLOCK-----",
"Fingerprint": "c93f767df53b0ca8395cfde90483475164ec6353", // DEPRECATED
"Primary": 1
}
]
}
*/
@Validated
data class User( // TODO: consider naming UserInfo or simialar
val id: Id,
val name: Name,
val displayName: Name,
val email: EmailAddress,
val keys: Keys,
val currency: NotBlankString, // might not be worth to have an endless enum
val subscribed: Boolean,
val delinquent: Boolean,
val addresses: Addresses,
val keys: UserKeys,
val plans: Collection<Plan>,
/**
* Size limit for a Message + relative attachments
* TODO does this include mail body, signature, sender, receivers & co?
* Whether the user controls their own keys or not, all free users are Private
*/
val private: Boolean,
val role: Role,
/**
* Key for the organization, available only if user is Admin of organization
*/
val organizationPrivateKey: NotBlankString?,
val currency: NotBlankString, // might not be worth to have an endless enum
/**
* Monetary credits for this user, this value is affected by [currency]
*/
val credits: Int,
val delinquent: Delinquent,
/**
* Size limit for a Message body + sum of attachments
*/
val totalUploadLimit: Bytes,
val dedicatedSpace: UserSpace
)
) : Validable by Validator<User>({
val checkAddresses = addresses.hasAddresses || plans.none { it is Plan.Mail }
val checkKeys = keys.hasKeys || !addresses.hasAddresses
val checkOrganization = role == Role.ORGANIZATION_ADMIN && organizationPrivateKey != null ||
role != Role.ORGANIZATION_ADMIN && organizationPrivateKey == null
val checkPlans = plans.count { it is Plan.Mail } <= 1 &&
plans.count { it is Plan.Vpn } <= 1
checkAddresses && checkKeys && checkOrganization && checkPlans
}) {
init { requireValid() }
}
sealed class Delinquent(val value: UInt, val mailRoutesAccessible: Boolean = true) {
object None : Delinquent(0u)
object InvoiceAvailable : Delinquent(1u)
object InvoiceOverdue : Delinquent(2u)
object InvoiceDelinquent : Delinquent(3u, mailRoutesAccessible = false)
object IncomingMailDisabled : Delinquent(4u, mailRoutesAccessible = false)
}
enum class Role(val i: Int) {
NO_ORGANIZATION(0),
ORGANIZATION_MEMBER(1),
ORGANIZATION_ADMIN(2)
}
// TODO can this entity be used on other spaces under a different name?
data class UserSpace(val total: Bytes, val used: Bytes)

View File

@ -0,0 +1,57 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.domain.entity.user
import ch.protonmail.android.domain.entity.Id
import ch.protonmail.android.domain.entity.NotBlankString
import ch.protonmail.android.domain.entity.Validable
import ch.protonmail.android.domain.entity.Validated
import ch.protonmail.android.domain.entity.Validator
import ch.protonmail.android.domain.entity.requireValid
/**
* Representation of an user's Key
* @author Davide Farella
*/
@Validated
data class UserKey(
val id: Id,
val version: Int,
val privateKey: NotBlankString
)
/**
* [User]s key ring, there can be zero if the users has not set up their mail address yet (i.e. VPN users).
* There can be multiple if the user has done a password reset
*
* @param primaryKey can be `null`, as an [Address] is not required to have keys
* @param keys can be empty only if [primaryKey] is `null`
*/
@Validated
data class UserKeys(
val primaryKey: UserKey?,
val keys: Collection<UserKey>
) : Validable by Validator<UserKeys>({
primaryKey == null && keys.isEmpty() ||
primaryKey in keys
}) {
init { requireValid() }
val hasKeys get() = keys.isNotEmpty()
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.domain.entity.user
import assert4k.*
import ch.protonmail.android.domain.entity.Id
import ch.protonmail.android.domain.entity.ValidationException
import kotlin.test.Test
/**
* Test suite for [AddressKey] and [AddressKeys]
* @author Davide Farella
*/
internal class AddressKeyTest {
@Test
fun `AddressKeys can be created if valid`() {
AddressKeys(
primaryKey = AddressKey(Id("id")),
keys = listOf(AddressKey(Id("id")), AddressKey(Id("another_id")))
)
AddressKeys(
primaryKey = null,
keys = emptyList()
)
}
@Test
fun `AddressKeys fails if primaryKey is null, but keys is NOT empty`() {
assert that fails<ValidationException> {
AddressKeys(
primaryKey = null,
keys = listOf(AddressKey(Id("id")))
)
}
}
@Test
fun `AddressKeys fails if primaryKey is NOT null, but keys is empty`() {
assert that fails<ValidationException> {
AddressKeys(
primaryKey = AddressKey(Id("id")),
keys = emptyList()
)
}
}
@Test
fun `AddressKeys fails if keys does not contain primaryKey`() {
assert that fails<ValidationException> {
AddressKeys(
primaryKey = AddressKey(Id("id")),
keys = listOf(AddressKey(Id("another_id")))
)
}
}
}

View File

@ -0,0 +1,176 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
@file:OptIn(ExperimentalUnsignedTypes::class)
package ch.protonmail.android.domain.entity.user
import assert4k.*
import ch.protonmail.android.domain.entity.Bytes
import ch.protonmail.android.domain.entity.EmailAddress
import ch.protonmail.android.domain.entity.Id
import ch.protonmail.android.domain.entity.Name
import ch.protonmail.android.domain.entity.NotBlankString
import ch.protonmail.android.domain.entity.ValidationException
import ch.protonmail.android.domain.entity.user.Plan.Mail
import ch.protonmail.android.domain.entity.user.Plan.Vpn
import kotlin.test.Test
/**
* Test suite for [User]
* @author Davide Farella
*/
internal class UserTest {
// region addresses
@Test
fun `User can be created if addresses are valid`() {
User(plans = listOf(Vpn.Paid), addresses = Addresses(null, emptyList()))
User(plans = listOf(Mail.Paid), addresses = notEmptyAddresses)
}
@Test
fun `User fails if there are no address by has Mail plan`() {
assert that fails<ValidationException> {
User(plans = listOf(Mail.Paid), addresses = Addresses(null, emptyList()))
}
}
// endregion
// region keys
@Test
fun `User can be created if keys are valid`() {
User(addresses = Addresses(null, emptyList()), keys = UserKeys(null, emptyList()))
User(addresses = notEmptyAddresses, keys = notEmptyKeys)
}
@Test
fun `User fails if there are addresses but no keys`() {
assert that fails<ValidationException> {
User(addresses = notEmptyAddresses, keys = UserKeys(null, emptyList()))
}
}
// endregion
// region plans
@Test
fun `User can be created if plans are valid`() {
User(plans = listOf())
User(plans = listOf(Mail.Free))
User(plans = listOf(Mail.Paid))
User(plans = listOf(Vpn.Free))
User(plans = listOf(Vpn.Paid))
User(plans = listOf(Mail.Free, Vpn.Free))
User(plans = listOf(Mail.Free, Vpn.Paid))
User(plans = listOf(Mail.Paid, Vpn.Free))
User(plans = listOf(Mail.Paid, Vpn.Paid))
}
@Test
fun `User fails if there are 2 Mail plans`() {
assert that fails<ValidationException> { User(plans = listOf(Mail.Free, Mail.Paid)) }
}
@Test
fun `User fails if there are 2 Vpn plans`() {
assert that fails<ValidationException> { User(plans = listOf(Vpn.Free, Vpn.Paid)) }
}
@Test
fun `User fails if there are 2 Mail and 2 Vpn plans`() {
assert that fails<ValidationException> { User(plans = listOf(Mail.Free, Mail.Paid, Vpn.Free, Vpn.Paid)) }
}
// endregion
// region role
@Test
fun `User can be created if role is valid`() {
User(role = Role.NO_ORGANIZATION, organizationPrivateKey = null)
User(role = Role.ORGANIZATION_MEMBER, organizationPrivateKey = null)
User(role = Role.ORGANIZATION_ADMIN, organizationPrivateKey = NotBlankString("key"))
}
@Test
fun `User fails if role is NO_ORGANIZATION but has organization key`() {
assert that fails<ValidationException> {
User(role = Role.NO_ORGANIZATION, organizationPrivateKey = NotBlankString("key"))
}
}
@Test
fun `User fails if role is ORGANIZATION_MEMBER but has organization key`() {
assert that fails<ValidationException> {
User(role = Role.ORGANIZATION_MEMBER, organizationPrivateKey = NotBlankString("key"))
}
}
@Test
fun `User fails if role is ORGANIZATION_ADMIN but has NOT organization key`() {
assert that fails<ValidationException> {
User(role = Role.ORGANIZATION_ADMIN, organizationPrivateKey = null)
}
}
// endregion
private fun User(
addresses: Addresses = notEmptyAddresses,
keys: UserKeys = notEmptyKeys,
role: Role = Role.NO_ORGANIZATION,
organizationPrivateKey: NotBlankString? = null,
plans: Collection<Plan> = emptyList()
) = User(
Id("id"),
Name("davide"),
Name("Davide"),
addresses,
keys,
plans,
false,
role,
organizationPrivateKey,
NotBlankString("Eur"),
0,
Delinquent.None,
Bytes(5_000u),
UserSpace(Bytes(5_000_000u), Bytes(25_000u))
)
private val notEmptyAddresses = Addresses(
Address(
Id("address"),
Id("domainId"),
EmailAddress("dav@protonmail.ch"),
null,
AddressKeys(null, emptyList())
),
listOf(
Address(
Id("address"),
Id("domainId"),
EmailAddress("dav@protonmail.ch"),
null,
AddressKeys(null, emptyList())
)
)
)
private val notEmptyKeys = UserKeys(
UserKey(Id("key"), 4, NotBlankString("key")),
listOf(UserKey(Id("key"), 4, NotBlankString("key")))
)
}