diff --git a/instrumentation/twilio-6.6/javaagent/src/test/groovy/test/TwilioClientTest.groovy b/instrumentation/twilio-6.6/javaagent/src/test/groovy/test/TwilioClientTest.groovy deleted file mode 100644 index 6c00a4790876..000000000000 --- a/instrumentation/twilio-6.6/javaagent/src/test/groovy/test/TwilioClientTest.groovy +++ /dev/null @@ -1,628 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package test - -import com.fasterxml.jackson.databind.ObjectMapper -import com.google.common.util.concurrent.ListenableFuture -import com.twilio.Twilio -import com.twilio.exception.ApiException -import com.twilio.http.NetworkHttpClient -import com.twilio.http.Response -import com.twilio.http.TwilioRestClient -import com.twilio.rest.api.v2010.account.Call -import com.twilio.rest.api.v2010.account.Message -import com.twilio.type.PhoneNumber - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.ServerAttributes -import io.opentelemetry.semconv.ErrorAttributes -import io.opentelemetry.semconv.HttpAttributes -import io.opentelemetry.semconv.NetworkAttributes -import io.opentelemetry.semconv.UrlAttributes -import org.apache.http.HttpEntity -import org.apache.http.StatusLine -import org.apache.http.client.methods.CloseableHttpResponse -import org.apache.http.impl.client.CloseableHttpClient -import org.apache.http.impl.client.HttpClientBuilder - -import java.util.concurrent.ExecutionException -import java.util.concurrent.TimeUnit - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.StatusCode.ERROR - -class TwilioClientTest extends AgentInstrumentationSpecification { - final static String ACCOUNT_SID = "abc" - final static String AUTH_TOKEN = "efg" - - final static String MESSAGE_RESPONSE_BODY = """ - { - "account_sid": "AC14984e09e497506cf0d5eb59b1f6ace7", - "api_version": "2010-04-01", - "body": "Hello, World!", - "date_created": "Thu, 30 Jul 2015 20:12:31 +0000", - "date_sent": "Thu, 30 Jul 2015 20:12:33 +0000", - "date_updated": "Thu, 30 Jul 2015 20:12:33 +0000", - "direction": "outbound-api", - "from": "+14155552345", - "messaging_service_sid": "MGXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - "num_media": "0", - "num_segments": "1", - "price": -0.00750, - "price_unit": "USD", - "sid": "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - "status": "sent", - "subresource_uris": { - "media": "/2010-04-01/Accounts/AC14984e09e497506cf0d5eb59b1f6ace7/Messages/SMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Media.json" - }, - "to": "+14155552345", - "uri": "/2010-04-01/Accounts/AC14984e09e497506cf0d5eb59b1f6ace7/Messages/SMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json" - } - """ - - final static String CALL_RESPONSE_BODY = """ - { - "account_sid": "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - "annotation": null, - "answered_by": null, - "api_version": "2010-04-01", - "caller_name": null, - "date_created": "Tue, 31 Aug 2010 20:36:28 +0000", - "date_updated": "Tue, 31 Aug 2010 20:36:44 +0000", - "direction": "inbound", - "duration": "15", - "end_time": "Tue, 31 Aug 2010 20:36:44 +0000", - "forwarded_from": "+141586753093", - "from": "+15017122661", - "from_formatted": "(501) 712-2661", - "group_sid": null, - "parent_call_sid": null, - "phone_number_sid": "PNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - "price": -0.03000, - "price_unit": "USD", - "sid": "CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - "start_time": "Tue, 31 Aug 2010 20:36:29 +0000", - "status": "completed", - "subresource_uris": { - "notifications": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Notifications.json", - "recordings": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Recordings.json", - "feedback": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Feedback.json", - "feedback_summaries": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/FeedbackSummary.json" - }, - "to": "+15558675310", - "to_formatted": "(555) 867-5310", - "uri": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json" - } - """ - - final static String ERROR_RESPONSE_BODY = """ - { - "code": 123, - "message": "Testing Failure", - "code": 567, - "more_info": "Testing" - } - """ - - TwilioRestClient twilioRestClient = Mock() - - def setupSpec() { - Twilio.init(ACCOUNT_SID, AUTH_TOKEN) - } - - def cleanup() { - Twilio.getExecutorService().shutdown() - Twilio.setExecutorService(null) - Twilio.setRestClient(null) - } - - def "synchronous message"() { - setup: - twilioRestClient.getObjectMapper() >> new ObjectMapper() - - 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()), 200) - - Message message = runWithSpan("test") { - Message.creator( - new PhoneNumber("+1 555 720 5913"), // To number - new PhoneNumber("+1 555 555 5215"), // From number - "Hello world!" // SMS body - ).create(twilioRestClient) - } - - expect: - - message.body == "Hello, World!" - - assertTraces(1) { - trace(0, 2) { - span(0) { - name "test" - hasNoParent() - attributes { - } - } - span(1) { - name "MessageCreator.create" - kind CLIENT - attributes { - "twilio.type" "com.twilio.rest.api.v2010.account.Message" - "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" - "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - "twilio.status" "sent" - } - } - } - } - } - - def "synchronous call"() { - setup: - twilioRestClient.getObjectMapper() >> new ObjectMapper() - - 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(CALL_RESPONSE_BODY.getBytes()), 200) - - Call call = runWithSpan("test") { - Call.creator( - new PhoneNumber("+15558881234"), // To number - new PhoneNumber("+15559994321"), // From number - - // Read TwiML at this URL when a call connects (hold music) - new URI("http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient") - ).create(twilioRestClient) - } - - expect: - - call.status == Call.Status.COMPLETED - - assertTraces(1) { - trace(0, 2) { - span(0) { - name "test" - hasNoParent() - attributes { - } - } - span(1) { - name "CallCreator.create" - kind CLIENT - attributes { - "twilio.type" "com.twilio.rest.api.v2010.account.Call" - "twilio.account" "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - "twilio.sid" "CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - "twilio.status" "completed" - } - } - } - } - } - - def "http client"() { - setup: - CloseableHttpClient httpClient = Mock() - httpClient.execute(_) >> mockResponse(MESSAGE_RESPONSE_BODY, 200) - - HttpClientBuilder clientBuilder = Mock() - clientBuilder.build() >> httpClient - - NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder) - - TwilioRestClient realTwilioRestClient = - new TwilioRestClient.Builder("username", "password") - .accountSid(ACCOUNT_SID) - .httpClient(networkHttpClient) - .build() - - Message message = runWithSpan("test") { - Message.creator( - new PhoneNumber("+1 555 720 5913"), // To number - new PhoneNumber("+1 555 555 5215"), // From number - "Hello world!" // SMS body - ).create(realTwilioRestClient) - } - - expect: - - message.body == "Hello, World!" - - assertTraces(1) { - trace(0, 3) { - span(0) { - name "test" - hasNoParent() - attributes { - } - } - span(1) { - name "MessageCreator.create" - kind CLIENT - childOf(span(0)) - attributes { - "twilio.type" "com.twilio.rest.api.v2010.account.Message" - "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" - "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - "twilio.status" "sent" - } - } - span(2) { - name "POST" - kind CLIENT - childOf span(1) - attributes { - "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" - "$ServerAttributes.SERVER_ADDRESS.key" "api.twilio.com" - "$HttpAttributes.HTTP_REQUEST_METHOD.key" "POST" - "$UrlAttributes.URL_FULL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" - "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE.key" 200 - } - } - } - } - } - - def "http client retry"() { - setup: - CloseableHttpClient httpClient = Mock() - httpClient.execute(_) >>> [ - mockResponse(ERROR_RESPONSE_BODY, 500), - mockResponse(MESSAGE_RESPONSE_BODY, 200) - ] - - HttpClientBuilder clientBuilder = Mock() - clientBuilder.build() >> httpClient - - NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder) - - TwilioRestClient realTwilioRestClient = - new TwilioRestClient.Builder("username", "password") - .accountSid(ACCOUNT_SID) - .httpClient(networkHttpClient) - .build() - - Message message = runWithSpan("test") { - Message.creator( - new PhoneNumber("+1 555 720 5913"), // To number - new PhoneNumber("+1 555 555 5215"), // From number - "Hello world!" // SMS body - ).create(realTwilioRestClient) - } - - expect: - message.body == "Hello, World!" - - assertTraces(1) { - trace(0, 4) { - span(0) { - name "test" - hasNoParent() - attributes { - } - } - span(1) { - name "MessageCreator.create" - kind CLIENT - childOf(span(0)) - attributes { - "twilio.type" "com.twilio.rest.api.v2010.account.Message" - "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" - "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - "twilio.status" "sent" - } - } - span(2) { - name "POST" - kind CLIENT - childOf span(1) - status ERROR - attributes { - "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" - "$ServerAttributes.SERVER_ADDRESS.key" "api.twilio.com" - "$HttpAttributes.HTTP_REQUEST_METHOD.key" "POST" - "$UrlAttributes.URL_FULL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" - "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE.key" 500 - "$ErrorAttributes.ERROR_TYPE" "500" - } - } - span(3) { - name "POST" - kind CLIENT - childOf span(1) - attributes { - "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" - "$ServerAttributes.SERVER_ADDRESS.key" "api.twilio.com" - "$HttpAttributes.HTTP_REQUEST_METHOD.key" "POST" - "$UrlAttributes.URL_FULL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" - "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE.key" 200 - } - } - } - } - } - - def "http client retry async"() { - setup: - CloseableHttpClient httpClient = Mock() - httpClient.execute(_) >>> [ - mockResponse(ERROR_RESPONSE_BODY, 500), - mockResponse(MESSAGE_RESPONSE_BODY, 200) - ] - - HttpClientBuilder clientBuilder = Mock() - clientBuilder.build() >> httpClient - - NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder) - - TwilioRestClient realTwilioRestClient = - new TwilioRestClient.Builder("username", "password") - .accountSid(ACCOUNT_SID) - .httpClient(networkHttpClient) - .build() - - Message message = runWithSpan("test") { - ListenableFuture future = Message.creator( - new PhoneNumber("+1 555 720 5913"), // To number - new PhoneNumber("+1 555 555 5215"), // From number - "Hello world!" // SMS body - ).createAsync(realTwilioRestClient) - - try { - return future.get(10, TimeUnit.SECONDS) - } finally { - // Give the future callback a chance to run - Thread.sleep(1000) - } - } - - expect: - message.body == "Hello, World!" - - assertTraces(1) { - trace(0, 4) { - span(0) { - name "test" - hasNoParent() - attributes { - } - } - span(1) { - name "MessageCreator.createAsync" - kind CLIENT - childOf(span(0)) - attributes { - "twilio.type" "com.twilio.rest.api.v2010.account.Message" - "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" - "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - "twilio.status" "sent" - } - } - span(2) { - name "POST" - kind CLIENT - childOf span(1) - status ERROR - attributes { - "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" - "$ServerAttributes.SERVER_ADDRESS.key" "api.twilio.com" - "$HttpAttributes.HTTP_REQUEST_METHOD.key" "POST" - "$UrlAttributes.URL_FULL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" - "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE.key" 500 - "$ErrorAttributes.ERROR_TYPE" "500" - } - } - span(3) { - name "POST" - kind CLIENT - childOf span(1) - attributes { - "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" - "$ServerAttributes.SERVER_ADDRESS.key" "api.twilio.com" - "$HttpAttributes.HTTP_REQUEST_METHOD.key" "POST" - "$UrlAttributes.URL_FULL.key" "https://api.twilio.com/2010-04-01/Accounts/abc/Messages.json" - "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE.key" 200 - } - } - } - } - - cleanup: - Twilio.getExecutorService().shutdown() - Twilio.setExecutorService(null) - Twilio.setRestClient(null) - } - - def "Sync Failure"() { - setup: - - twilioRestClient.getObjectMapper() >> new ObjectMapper() - - 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(ERROR_RESPONSE_BODY.getBytes()), 500) - - when: - runWithSpan("test") { - Message.creator( - new PhoneNumber("+1 555 720 5913"), // To number - new PhoneNumber("+1 555 555 5215"), // From number - "Hello world!" // SMS body - ).create(twilioRestClient) - } - - then: - thrown(ApiException) - - expect: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "test" - status ERROR - errorEvent(ApiException, "Testing Failure") - hasNoParent() - } - span(1) { - name "MessageCreator.create" - kind CLIENT - status ERROR - errorEvent(ApiException, "Testing Failure") - } - } - } - } - - def "root span"() { - setup: - twilioRestClient.getObjectMapper() >> new ObjectMapper() - - 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()), 200) - - Message message = Message.creator( - new PhoneNumber("+1 555 720 5913"), // To number - new PhoneNumber("+1 555 555 5215"), // From number - "Hello world!" // SMS body - ).create(twilioRestClient) - - expect: - - message.body == "Hello, World!" - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "MessageCreator.create" - kind CLIENT - hasNoParent() - attributes { - "twilio.type" "com.twilio.rest.api.v2010.account.Message" - "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" - "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - "twilio.status" "sent" - } - } - } - } - } - - def "asynchronous call"(a) { - setup: - twilioRestClient.getObjectMapper() >> new ObjectMapper() - - 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes()), 200) - - when: - - Message message = runWithSpan("test") { - - ListenableFuture future = Message.creator( - new PhoneNumber("+1 555 720 5913"), // To number - new PhoneNumber("+1 555 555 5215"), // From number - "Hello world!" // SMS body - ).createAsync(twilioRestClient) - - try { - return future.get(10, TimeUnit.SECONDS) - } finally { - // Give the future callback a chance to run - Thread.sleep(1000) - } - } - - then: - - message != null - message.body == "Hello, World!" - - assertTraces(1) { - trace(0, 2) { - span(0) { - name "test" - hasNoParent() - attributes { - } - } - span(1) { - name "MessageCreator.createAsync" - kind CLIENT - attributes { - "twilio.type" "com.twilio.rest.api.v2010.account.Message" - "twilio.account" "AC14984e09e497506cf0d5eb59b1f6ace7" - "twilio.sid" "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - "twilio.status" "sent" - } - } - } - } - - cleanup: - Twilio.getExecutorService().shutdown() - Twilio.setExecutorService(null) - Twilio.setRestClient(null) - - where: - a | _ - 1 | _ - 2 | _ - } - - def "asynchronous error"() { - setup: - twilioRestClient.getObjectMapper() >> new ObjectMapper() - - 1 * twilioRestClient.request(_) >> new Response(new ByteArrayInputStream(ERROR_RESPONSE_BODY.getBytes()), 500) - - when: - runWithSpan("test") { - ListenableFuture future = Message.creator( - new PhoneNumber("+1 555 720 5913"), // To number - new PhoneNumber("+1 555 555 5215"), // From number - "Hello world!" // SMS body - ).createAsync(twilioRestClient) - - try { - return future.get(10, TimeUnit.SECONDS) - } finally { - Thread.sleep(1000) - } - } - - then: - thrown(ExecutionException) - - expect: - - assertTraces(1) { - trace(0, 2) { - span(0) { - name "test" - status ERROR - errorEvent(ApiException, "Testing Failure") - hasNoParent() - } - span(1) { - name "MessageCreator.createAsync" - kind CLIENT - status ERROR - errorEvent(ApiException, "Testing Failure") - } - } - } - } - - private CloseableHttpResponse mockResponse(String body, int statusCode) { - HttpEntity httpEntity = Mock() - httpEntity.getContent() >> { new ByteArrayInputStream(body.getBytes()) } - httpEntity.isRepeatable() >> true - httpEntity.getContentLength() >> body.length() - - StatusLine statusLine = Mock() - statusLine.getStatusCode() >> statusCode - - CloseableHttpResponse httpResponse = Mock() - httpResponse.getEntity() >> httpEntity - httpResponse.getStatusLine() >> statusLine - - return httpResponse - } -} diff --git a/instrumentation/twilio-6.6/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioClientTest.java b/instrumentation/twilio-6.6/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioClientTest.java new file mode 100644 index 000000000000..ff110acff97d --- /dev/null +++ b/instrumentation/twilio-6.6/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/twilio/TwilioClientTest.java @@ -0,0 +1,551 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.twilio; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.ListenableFuture; +import com.twilio.Twilio; +import com.twilio.exception.ApiException; +import com.twilio.http.NetworkHttpClient; +import com.twilio.http.Response; +import com.twilio.http.TwilioRestClient; +import com.twilio.rest.api.v2010.account.Call; +import com.twilio.rest.api.v2010.account.Message; +import com.twilio.type.PhoneNumber; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.apache.http.HttpEntity; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TwilioClientTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final String ACCOUNT_SID = "abc"; + private static final String AUTH_TOKEN = "efg"; + + private static final String MESSAGE_RESPONSE_BODY = + " {\n" + + " \"account_sid\": \"AC14984e09e497506cf0d5eb59b1f6ace7\",\n" + + " \"api_version\": \"2010-04-01\",\n" + + " \"body\": \"Hello, World!\",\n" + + " \"date_created\": \"Thu, 30 Jul 2015 20:12:31 +0000\",\n" + + " \"date_sent\": \"Thu, 30 Jul 2015 20:12:33 +0000\",\n" + + " \"date_updated\": \"Thu, 30 Jul 2015 20:12:33 +0000\",\n" + + " \"direction\": \"outbound-api\",\n" + + " \"from\": \"+14155552345\",\n" + + " \"messaging_service_sid\": \"MGXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\",\n" + + " \"num_media\": \"0\",\n" + + " \"num_segments\": \"1\",\n" + + " \"price\": -0.00750,\n" + + " \"price_unit\": \"USD\",\n" + + " \"sid\": \"MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\",\n" + + " \"status\": \"sent\",\n" + + " \"subresource_uris\": {\n" + + " \"media\": \"/2010-04-01/Accounts/AC14984e09e497506cf0d5eb59b1f6ace7/Messages/SMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Media.json\"\n" + + " },\n" + + " \"to\": \"+14155552345\",\n" + + " \"uri\": \"/2010-04-01/Accounts/AC14984e09e497506cf0d5eb59b1f6ace7/Messages/SMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json\"\n" + + " }"; + private static final String CALL_RESPONSE_BODY = + " {\n" + + " \"account_sid\": \"ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\",\n" + + " \"annotation\": null,\n" + + " \"answered_by\": null,\n" + + " \"api_version\": \"2010-04-01\",\n" + + " \"caller_name\": null,\n" + + " \"date_created\": \"Tue, 31 Aug 2010 20:36:28 +0000\",\n" + + " \"date_updated\": \"Tue, 31 Aug 2010 20:36:44 +0000\",\n" + + " \"direction\": \"inbound\",\n" + + " \"duration\": \"15\",\n" + + " \"end_time\": \"Tue, 31 Aug 2010 20:36:44 +0000\",\n" + + " \"forwarded_from\": \"+141586753093\",\n" + + " \"from\": \"+15017122661\",\n" + + " \"from_formatted\": \"(501) 712-2661\",\n" + + " \"group_sid\": null,\n" + + " \"parent_call_sid\": null,\n" + + " \"phone_number_sid\": \"PNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\",\n" + + " \"price\": -0.03000,\n" + + " \"price_unit\": \"USD\",\n" + + " \"sid\": \"CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\",\n" + + " \"start_time\": \"Tue, 31 Aug 2010 20:36:29 +0000\",\n" + + " \"status\": \"completed\",\n" + + " \"subresource_uris\": {\n" + + " \"notifications\": \"/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Notifications.json\",\n" + + " \"recordings\": \"/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Recordings.json\",\n" + + " \"feedback\": \"/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Feedback.json\",\n" + + " \"feedback_summaries\": \"/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/FeedbackSummary.json\"\n" + + " },\n" + + " \"to\": \"+15558675310\",\n" + + " \"to_formatted\": \"(555) 867-5310\",\n" + + " \"uri\": \"/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json\"\n" + + " }"; + private static final String ERROR_RESPONSE_BODY = + "{\n" + + " \"code\": 123,\n" + + " \"message\": \"Testing Failure\",\n" + + " \"code\": 567,\n" + + " \"more_info\": \"Testing\"\n" + + " }"; + + @Mock private TwilioRestClient twilioRestClient; + + @Mock private CloseableHttpClient httpClient; + + @BeforeAll + static void setUp() { + Twilio.init(ACCOUNT_SID, AUTH_TOKEN); + } + + @AfterAll + static void tearDown() { + Twilio.getExecutorService().shutdown(); + Twilio.setExecutorService(null); + Twilio.setRestClient(null); + } + + @Test + void synchronousMessage() { + when(twilioRestClient.getObjectMapper()).thenReturn(new ObjectMapper()); + when(twilioRestClient.request(any())) + .thenReturn( + new Response( + new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes(StandardCharsets.UTF_8)), + 200)); + + Message message = + testing.runWithSpan( + "test", + () -> + Message.creator( + new PhoneNumber("+1 555 720 5913"), + new PhoneNumber("+1 555 555 5215"), + "Hello world!") + .create(twilioRestClient)); + + assertThat(message.getBody()).isEqualTo("Hello, World!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("MessageCreator.create") + .hasKind(CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("twilio.type"), + "com.twilio.rest.api.v2010.account.Message"), + equalTo( + stringKey("twilio.account"), "AC14984e09e497506cf0d5eb59b1f6ace7"), + equalTo(stringKey("twilio.sid"), "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"), + equalTo(stringKey("twilio.status"), "sent")))); + } + + @Test + void synchronousCall() throws URISyntaxException { + when(twilioRestClient.getObjectMapper()).thenReturn(new ObjectMapper()); + when(twilioRestClient.request(any())) + .thenReturn( + new Response( + new ByteArrayInputStream(CALL_RESPONSE_BODY.getBytes(StandardCharsets.UTF_8)), + 200)); + + Call call = + testing.runWithSpan( + "test", + () -> + Call.creator( + new PhoneNumber("+15558881234"), + new PhoneNumber("+15559994321"), + new URI("http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient")) + .create(twilioRestClient)); + + assertThat(call.getStatus()).isEqualTo(Call.Status.COMPLETED); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("CallCreator.create") + .hasKind(CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("twilio.type"), "com.twilio.rest.api.v2010.account.Call"), + equalTo( + stringKey("twilio.account"), "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"), + equalTo(stringKey("twilio.sid"), "CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"), + equalTo(stringKey("twilio.status"), "completed")))); + } + + @Test + void httpClient() throws IOException { + CloseableHttpResponse response = mockResponse(MESSAGE_RESPONSE_BODY, 200); + when(httpClient.execute(any())).thenReturn(response); + + HttpClientBuilder clientBuilder = getHttpClientBuilder(httpClient); + + NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder); + + TwilioRestClient realTwilioRestClient = + new TwilioRestClient.Builder("username", "password") + .accountSid(ACCOUNT_SID) + .httpClient(networkHttpClient) + .build(); + + Message message = + testing.runWithSpan( + "test", + () -> + Message.creator( + new PhoneNumber("+1 555 720 5913"), + new PhoneNumber("+1 555 555 5215"), + "Hello world!") + .create(realTwilioRestClient)); + + assertThat(message.getBody()).isEqualTo("Hello, World!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("MessageCreator.create") + .hasKind(CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("twilio.type"), + "com.twilio.rest.api.v2010.account.Message"), + equalTo( + stringKey("twilio.account"), "AC14984e09e497506cf0d5eb59b1f6ace7"), + equalTo(stringKey("twilio.sid"), "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"), + equalTo(stringKey("twilio.status"), "sent")))); + } + + @SuppressWarnings("CannotMockMethod") + private static @NotNull HttpClientBuilder getHttpClientBuilder(CloseableHttpClient httpClient) { + HttpClientBuilder clientBuilder = spy(HttpClientBuilder.create()); + when(clientBuilder.build()).thenReturn(httpClient); + return clientBuilder; + } + + @Test + void httpClientRetry() throws IOException { + CloseableHttpResponse response1 = mockResponse(ERROR_RESPONSE_BODY, 500); + CloseableHttpResponse response2 = mockResponse(MESSAGE_RESPONSE_BODY, 200); + when(httpClient.execute(any())).thenReturn(response1, response2); + + HttpClientBuilder clientBuilder = getHttpClientBuilder(httpClient); + + NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder); + + TwilioRestClient realTwilioRestClient = + new TwilioRestClient.Builder("username", "password") + .accountSid(ACCOUNT_SID) + .httpClient(networkHttpClient) + .build(); + + Message message = + testing.runWithSpan( + "test", + () -> + Message.creator( + new PhoneNumber("+1 555 720 5913"), + new PhoneNumber("+1 555 555 5215"), + "Hello world!") + .create(realTwilioRestClient)); + + assertThat(message.getBody()).isEqualTo("Hello, World!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("MessageCreator.create") + .hasParent(trace.getSpan(0)) + .hasKind(CLIENT) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("twilio.type"), + "com.twilio.rest.api.v2010.account.Message"), + equalTo( + stringKey("twilio.account"), "AC14984e09e497506cf0d5eb59b1f6ace7"), + equalTo(stringKey("twilio.sid"), "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"), + equalTo(stringKey("twilio.status"), "sent")))); + } + + @Test + void httpClientRetryAsync() throws Exception { + CloseableHttpResponse response1 = mockResponse(ERROR_RESPONSE_BODY, 500); + CloseableHttpResponse response2 = mockResponse(MESSAGE_RESPONSE_BODY, 200); + when(httpClient.execute(any())).thenReturn(response1, response2); + + HttpClientBuilder clientBuilder = getHttpClientBuilder(httpClient); + + NetworkHttpClient networkHttpClient = new NetworkHttpClient(clientBuilder); + + TwilioRestClient realTwilioRestClient = + new TwilioRestClient.Builder("username", "password") + .accountSid(ACCOUNT_SID) + .httpClient(networkHttpClient) + .build(); + + Message message = + testing.runWithSpan( + "test", + () -> { + ListenableFuture future = + Message.creator( + new PhoneNumber("+1 555 720 5913"), + new PhoneNumber("+1 555 555 5215"), + "Hello world!") + .createAsync(realTwilioRestClient); + + try { + return future.get(10, TimeUnit.SECONDS); + } finally { + Thread.sleep(1000); + } + }); + + assertThat(message.getBody()).isEqualTo("Hello, World!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("MessageCreator.createAsync") + .hasKind(CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("twilio.type"), + "com.twilio.rest.api.v2010.account.Message"), + equalTo( + stringKey("twilio.account"), "AC14984e09e497506cf0d5eb59b1f6ace7"), + equalTo(stringKey("twilio.sid"), "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"), + equalTo(stringKey("twilio.status"), "sent")))); + } + + @Test + void syncFailure() { + when(twilioRestClient.getObjectMapper()).thenReturn(new ObjectMapper()); + when(twilioRestClient.request(any())) + .thenReturn( + new Response( + new ByteArrayInputStream(ERROR_RESPONSE_BODY.getBytes(StandardCharsets.UTF_8)), + 500)); + + assertThatThrownBy( + () -> + testing.runWithSpan( + "test", + () -> + Message.creator( + new PhoneNumber("+1 555 720 5913"), + new PhoneNumber("+1 555 555 5215"), + "Hello world!") + .create(twilioRestClient))) + .isInstanceOf(ApiException.class); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("test") + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(new ApiException("Testing Failure")), + span -> + span.hasName("MessageCreator.create") + .hasKind(CLIENT) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasException(new ApiException("Testing Failure")))); + } + + @Test + void rootSpan() { + when(twilioRestClient.getObjectMapper()).thenReturn(new ObjectMapper()); + when(twilioRestClient.request(any())) + .thenReturn( + new Response( + new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes(StandardCharsets.UTF_8)), + 200)); + + Message message = + Message.creator( + new PhoneNumber("+1 555 720 5913"), + new PhoneNumber("+1 555 555 5215"), + "Hello world!") + .create(twilioRestClient); + + assertThat(message.getBody()).isEqualTo("Hello, World!"); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("MessageCreator.create") + .hasKind(CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("twilio.type"), + "com.twilio.rest.api.v2010.account.Message"), + equalTo( + stringKey("twilio.account"), "AC14984e09e497506cf0d5eb59b1f6ace7"), + equalTo(stringKey("twilio.sid"), "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"), + equalTo(stringKey("twilio.status"), "sent")))); + } + + @Test + void asynchronousCall() throws Exception { + when(twilioRestClient.getObjectMapper()).thenReturn(new ObjectMapper()); + when(twilioRestClient.request(any())) + .thenReturn( + new Response( + new ByteArrayInputStream(MESSAGE_RESPONSE_BODY.getBytes(StandardCharsets.UTF_8)), + 200)); + + Message message = + testing.runWithSpan( + "test", + () -> { + ListenableFuture future = + Message.creator( + new PhoneNumber("+1 555 720 5913"), + new PhoneNumber("+1 555 555 5215"), + "Hello world!") + .createAsync(twilioRestClient); + + try { + return future.get(10, TimeUnit.SECONDS); + } finally { + Thread.sleep(1000); + } + }); + + assertThat(message).isNotNull(); + assertThat(message.getBody()).isEqualTo("Hello, World!"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("test").hasNoParent().hasAttributes(Attributes.empty()), + span -> + span.hasName("MessageCreator.createAsync") + .hasKind(CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + stringKey("twilio.type"), + "com.twilio.rest.api.v2010.account.Message"), + equalTo( + stringKey("twilio.account"), "AC14984e09e497506cf0d5eb59b1f6ace7"), + equalTo(stringKey("twilio.sid"), "MMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"), + equalTo(stringKey("twilio.status"), "sent")))); + } + + @Test + void asynchronousError() { + when(twilioRestClient.getObjectMapper()).thenReturn(new ObjectMapper()); + when(twilioRestClient.request(any())) + .thenReturn( + new Response( + new ByteArrayInputStream(ERROR_RESPONSE_BODY.getBytes(StandardCharsets.UTF_8)), + 500)); + + assertThatThrownBy( + () -> + testing.runWithSpan( + "test", + () -> { + ListenableFuture future = + Message.creator( + new PhoneNumber("+1 555 720 5913"), + new PhoneNumber("+1 555 555 5215"), + "Hello world!") + .createAsync(twilioRestClient); + + try { + return future.get(10, TimeUnit.SECONDS); + } finally { + Thread.sleep(1000); + } + })) + .isInstanceOf(ExecutionException.class); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("test") + .hasNoParent() + .hasStatus(StatusData.error()) + .hasException(new ApiException("Testing Failure")), + span -> + span.hasName("MessageCreator.createAsync") + .hasKind(CLIENT) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasException(new ApiException("Testing Failure")))); + } + + private static CloseableHttpResponse mockResponse(String body, int statusCode) + throws IOException { + HttpEntity httpEntity = mock(HttpEntity.class); + when(httpEntity.getContent()) + .thenReturn(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); + when(httpEntity.isRepeatable()).thenReturn(true); + + StatusLine statusLine = mock(StatusLine.class); + when(statusLine.getStatusCode()).thenReturn(statusCode); + + CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class); + when(httpResponse.getEntity()).thenReturn(httpEntity); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + + return httpResponse; + } +}