diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b02f06..7caf085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [0.9.0] - 2024-08-2? ### Added -- Application API +- Application & Users API ## [0.8.0] - 2024-08-09 diff --git a/src/main/kotlin/com/vonage/client/kt/Users.kt b/src/main/kotlin/com/vonage/client/kt/Users.kt new file mode 100644 index 0000000..3982689 --- /dev/null +++ b/src/main/kotlin/com/vonage/client/kt/Users.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024 Vonage + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.vonage.client.kt + +import com.vonage.client.users.* + +class Users internal constructor(private val client: UsersClient) { + + fun user(userId: String): ExistingUser = ExistingUser(userId) + + fun user(user: BaseUser): ExistingUser = ExistingUser(user.id) + + inner class ExistingUser internal constructor(val userId: String) { + fun get(): User = client.getUser(userId) + + fun update(properties: User.Builder.() -> Unit): User = + client.updateUser(userId, User.builder().apply(properties).build()) + + fun delete(): Unit = client.deleteUser(userId) + } + + fun create(properties: User.Builder.() -> Unit): User = + client.createUser(User.builder().apply(properties).build()) + + fun list(filter: (ListUsersRequest.Builder.() -> Unit)? = null): ListUsersResponse { + val request = ListUsersRequest.builder() + if (filter == null) { + request.pageSize(100) + } + else { + request.apply(filter) + } + return client.listUsers(request.build()) + } +} diff --git a/src/main/kotlin/com/vonage/client/kt/Vonage.kt b/src/main/kotlin/com/vonage/client/kt/Vonage.kt index 9c0a304..5738635 100644 --- a/src/main/kotlin/com/vonage/client/kt/Vonage.kt +++ b/src/main/kotlin/com/vonage/client/kt/Vonage.kt @@ -31,6 +31,7 @@ class Vonage(init: VonageClient.Builder.() -> Unit) { val simSwap = SimSwap(client.simSwapClient) val sms = Sms(client.smsClient) val subaccounts = Subaccounts(client.subaccountsClient) + val users = Users(client.usersClient) val verify = Verify(client.verify2Client) val verifyLegacy = VerifyLegacy(client.verifyClient) val voice = Voice(client.voiceClient) diff --git a/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt b/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt index a6c384c..d8833cb 100644 --- a/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt @@ -58,6 +58,7 @@ abstract class AbstractTest { protected val country = "GB" protected val secret = "ABCDEFGH01234abc" protected val sipUri = "sip:rebekka@sip.example.com" + protected val websocketUri = "wss://example.com/socket" protected val clientRef = "my-personal-reference" protected val textHexEncoded = "48656c6c6f2c20576f726c6421" protected val entityId = "1101407360000017170" @@ -84,6 +85,10 @@ abstract class AbstractTest { protected val statusCallbackUrl = "$callbackUrl/status" protected val moCallbackUrl = "$callbackUrl/inbound-sms" protected val drCallbackUrl = "$callbackUrl/delivery-receipt" + protected val imageUrl = "$exampleUrlBase/image.jpg" + protected val audioUrl = "$exampleUrlBase/audio.mp3" + protected val videoUrl = "$exampleUrlBase/video.mp4" + protected val fileUrl = "$exampleUrlBase/file.pdf" private val port = 8081 private val wiremock: WireMockServer = WireMockServer( @@ -281,7 +286,7 @@ abstract class AbstractTest { protected inline fun assertApiResponseException( url: String, requestMethod: HttpMethod, actualCall: () -> Any, status: Int, - errorType: String? = null, title: String? = null, + errorType: String? = null, title: String? = null, code: String? = null, detail: String? = null, instance: String? = null): E { val responseParams = mutableMapOf() @@ -289,6 +294,7 @@ abstract class AbstractTest { if (title != null) responseParams["title"] = title if (detail != null) responseParams["detail"] = detail if (instance != null) responseParams["instance"] = instance + if (code != null) responseParams["code"] = code mockRequest(requestMethod, url).mockReturn(status, responseParams) val exception = assertThrows { actualCall.invoke() } @@ -298,6 +304,7 @@ abstract class AbstractTest { assertEquals(title, exception.title) assertEquals(instance, exception.instance) assertEquals(detail, exception.detail) + assertEquals(code, exception.code) return exception } diff --git a/src/test/kotlin/com/vonage/client/kt/MessagesTest.kt b/src/test/kotlin/com/vonage/client/kt/MessagesTest.kt index 18e43c8..50cb21d 100644 --- a/src/test/kotlin/com/vonage/client/kt/MessagesTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/MessagesTest.kt @@ -37,10 +37,6 @@ class MessagesTest : AbstractTest() { private val viberChannel = "viber_service" private val messengerChannel = "messenger" private val caption = "Additional text to accompany the media" - private val imageUrl = "https://example.com/image.jpg" - private val audioUrl = "https://example.com/audio.mp3" - private val videoUrl = "https://example.com/video.mp4" - private val fileUrl = "https://example.com/file.pdf" private val captionMap = mapOf("caption" to caption) diff --git a/src/test/kotlin/com/vonage/client/kt/UsersTest.kt b/src/test/kotlin/com/vonage/client/kt/UsersTest.kt new file mode 100644 index 0000000..b624160 --- /dev/null +++ b/src/test/kotlin/com/vonage/client/kt/UsersTest.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2024 Vonage + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.vonage.client.kt + +import com.vonage.client.common.HttpMethod +import com.vonage.client.users.* +import java.net.URI +import kotlin.test.* + +class UsersTest : AbstractTest() { + private val client = vonage.users + private val authType = AuthType.JWT + private val baseUrl = "/v1/users" + private val userId = "USR-82e028d9-5201-4f1e-8188-604b2d3471ec" + private val userUrl = "$baseUrl/$userId" + private val existingUser = client.user(userId) + private val name = "my_user_name" + private val displayName = "Test User" + private val ttl = 3600 + private val pageSize = 10 + private val customData = mapOf("custom_key" to "custom_value") + private val sipUser = "sip_user" + private val sipPassword = "Passw0rd1234" + private val messengerId = "12345abcd" + private val baseUserRequest = mapOf("name" to name) + private val userIdMapOnly = mapOf("id" to userId) + private val baseUserResponse = userIdMapOnly + baseUserRequest + private val fullUserRequest = baseUserRequest + mapOf( + "display_name" to displayName, + "image_url" to imageUrl, + "properties" to mapOf( + "custom_data" to customData, + "ttl" to ttl + ), + "custom_data" to customData, + "channels" to mapOf( + "sip" to listOf(mapOf( + "uri" to sipUri, + "username" to sipUser, + "password" to sipPassword + )), + "messenger" to listOf(mapOf( + "id" to messengerId + )) + ) + ) + private val fullUserResponse = userIdMapOnly + fullUserRequest + + private fun assertEqualsUser(parsed: User) { + assertEquals(userId, parsed.id) + assertEquals(name, parsed.name) + assertEquals(displayName, parsed.displayName) + assertEquals(URI.create(imageUrl), parsed.imageUrl) + assertEquals(customData, parsed.customData) + val channels = parsed.channels + assertNotNull(channels) + // TODO the rest + } + + private fun assertUserNotFoundException(method: HttpMethod, invocation: Users.ExistingUser.() -> Any) = + assertApiResponseException( + url = userUrl, requestMethod = method, + actualCall = { invocation.invoke(existingUser) }, + status = 404, + title = "Not found.", + errorType = "https://developer.vonage.com/api/conversation#user:error:not-found", + code = "user:error:not-found", + detail = "User does not exist, or you do not have access.", + instance = "00a5916655d650e920ccf0daf40ef4ee" + ) + + private fun assertListUsers(filter: Map, invocation: Users.() -> ListUsersResponse) { + mockGet( + expectedUrl = baseUrl, authType = authType, + expectedQueryParams = filter, + expectedResponseParams = mapOf( + "page_size" to pageSize, + "_embedded" to mapOf( + "users" to listOf( + mapOf(), + mapOf( + "id" to userId, + "name" to name, + "display_name" to displayName, + "_links" to mapOf( + "self" to mapOf( + "href" to "https://api.nexmo.com$userUrl" + ) + ) + ), + userIdMapOnly + ) + ), + "_links" to mapOf( + "first" to mapOf( + "href" to "https://api.nexmo.com/v1/users?order=desc&page_size=10" + ), + "self" to mapOf( + "href" to "https://api.nexmo.com/v1/users?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg%3D" + ), + "next" to mapOf( + "href" to "https://api.nexmo.com/v1/users?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg%3D" + ), + "prev" to mapOf( + "href" to "https://api.nexmo.com/v1/users?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg%3D" + ) + ) + ) + ) + val response = invocation.invoke(client) + assertNotNull(response) + val users = response.users + assertNotNull(users) + assertEquals(3, users.size) + // TODO remaining assertions + } + + @Test + fun `get user`() { + mockGet(expectedUrl = userUrl, authType = authType, expectedResponseParams = fullUserResponse) + assertEqualsUser(existingUser.get()) + assertUserNotFoundException(HttpMethod.GET, Users.ExistingUser::get) + } + + @Test + fun `delete user`() { + mockDelete(expectedUrl = userUrl, authType = authType) + existingUser.delete() + assertUserNotFoundException(HttpMethod.DELETE, Users.ExistingUser::delete) + } + + @Test + fun `update user`() { + val newDisplayName = "Updated DP" + val newPic = "$exampleUrlBase/new_pic.png" + mockPatch( + expectedUrl = userUrl, authType = authType, + expectedRequestParams = userIdMapOnly + mapOf( + "display_name" to newDisplayName, + "image_url" to newPic + ), + expectedResponseParams = fullUserResponse + ) + assertEqualsUser(existingUser.update { + displayName(newDisplayName) + imageUrl(newPic) + }) + + assertUserNotFoundException(HttpMethod.PATCH) { update { }} + } + + @Test + fun `create user required parameters`() { + mockPost( + expectedUrl = baseUrl, authType = authType, + expectedRequestParams = mapOf(), + expectedResponseParams = fullUserResponse + ) + assertEqualsUser(client.create {}) + + assert401ApiResponseException(baseUrl, HttpMethod.POST) { + client.create {} + } + } + + @Test + fun `create user all parameters`() { + mockPost( + expectedUrl = baseUrl, authType = authType, + expectedRequestParams = fullUserRequest, + expectedResponseParams = fullUserResponse + ) + assertEqualsUser(client.create { + name(name) + displayName(displayName) + imageUrl(imageUrl) + customData(customData) + // TODO channels + }) + } + + @Test + fun `list users no filter`() { + assertListUsers(emptyMap(), Users::list) + assert401ApiResponseException(baseUrl, HttpMethod.GET, client::list) + } + + @Test + fun `list users all filters`() { + assertListUsers(mapOf( + "order" to "desc", + "page_size" to pageSize + // TODO remaining filters + )) { + list { + order(ListUsersRequest.SortOrder.DESC) + pageSize(pageSize) + } + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt b/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt index 2bfe797..b35667e 100644 --- a/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt @@ -42,7 +42,6 @@ class VoiceTest : AbstractTest() { private val vbcExt = "4321" private val streamUrl = "$exampleUrlBase/waiting.mp3" private val onAnswerUrl = "$exampleUrlBase/ncco.json" - private val websocketUri = "wss://example.com/socket" private val ringbackTone = "http://example.org/ringbackTone.wav" private val wsContentType = "audio/l16;rate=8000" private val userToUserHeader = "56a390f3d2b7310023a"