summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-01-12 16:29:03 +0100
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-01-19 11:31:31 +0100
commit1a20b9875f140d4911116dcd683264bb4bebbb64 (patch)
tree088d8f0db342cfaad696319f8bf501c3c71ffe1d /android
parentfccd8f71bcbb33733428999099fc863c943a7165 (diff)
downloadmullvadvpn-1a20b9875f140d4911116dcd683264bb4bebbb64.tar.xz
mullvadvpn-1a20b9875f140d4911116dcd683264bb4bebbb64.zip
Add mockapi test for handling too many devices
Diffstat (limited to 'android')
-rw-r--r--android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/interactor/AppInteractor.kt5
-rw-r--r--android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/CreateAccountMockApiTest.kt7
-rw-r--r--android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/LoginMockApiTest.kt7
-rw-r--r--android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/LogoutMockApiTest.kt5
-rw-r--r--android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/MockApiDispatcher.kt104
-rw-r--r--android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/TooManyDevicesMockApiTest.kt67
-rw-r--r--android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/constant/Constants.kt24
-rw-r--r--android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/util/JsonUtils.kt6
8 files changed, 184 insertions, 41 deletions
diff --git a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/interactor/AppInteractor.kt b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/interactor/AppInteractor.kt
index fd976887ce..ef52a7ef55 100644
--- a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/interactor/AppInteractor.kt
+++ b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/interactor/AppInteractor.kt
@@ -119,6 +119,11 @@ class AppInteractor(
device.findObjectWithTimeout(By.text("Login"), timeout)
}
+ fun attemptToRemoveDevice() {
+ device.findObjectWithTimeout(By.desc("Remove")).click()
+ clickActionButtonByText("Yes, log out device")
+ }
+
private fun String.extractIpAddress(): String {
return split(" ")[1].split(" ")[0]
}
diff --git a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/CreateAccountMockApiTest.kt b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/CreateAccountMockApiTest.kt
index a07a9e5abb..b0ad767e31 100644
--- a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/CreateAccountMockApiTest.kt
+++ b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/CreateAccountMockApiTest.kt
@@ -4,6 +4,8 @@ import net.mullvad.mullvadvpn.lib.common.util.groupWithSpaces
import net.mullvad.mullvadvpn.test.common.extension.clickAgreeOnPrivacyDisclaimer
import net.mullvad.mullvadvpn.test.common.extension.clickAllowOnNotificationPermissionPromptIfApiLevel33AndAbove
import net.mullvad.mullvadvpn.test.common.extension.dismissChangelogDialogIfShown
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_2
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_2
import org.junit.jupiter.api.Test
class CreateAccountMockApiTest : MockApiTest() {
@@ -11,7 +13,10 @@ class CreateAccountMockApiTest : MockApiTest() {
fun testCreateAccountSuccessful() {
// Arrange
val createdAccountToken = "1234123412341234"
- apiDispatcher.apply { expectedAccountToken = createdAccountToken }
+ apiDispatcher.apply {
+ expectedAccountToken = createdAccountToken
+ devicePendingToGetCreated = DUMMY_ID_2 to DUMMY_DEVICE_NAME_2
+ }
app.launch(endpoint)
// Act
diff --git a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/LoginMockApiTest.kt b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/LoginMockApiTest.kt
index 3f3d882835..75925a08d5 100644
--- a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/LoginMockApiTest.kt
+++ b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/LoginMockApiTest.kt
@@ -8,6 +8,9 @@ import net.mullvad.mullvadvpn.test.common.extension.clickAgreeOnPrivacyDisclaime
import net.mullvad.mullvadvpn.test.common.extension.clickAllowOnNotificationPermissionPromptIfApiLevel33AndAbove
import net.mullvad.mullvadvpn.test.common.extension.dismissChangelogDialogIfShown
import net.mullvad.mullvadvpn.test.common.extension.findObjectWithTimeout
+import net.mullvad.mullvadvpn.test.mockapi.constant.DEFAULT_DEVICE_LIST
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_2
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_2
import net.mullvad.mullvadvpn.test.mockapi.util.currentUtcTimeWithOffsetZero
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@@ -46,6 +49,8 @@ class LoginMockApiTest : MockApiTest() {
apiDispatcher.apply {
expectedAccountToken = validAccountToken
accountExpiry = currentUtcTimeWithOffsetZero().plusDays(1)
+ devices = DEFAULT_DEVICE_LIST.toMutableMap()
+ devicePendingToGetCreated = DUMMY_ID_2 to DUMMY_DEVICE_NAME_2
}
// Act
@@ -67,6 +72,8 @@ class LoginMockApiTest : MockApiTest() {
apiDispatcher.apply {
expectedAccountToken = validAccountToken
accountExpiry = currentUtcTimeWithOffsetZero().minusDays(1)
+ devices = DEFAULT_DEVICE_LIST.toMutableMap()
+ devicePendingToGetCreated = DUMMY_ID_2 to DUMMY_DEVICE_NAME_2
}
// Act
diff --git a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/LogoutMockApiTest.kt b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/LogoutMockApiTest.kt
index 266fe73977..1fd266c5d6 100644
--- a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/LogoutMockApiTest.kt
+++ b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/LogoutMockApiTest.kt
@@ -5,6 +5,9 @@ import net.mullvad.mullvadvpn.test.common.extension.clickAgreeOnPrivacyDisclaime
import net.mullvad.mullvadvpn.test.common.extension.clickAllowOnNotificationPermissionPromptIfApiLevel33AndAbove
import net.mullvad.mullvadvpn.test.common.extension.dismissChangelogDialogIfShown
import net.mullvad.mullvadvpn.test.common.extension.findObjectWithTimeout
+import net.mullvad.mullvadvpn.test.mockapi.constant.DEFAULT_DEVICE_LIST
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_2
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_2
import net.mullvad.mullvadvpn.test.mockapi.util.currentUtcTimeWithOffsetZero
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Test
@@ -18,6 +21,8 @@ class LogoutMockApiTest : MockApiTest() {
apiDispatcher.apply {
expectedAccountToken = validAccountToken
accountExpiry = currentUtcTimeWithOffsetZero().plusMonths(1)
+ devices = DEFAULT_DEVICE_LIST.toMutableMap()
+ devicePendingToGetCreated = DUMMY_ID_2 to DUMMY_DEVICE_NAME_2
}
// Act
diff --git a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/MockApiDispatcher.kt b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/MockApiDispatcher.kt
index 74214501a1..821a95f68a 100644
--- a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/MockApiDispatcher.kt
+++ b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/MockApiDispatcher.kt
@@ -6,14 +6,14 @@ import net.mullvad.mullvadvpn.test.mockapi.constant.AUTH_TOKEN_URL_PATH
import net.mullvad.mullvadvpn.test.mockapi.constant.CREATE_ACCOUNT_URL_PATH
import net.mullvad.mullvadvpn.test.mockapi.constant.DEVICES_URL_PATH
import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ACCESS_TOKEN
-import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME
-import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_1
import net.mullvad.mullvadvpn.test.mockapi.constant.LOG_TAG
import net.mullvad.mullvadvpn.test.mockapi.util.accessTokenJsonResponse
import net.mullvad.mullvadvpn.test.mockapi.util.accountCreationJson
import net.mullvad.mullvadvpn.test.mockapi.util.accountInfoJson
import net.mullvad.mullvadvpn.test.mockapi.util.currentUtcTimeWithOffsetZero
import net.mullvad.mullvadvpn.test.mockapi.util.deviceJson
+import net.mullvad.mullvadvpn.test.mockapi.util.tooManyDevicesJsonResponse
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.RecordedRequest
@@ -25,12 +25,17 @@ class MockApiDispatcher : Dispatcher() {
var expectedAccountToken: String? = null
var accountExpiry: DateTime? = null
+ var devices: MutableMap<String, String>? = null
+ private val canAddDevices: Boolean
+ get() = (devices?.size ?: 0) < 5
+
+ var devicePendingToGetCreated: Pair<String, String>? = null
private var cachedPubKeyFromAppUnderTest: String? = null
override fun dispatch(request: RecordedRequest): MockResponse {
Log.d(LOG_TAG, "Request: $request (body=${request.body.peek().readUtf8()})")
- return when (request.path) {
+ return when (request.path ?: "") {
AUTH_TOKEN_URL_PATH -> handleLoginRequest(request.body)
DEVICES_URL_PATH -> {
when (request.method) {
@@ -41,10 +46,29 @@ class MockApiDispatcher : Dispatcher() {
else -> MockResponse().setResponseCode(404)
}
}
- "$DEVICES_URL_PATH/$DUMMY_ID" -> handleDeviceInfoRequest()
ACCOUNT_URL_PATH -> handleAccountInfoRequest()
CREATE_ACCOUNT_URL_PATH -> handleAccountCreationRequest()
- else -> MockResponse().setResponseCode(404)
+ else -> {
+ if (request.path?.contains(DEVICES_URL_PATH) == true) {
+ val deviceId = request.path?.split("/")?.lastOrNull()
+ if (deviceId != null && devices?.contains(deviceId) == true) {
+ when (request.method) {
+ "get",
+ "GET" -> handleDeviceInfoRequest(deviceId)
+ "delete",
+ "DELETE" -> {
+ devices?.remove(deviceId)
+ MockResponse().setResponseCode(204)
+ }
+ else -> MockResponse().setResponseCode(404)
+ }
+ } else {
+ MockResponse().setResponseCode(404)
+ }
+ } else {
+ MockResponse().setResponseCode(404)
+ }
+ }
}.also { response ->
Log.d(LOG_TAG, "Response: $response (body=${response.getBody()?.peek()?.readUtf8()})")
}
@@ -78,20 +102,20 @@ class MockApiDispatcher : Dispatcher() {
MockResponse()
.setResponseCode(200)
.addJsonHeader()
- .setBody(accountInfoJson(id = DUMMY_ID, expiry = expiry).toString())
+ .setBody(accountInfoJson(id = DUMMY_ID_1, expiry = expiry).toString())
}
?: MockResponse().setResponseCode(400)
}
- private fun handleDeviceInfoRequest(): MockResponse {
+ private fun handleDeviceInfoRequest(deviceId: String): MockResponse {
return cachedPubKeyFromAppUnderTest?.let { cachedKey ->
MockResponse()
.setResponseCode(200)
.addJsonHeader()
.setBody(
deviceJson(
- id = DUMMY_ID,
- name = DUMMY_DEVICE_NAME,
+ id = deviceId,
+ name = devices!![deviceId]!!, // Should always exist
publicKey = cachedKey,
creationDate = currentUtcTimeWithOffsetZero().minusDays(1)
)
@@ -106,53 +130,57 @@ class MockApiDispatcher : Dispatcher() {
.getPubKey()
.also { newKey -> cachedPubKeyFromAppUnderTest = newKey }
?.let { newKey ->
- MockResponse()
- .setResponseCode(201)
- .addJsonHeader()
- .setBody(
- deviceJson(
- id = DUMMY_ID,
- name = DUMMY_DEVICE_NAME,
- publicKey = newKey,
- creationDate = currentUtcTimeWithOffsetZero().minusDays(1)
- )
- .toString()
- )
+ if (canAddDevices && devicePendingToGetCreated != null) {
+ MockResponse()
+ .setResponseCode(201)
+ .addJsonHeader()
+ .setBody(
+ deviceJson(
+ id = devicePendingToGetCreated!!.first,
+ name = devicePendingToGetCreated!!.second,
+ publicKey = newKey,
+ creationDate = currentUtcTimeWithOffsetZero().minusDays(1)
+ )
+ .toString()
+ )
+ } else {
+ MockResponse()
+ .setResponseCode(400)
+ .addJsonHeader()
+ .setBody(tooManyDevicesJsonResponse().toString())
+ }
}
?: MockResponse().setResponseCode(400)
}
private fun handleDeviceListRequest(): MockResponse {
return cachedPubKeyFromAppUnderTest?.let { cachedKey ->
- MockResponse()
- .setResponseCode(200)
- .addJsonHeader()
- .setBody(
- JSONArray()
- .put(
- deviceJson(
- id = DUMMY_ID,
- name = DUMMY_DEVICE_NAME,
- publicKey = cachedKey,
- creationDate = currentUtcTimeWithOffsetZero().minusDays(1)
- )
- )
- .toString()
+ val body = JSONArray()
+ devices?.onEachIndexed { index, entry ->
+ body.put(
+ deviceJson(
+ id = entry.key,
+ name = entry.value,
+ publicKey = cachedKey,
+ creationDate = currentUtcTimeWithOffsetZero().minusDays(index + 1)
+ )
)
+ }
+ MockResponse().setResponseCode(200).addJsonHeader().setBody(body.toString())
}
?: MockResponse().setResponseCode(400)
}
private fun handleAccountCreationRequest(): MockResponse {
- return expectedAccountToken?.let { expiry ->
+ return expectedAccountToken?.let { expectedAccountToken ->
MockResponse()
.setResponseCode(201)
.addJsonHeader()
.setBody(
accountCreationJson(
- id = DUMMY_ID,
+ id = DUMMY_ID_1,
expiry = DateTime(),
- accountToken = expectedAccountToken!!
+ accountToken = expectedAccountToken
)
.toString()
)
diff --git a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/TooManyDevicesMockApiTest.kt b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/TooManyDevicesMockApiTest.kt
new file mode 100644
index 0000000000..b4963b5272
--- /dev/null
+++ b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/TooManyDevicesMockApiTest.kt
@@ -0,0 +1,67 @@
+package net.mullvad.mullvadvpn.test.mockapi
+
+import androidx.test.uiautomator.By
+import net.mullvad.mullvadvpn.test.common.extension.clickAgreeOnPrivacyDisclaimer
+import net.mullvad.mullvadvpn.test.common.extension.clickAllowOnNotificationPermissionPromptIfApiLevel33AndAbove
+import net.mullvad.mullvadvpn.test.common.extension.dismissChangelogDialogIfShown
+import net.mullvad.mullvadvpn.test.common.extension.findObjectWithTimeout
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_1
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_2
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_3
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_4
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_5
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_6
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_1
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_2
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_3
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_4
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_5
+import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_6
+import net.mullvad.mullvadvpn.test.mockapi.util.currentUtcTimeWithOffsetZero
+import org.junit.jupiter.api.Test
+
+class TooManyDevicesMockApiTest : MockApiTest() {
+ @Test
+ fun testRemoveDeviceSuccessfulAndLogin() {
+ // Arrange
+ val validAccountToken = "1234123412341234"
+ apiDispatcher.apply {
+ expectedAccountToken = validAccountToken
+ accountExpiry = currentUtcTimeWithOffsetZero().plusMonths(1)
+ devices =
+ mutableMapOf(
+ DUMMY_ID_1 to DUMMY_DEVICE_NAME_1,
+ DUMMY_ID_2 to DUMMY_DEVICE_NAME_2,
+ DUMMY_ID_3 to DUMMY_DEVICE_NAME_3,
+ DUMMY_ID_4 to DUMMY_DEVICE_NAME_4,
+ DUMMY_ID_5 to DUMMY_DEVICE_NAME_5
+ )
+ devicePendingToGetCreated = DUMMY_ID_6 to DUMMY_DEVICE_NAME_6
+ }
+
+ // Act
+ app.launch(endpoint)
+ device.clickAgreeOnPrivacyDisclaimer()
+ device.clickAllowOnNotificationPermissionPromptIfApiLevel33AndAbove()
+ device.dismissChangelogDialogIfShown()
+ app.waitForLoginPrompt()
+ app.attemptLogin(validAccountToken)
+
+ // Assert that we have too many devices
+ device.findObjectWithTimeout(By.text("Too many devices"))
+ // And that the continue with login button is disabled
+ device.findObjectWithTimeout(By.text("Continue with login")).isEnabled
+
+ // Act
+ app.attemptToRemoveDevice()
+
+ // Assert that a device was removed
+ device.findObjectWithTimeout(By.text("Super!"))
+
+ // Act
+ app.clickActionButtonByText("Continue with login")
+
+ // Assert that we are logged in
+ device.findObjectWithTimeout(By.text("UNSECURED CONNECTION"))
+ }
+}
diff --git a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/constant/Constants.kt b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/constant/Constants.kt
index 37e782af11..27099cc67a 100644
--- a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/constant/Constants.kt
+++ b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/constant/Constants.kt
@@ -9,7 +9,27 @@ const val DEVICES_URL_PATH = "/accounts/v1/devices"
const val ACCOUNT_URL_PATH = "/accounts/v1/accounts/me"
const val CREATE_ACCOUNT_URL_PATH = "/accounts/v1/accounts"
-const val DUMMY_ID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
-const val DUMMY_DEVICE_NAME = "mole mole"
+const val DUMMY_ID_1 = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
+const val DUMMY_DEVICE_NAME_1 = "mole mole"
+const val DUMMY_ID_2 = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
+const val DUMMY_DEVICE_NAME_2 = "elom elom"
+const val DUMMY_ID_3 = "cccccccc-cccc-cccc-cccc-cccccccccccc"
+const val DUMMY_DEVICE_NAME_3 = "mole elom"
+const val DUMMY_ID_4 = "dddddddd-dddd-dddd-dddd-dddddddddddd"
+const val DUMMY_DEVICE_NAME_4 = "yellow hat"
+const val DUMMY_ID_5 = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"
+const val DUMMY_DEVICE_NAME_5 = "secure tunnel"
+const val DUMMY_ID_6 = "ffffffff-ffff-ffff-ffff-ffffffffffff"
+const val DUMMY_DEVICE_NAME_6 = "red lobster"
const val DUMMY_ACCESS_TOKEN =
"mva_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+
+val DEFAULT_DEVICE_LIST = mapOf(DUMMY_ID_1 to DUMMY_DEVICE_NAME_1)
+val FULL_DEVICE_LIST =
+ mapOf(
+ DUMMY_ID_1 to DUMMY_DEVICE_NAME_1,
+ DUMMY_ID_2 to DUMMY_DEVICE_NAME_2,
+ DUMMY_ID_3 to DUMMY_DEVICE_NAME_3,
+ DUMMY_ID_4 to DUMMY_DEVICE_NAME_4,
+ DUMMY_ID_5 to DUMMY_DEVICE_NAME_5
+ )
diff --git a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/util/JsonUtils.kt b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/util/JsonUtils.kt
index eea4f3e973..a258d5daf8 100644
--- a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/util/JsonUtils.kt
+++ b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/util/JsonUtils.kt
@@ -30,3 +30,9 @@ fun accessTokenJsonResponse(accessToken: String, expiry: DateTime) =
put("access_token", accessToken)
put("expiry", expiry.formatStrictlyAccordingToIso8601AndRfc3339())
}
+
+fun tooManyDevicesJsonResponse() =
+ JSONObject().apply {
+ put("code", "MAX_DEVICES_REACHED")
+ put("detail", "This account already has the maximum number of devices.")
+ }