Skip to content

Commit 95f8ae2

Browse files
committed
feat: Add Voice.downloadRecording
1 parent 739257b commit 95f8ae2

File tree

4 files changed

+65
-15
lines changed

4 files changed

+65
-15
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77
## [1.0.0] - 2024-10-25 (expected)
88
First stable GA release
99

10+
### Added
11+
- `Voice.downloadRecording(String, Path)` method
12+
1013
### Changed
1114
- `Sms.wasSuccessfullySent()` now an extension function rather than being part of the client
1215
- `Numbers.listOwned()` now returns `List<OwnedNumber>` instead of `ListNumbersResponse`

src/main/kotlin/com/vonage/client/kt/Voice.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.vonage.client.kt
1818
import com.vonage.client.users.channels.Websocket
1919
import com.vonage.client.voice.*
2020
import com.vonage.client.voice.ncco.*
21+
import java.nio.file.Path
2122
import java.time.Instant
2223
import java.util.*
2324

@@ -154,6 +155,15 @@ class Voice internal constructor(private val client: VoiceClient) {
154155
*/
155156
fun createCall(properties: Call.Builder.() -> Unit): CallEvent =
156157
client.createCall(Call.builder().apply(properties).build())
158+
159+
/**
160+
* Download a recording of a call and save it to a file.
161+
*
162+
* @param recordingUrl The URL of the recording to download.
163+
* @param destination Absolute path to save the recording to.
164+
*/
165+
fun downloadRecording(recordingUrl: String, destination: Path): Unit =
166+
client.saveRecording(recordingUrl, destination)
157167
}
158168

159169
/**

src/test/kotlin/com/vonage/client/kt/AbstractTest.kt

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import org.junit.jupiter.api.AfterEach
2727
import org.junit.jupiter.api.BeforeEach
2828
import org.junit.jupiter.api.assertThrows
2929
import com.fasterxml.jackson.databind.ObjectMapper
30+
import com.github.tomakehurst.wiremock.client.MappingBuilder
3031
import com.vonage.client.HttpWrapper
3132
import com.vonage.client.users.channels.Websocket
3233
import java.net.URI
@@ -105,7 +106,7 @@ abstract class AbstractTest {
105106
protected val fileUrl = "$exampleUrlBase/file.pdf"
106107

107108
private val port = 8081
108-
private val wmBaseUrl = "http://localhost:$port"
109+
protected val wmBaseUrl = "http://localhost:$port"
109110
private val wiremock: WireMockServer = WireMockServer(
110111
options().port(port).notifier(ConsoleNotifier(false))
111112
)
@@ -168,28 +169,31 @@ abstract class AbstractTest {
168169

169170
private fun Any.toJson(): String = ObjectMapper().writeValueAsString(this)
170171

172+
private fun MappingBuilder.withAuth(authType: AuthType?): MappingBuilder {
173+
when (authType) {
174+
AuthType.API_KEY_SECRET_QUERY_PARAMS -> {
175+
withFormParam(apiKeyName, equalTo(apiKey))
176+
.withFormParam(apiSecretName, equalTo(apiSecret))
177+
}
178+
AuthType.JWT -> withHeader(authHeaderName, matching(jwtBearerPattern))
179+
AuthType.ACCESS_TOKEN -> withHeader(authHeaderName, equalTo(accessTokenBearer))
180+
AuthType.API_KEY_SECRET_HEADER -> withHeader(authHeaderName, equalTo(basicSecretEncodedHeader))
181+
AuthType.API_KEY_SIGNATURE_SECRET -> withFormParam(apiKeyName, equalTo(apiKey))
182+
null -> Unit
183+
}
184+
return this
185+
}
186+
171187
protected fun mockPostQueryParams(expectedUrl: String, expectedRequestParams: Map<String, Any>,
172188
authType: AuthType? = AuthType.API_KEY_SECRET_QUERY_PARAMS,
173189
contentType: Boolean = false, status: Int = 200,
174190
expectedResponseParams: Any? = null) {
175191

176-
val stub = post(urlPathEqualTo(expectedUrl))
192+
val stub = post(urlPathEqualTo(expectedUrl)).withAuth(authType)
177193
if (contentType) {
178194
stub.withHeader(contentTypeHeaderName, equalTo(ContentType.FORM_URLENCODED.mime))
179195
}
180196

181-
when (authType) {
182-
AuthType.API_KEY_SECRET_QUERY_PARAMS -> {
183-
stub.withFormParam(apiKeyName, equalTo(apiKey))
184-
.withFormParam(apiSecretName, equalTo(apiSecret))
185-
}
186-
AuthType.JWT -> stub.withHeader(authHeaderName, matching(jwtBearerPattern))
187-
AuthType.ACCESS_TOKEN -> stub.withHeader(authHeaderName, equalTo(accessTokenBearer))
188-
AuthType.API_KEY_SECRET_HEADER -> stub.withHeader(authHeaderName, equalTo(basicSecretEncodedHeader))
189-
AuthType.API_KEY_SIGNATURE_SECRET -> stub.withFormParam(apiKeyName, equalTo(apiKey))
190-
null -> Unit
191-
}
192-
193197
expectedRequestParams.forEach {(k, v) -> stub.withFormParam(k, equalTo(v.toString()))}
194198

195199
val response = aResponse().withStatus(status)
@@ -200,6 +204,13 @@ abstract class AbstractTest {
200204
wiremock.stubFor(stub)
201205
}
202206

207+
protected fun mockGetBinary(resourceUrl: String, body: ByteArray, authType: AuthType? = AuthType.JWT) {
208+
wiremock.stubFor(
209+
get(urlPathEqualTo(resourceUrl)).withAuth(authType)
210+
.willReturn(aResponse().withBody(body).withStatus(200))
211+
)
212+
}
213+
203214
protected fun mockRequest(
204215
httpMethod: HttpMethod,
205216
expectedUrl: String,
@@ -283,7 +294,6 @@ abstract class AbstractTest {
283294
mockRequest(HttpMethod.GET, expectedUrl, accept = ContentType.APPLICATION_JSON, authType = authType,
284295
expectedParams = expectedQueryParams).mockReturn(status, expectedResponseParams)
285296

286-
287297
protected fun BuildingStep.mockReturn(
288298
status: Int? = null, expectedBody: Map<String, Any>? = null): ReturnsStep =
289299
returns {

src/test/kotlin/com/vonage/client/kt/VoiceTest.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ package com.vonage.client.kt
1818
import com.vonage.client.common.HttpMethod
1919
import com.vonage.client.voice.*
2020
import com.vonage.client.voice.ncco.*
21+
import org.junit.jupiter.api.Assertions.assertArrayEquals
22+
import java.nio.file.Files
2123
import java.util.*
24+
import kotlin.io.path.deleteExisting
25+
import kotlin.io.path.exists
26+
import kotlin.io.path.readBytes
2227
import kotlin.test.*
2328

2429
class VoiceTest : AbstractTest() {
@@ -406,6 +411,28 @@ class VoiceTest : AbstractTest() {
406411
assertEqualsSampleCallsPage(client.listCalls())
407412
}
408413

414+
@Test
415+
fun `download recording to temp file`() {
416+
val fileName = "$randomUuidStr.wav"
417+
val relativeUrl = "/api.nexmo.com/v1/files/$fileName"
418+
val recordingUrl = wmBaseUrl + relativeUrl
419+
var tempFile = Files.createTempFile(null, null)
420+
tempFile.deleteExisting()
421+
val content = "<110B1NARY0101;>".toByteArray(Charsets.UTF_8)
422+
mockGetBinary(relativeUrl, content)
423+
424+
client.downloadRecording(recordingUrl, tempFile)
425+
assertArrayEquals(content, tempFile.readBytes())
426+
tempFile.deleteExisting()
427+
assertFalse(tempFile.exists())
428+
429+
client.downloadRecording(recordingUrl, tempFile.parent)
430+
tempFile = tempFile.parent.resolve(fileName)
431+
assertTrue(tempFile.exists())
432+
assertArrayEquals(content, tempFile.readBytes())
433+
tempFile.deleteExisting()
434+
}
435+
409436
@Test
410437
fun `create TTS call with required parameters only`() {
411438
val ssmlText = "<speak><prosody rate='fast'>I can speak fast.</prosody></speak>"

0 commit comments

Comments
 (0)